diff --git a/app/security.py b/app/security.py index 6634fd9..7911f03 100644 --- a/app/security.py +++ b/app/security.py @@ -10,7 +10,7 @@ from starlette.types import ASGIApp from .settings import settings -CSP_DIRECTIVES = ( +CSP_DIRECTIVES_BASE = ( "default-src 'self'; " "script-src 'self' https://cdn.jsdelivr.net; " "style-src 'self' 'unsafe-inline'; " @@ -20,11 +20,16 @@ CSP_DIRECTIVES = ( "base-uri 'self'; " "form-action 'self'; " "frame-ancestors 'none'; " - "object-src 'none'; " - "upgrade-insecure-requests" + "object-src 'none'" ) +def _csp_directives() -> str: + if settings.trust_proxy: + return f"{CSP_DIRECTIVES_BASE}; upgrade-insecure-requests" + return CSP_DIRECTIVES_BASE + + class SecurityHeadersMiddleware(BaseHTTPMiddleware): def __init__(self, app: ASGIApp): super().__init__(app) @@ -34,7 +39,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): response.headers.setdefault("X-Content-Type-Options", "nosniff") response.headers.setdefault("X-Frame-Options", "DENY") response.headers.setdefault("Referrer-Policy", "same-origin") - response.headers.setdefault("Content-Security-Policy", CSP_DIRECTIVES) + response.headers.setdefault("Content-Security-Policy", _csp_directives()) response.headers.setdefault( "Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()", diff --git a/tests/test_security_headers.py b/tests/test_security_headers.py new file mode 100644 index 0000000..3802d67 --- /dev/null +++ b/tests/test_security_headers.py @@ -0,0 +1,23 @@ +"""Security header behavior.""" + +from __future__ import annotations + +from app import security + + +def test_csp_does_not_upgrade_http_when_proxy_not_trusted(app_client, monkeypatch): + monkeypatch.setattr(security.settings, "trust_proxy", False) + + response = app_client.get("/healthz") + + csp = response.headers["content-security-policy"] + assert "upgrade-insecure-requests" not in csp + + +def test_csp_upgrades_requests_when_proxy_is_trusted(app_client, monkeypatch): + monkeypatch.setattr(security.settings, "trust_proxy", True) + + response = app_client.get("/healthz") + + csp = response.headers["content-security-policy"] + assert "upgrade-insecure-requests" in csp