Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/nene2/middleware/security_headers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Security headers middleware.

Adds defensive HTTP headers to every response.
CSP is skipped for OpenAPI documentation paths (/docs, /redoc, /openapi.json)
because Swagger UI loads assets from CDN which would be blocked by default-src 'self'.
"""

from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
Expand All @@ -15,12 +17,21 @@
"Permissions-Policy": "geolocation=(), microphone=()",
}

_OPENAPI_PATHS: frozenset[str] = frozenset({"/docs", "/redoc", "/openapi.json"})


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""Attach security headers to every HTTP response."""
"""Attach security headers to every HTTP response.

Content-Security-Policy is omitted for OpenAPI documentation paths so that
Swagger UI and ReDoc (which load assets from CDN) continue to work in development.
"""

async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
response = await call_next(request)
is_openapi_path = request.url.path in _OPENAPI_PATHS
for header, value in _HEADERS.items():
if is_openapi_path and header == "Content-Security-Policy":
continue
response.headers[header] = value
return response
10 changes: 10 additions & 0 deletions tests/nene2/middleware/test_security_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ def test_security_headers_present() -> None:
assert "Permissions-Policy" in response.headers


def test_csp_absent_on_openapi_paths() -> None:
client = TestClient(_make_app())
for path in ("/docs", "/redoc", "/openapi.json"):
response = client.get(path)
assert "Content-Security-Policy" not in response.headers, (
f"CSP should not be set for {path}"
)
assert response.headers["X-Content-Type-Options"] == "nosniff"


def test_security_headers_on_error_response() -> None:
app = FastAPI()
app.add_middleware(SecurityHeadersMiddleware)
Expand Down
Loading