Skip to content
Merged
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
38 changes: 28 additions & 10 deletions src/document_anonymizer/security/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from starlette.responses import Response

_STATIC_PATH_PREFIX = "/static/"
_SWAGGER_CDN = "https://cdn.jsdelivr.net"
_SWAGGER_PATHS = ("/docs", "/openapi.json")

# Number of random bytes for CSP nonce (16 bytes = 128 bits of entropy)
_CSP_NONCE_BYTES = 16
Expand Down Expand Up @@ -41,16 +43,32 @@ async def dispatch(
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains"
)
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
f"script-src 'self' 'nonce-{csp_nonce}'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data:; "
"font-src 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self'"
)
# Swagger UI (/docs) loads JS/CSS from cdn.jsdelivr.net and
# uses inline scripts that don't carry our nonce. Relax CSP
# for the docs path only — it serves no user data.
is_swagger = request.url.path in _SWAGGER_PATHS
if is_swagger:
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
f"script-src 'self' 'unsafe-inline' {_SWAGGER_CDN}; "
f"style-src 'self' 'unsafe-inline' {_SWAGGER_CDN}; "
f"img-src 'self' data: {_SWAGGER_CDN}; "
"font-src 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self'"
)
else:
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
f"script-src 'self' 'nonce-{csp_nonce}'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data:; "
"font-src 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self'"
)
response.headers["Permissions-Policy"] = (
"camera=(), microphone=(), geolocation=()"
)
Expand Down