Add security headers middleware and documentation
- Add SecurityHeadersMiddleware to backend/app/main.py - Implements Content-Security-Policy: default-src 'self' - Implements X-Frame-Options: DENY (clickjacking protection) - Implements X-Content-Type-Options: nosniff (MIME-sniffing protection) - Implements X-XSS-Protection: 1; mode=block (browser XSS filters) - Add CSP meta tag to frontend/index.html for defense-in-depth - Create Docs/Security.md with comprehensive security headers documentation - Add test suite (backend/tests/test_security_headers_middleware.py) with 5 tests - Tests verify headers are present on success and error responses - Tests ensure all four security headers are correctly set - All existing tests continue to pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
110
backend/tests/test_security_headers_middleware.py
Normal file
110
backend/tests/test_security_headers_middleware.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Unit tests for security headers middleware."""
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from app.config import Settings
|
||||
from app.main import create_app
|
||||
|
||||
|
||||
def test_security_headers_middleware_adds_csp_header() -> None:
|
||||
"""Security headers middleware adds Content-Security-Policy header to responses."""
|
||||
settings = Settings(
|
||||
database_path="/tmp/test.db",
|
||||
fail2ban_socket="/tmp/fake_fail2ban.sock",
|
||||
fail2ban_config_dir="/tmp/fail2ban",
|
||||
session_secret="test-secret-key-do-not-use-in-production",
|
||||
session_duration_minutes=60,
|
||||
timezone="UTC",
|
||||
log_level="debug",
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/health")
|
||||
|
||||
assert "Content-Security-Policy" in response.headers
|
||||
assert response.headers["Content-Security-Policy"] == "default-src 'self'"
|
||||
|
||||
|
||||
def test_security_headers_middleware_adds_x_frame_options() -> None:
|
||||
"""Security headers middleware adds X-Frame-Options header to responses."""
|
||||
settings = Settings(
|
||||
database_path="/tmp/test.db",
|
||||
fail2ban_socket="/tmp/fake_fail2ban.sock",
|
||||
fail2ban_config_dir="/tmp/fail2ban",
|
||||
session_secret="test-secret-key-do-not-use-in-production",
|
||||
session_duration_minutes=60,
|
||||
timezone="UTC",
|
||||
log_level="debug",
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/health")
|
||||
|
||||
assert "X-Frame-Options" in response.headers
|
||||
assert response.headers["X-Frame-Options"] == "DENY"
|
||||
|
||||
|
||||
def test_security_headers_middleware_adds_x_content_type_options() -> None:
|
||||
"""Security headers middleware adds X-Content-Type-Options header to responses."""
|
||||
settings = Settings(
|
||||
database_path="/tmp/test.db",
|
||||
fail2ban_socket="/tmp/fake_fail2ban.sock",
|
||||
fail2ban_config_dir="/tmp/fail2ban",
|
||||
session_secret="test-secret-key-do-not-use-in-production",
|
||||
session_duration_minutes=60,
|
||||
timezone="UTC",
|
||||
log_level="debug",
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/health")
|
||||
|
||||
assert "X-Content-Type-Options" in response.headers
|
||||
assert response.headers["X-Content-Type-Options"] == "nosniff"
|
||||
|
||||
|
||||
def test_security_headers_middleware_adds_x_xss_protection() -> None:
|
||||
"""Security headers middleware adds X-XSS-Protection header to responses."""
|
||||
settings = Settings(
|
||||
database_path="/tmp/test.db",
|
||||
fail2ban_socket="/tmp/fake_fail2ban.sock",
|
||||
fail2ban_config_dir="/tmp/fail2ban",
|
||||
session_secret="test-secret-key-do-not-use-in-production",
|
||||
session_duration_minutes=60,
|
||||
timezone="UTC",
|
||||
log_level="debug",
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/health")
|
||||
|
||||
assert "X-XSS-Protection" in response.headers
|
||||
assert response.headers["X-XSS-Protection"] == "1; mode=block"
|
||||
|
||||
|
||||
def test_security_headers_on_all_response_types() -> None:
|
||||
"""Security headers are added to all response types (success and error)."""
|
||||
settings = Settings(
|
||||
database_path="/tmp/test.db",
|
||||
fail2ban_socket="/tmp/fake_fail2ban.sock",
|
||||
fail2ban_config_dir="/tmp/fail2ban",
|
||||
session_secret="test-secret-key-do-not-use-in-production",
|
||||
session_duration_minutes=60,
|
||||
timezone="UTC",
|
||||
log_level="debug",
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
client = TestClient(app)
|
||||
|
||||
# Test on successful response
|
||||
response = client.get("/api/health")
|
||||
assert response.status_code == 200
|
||||
assert "Content-Security-Policy" in response.headers
|
||||
assert "X-Frame-Options" in response.headers
|
||||
assert "X-Content-Type-Options" in response.headers
|
||||
assert "X-XSS-Protection" in response.headers
|
||||
Reference in New Issue
Block a user