From d272d7aca423c8ffcacef7f209fe62451c2c2891 Mon Sep 17 00:00:00 2001 From: hideyukiMORI Date: Wed, 20 May 2026 00:43:19 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20SecurityHeadersMiddleware=20=E3=81=AE=20?= =?UTF-8?q?CSP=20=E3=82=92=20OpenAPI=20=E3=83=91=E3=82=B9=E3=81=A7?= =?UTF-8?q?=E9=99=A4=E5=A4=96=E3=81=99=E3=82=8B=20(#112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Content-Security-Policy: default-src 'self' が /docs・/redoc・/openapi.json に 付与されると、CDN からアセットを読み込む Swagger UI と ReDoc が動作しなくなる問題を修正。 OpenAPI ドキュメントパスでは CSP ヘッダーのみスキップし、 X-Content-Type-Options 等の他のセキュリティヘッダーは引き続き付与する。 テスト1件追加: /docs・/redoc・/openapi.json への CSP 不付与を確認 Co-Authored-By: Claude Sonnet 4.6 --- src/nene2/middleware/security_headers.py | 13 ++++++++++++- tests/nene2/middleware/test_security_headers.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/nene2/middleware/security_headers.py b/src/nene2/middleware/security_headers.py index 05b6cb8..7593dd1 100644 --- a/src/nene2/middleware/security_headers.py +++ b/src/nene2/middleware/security_headers.py @@ -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 @@ -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 diff --git a/tests/nene2/middleware/test_security_headers.py b/tests/nene2/middleware/test_security_headers.py index aea581d..4b7d533 100644 --- a/tests/nene2/middleware/test_security_headers.py +++ b/tests/nene2/middleware/test_security_headers.py @@ -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)