|
@@ -3,6 +3,7 @@ import logging
|
|
|
import mimetypes as _mimetypes
|
|
import mimetypes as _mimetypes
|
|
|
import os
|
|
import os
|
|
|
import posixpath
|
|
import posixpath
|
|
|
|
|
+import secrets
|
|
|
import time
|
|
import time
|
|
|
from contextlib import asynccontextmanager
|
|
from contextlib import asynccontextmanager
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from datetime import datetime, timedelta, timezone
|
|
@@ -5147,6 +5148,16 @@ def _frame_ancestors(default_value: str) -> str:
|
|
|
@app.middleware("http")
|
|
@app.middleware("http")
|
|
|
async def security_headers_middleware(request, call_next):
|
|
async def security_headers_middleware(request, call_next):
|
|
|
"""Add standard HTTP security headers to every response."""
|
|
"""Add standard HTTP security headers to every response."""
|
|
|
|
|
+ # Per-request nonce stamped into `script-src` (#1460). On its own this
|
|
|
|
|
+ # changes nothing for Bambuddy's own pages — index.html has no inline
|
|
|
|
|
+ # scripts since the SW registration moved to /sw-register.js. The reason
|
|
|
|
|
+ # it's here is Cloudflare: a CF-fronted deployment has the bot-detection
|
|
|
|
|
+ # script injected into the HTML on the edge, with a fresh hash on every
|
|
|
|
|
+ # load (so hashes can't be allowlisted). When CF sees a nonce in our CSP,
|
|
|
|
|
+ # it clones the same nonce onto its injected <script>, and the inline
|
|
|
|
|
+ # script passes the policy without us needing 'unsafe-inline'. See
|
|
|
|
|
+ # https://developers.cloudflare.com/cloudflare-challenges/challenge-types/javascript-detections/#if-you-have-a-content-security-policy-csp
|
|
|
|
|
+ csp_nonce = secrets.token_urlsafe(16)
|
|
|
response = await call_next(request)
|
|
response = await call_next(request)
|
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
|
# X-Frame-Options is the legacy cross-origin embedding control. Modern
|
|
# X-Frame-Options is the legacy cross-origin embedding control. Modern
|
|
@@ -5199,7 +5210,7 @@ async def security_headers_middleware(request, call_next):
|
|
|
else:
|
|
else:
|
|
|
response.headers["Content-Security-Policy"] = (
|
|
response.headers["Content-Security-Policy"] = (
|
|
|
"default-src 'self'; "
|
|
"default-src 'self'; "
|
|
|
- "script-src 'self'; "
|
|
|
|
|
|
|
+ f"script-src 'self' 'nonce-{csp_nonce}'; "
|
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
|
"img-src 'self' data: blob:; "
|
|
"img-src 'self' data: blob:; "
|
|
|
"media-src 'self' blob:; "
|
|
"media-src 'self' blob:; "
|
|
@@ -5501,7 +5512,11 @@ async def health_check():
|
|
|
return {"status": "healthy"}
|
|
return {"status": "healthy"}
|
|
|
|
|
|
|
|
|
|
|
|
|
-@app.get("/manifest.json")
|
|
|
|
|
|
|
+# GET + HEAD on the three PWA bootstrap routes (#1460). Scanners and a plain
|
|
|
|
|
+# `curl -I` use HEAD; FastAPI's @app.get only registers GET, so HEAD answers
|
|
|
|
|
+# with 405 Method Not Allowed and shows up as a "broken manifest" red herring
|
|
|
|
|
+# in deployment debugging.
|
|
|
|
|
+@app.api_route("/manifest.json", methods=["GET", "HEAD"])
|
|
|
async def serve_manifest():
|
|
async def serve_manifest():
|
|
|
"""Serve PWA manifest."""
|
|
"""Serve PWA manifest."""
|
|
|
manifest_file = app_settings.static_dir / "manifest.json"
|
|
manifest_file = app_settings.static_dir / "manifest.json"
|
|
@@ -5510,7 +5525,7 @@ async def serve_manifest():
|
|
|
return {"error": "Manifest not found"}
|
|
return {"error": "Manifest not found"}
|
|
|
|
|
|
|
|
|
|
|
|
|
-@app.get("/sw.js")
|
|
|
|
|
|
|
+@app.api_route("/sw.js", methods=["GET", "HEAD"])
|
|
|
async def serve_service_worker():
|
|
async def serve_service_worker():
|
|
|
"""Serve service worker."""
|
|
"""Serve service worker."""
|
|
|
sw_file = app_settings.static_dir / "sw.js"
|
|
sw_file = app_settings.static_dir / "sw.js"
|
|
@@ -5523,7 +5538,7 @@ async def serve_service_worker():
|
|
|
return {"error": "Service worker not found"}
|
|
return {"error": "Service worker not found"}
|
|
|
|
|
|
|
|
|
|
|
|
|
-@app.get("/sw-register.js")
|
|
|
|
|
|
|
+@app.api_route("/sw-register.js", methods=["GET", "HEAD"])
|
|
|
async def serve_sw_register():
|
|
async def serve_sw_register():
|
|
|
"""Serve the service-worker registration bootstrap script.
|
|
"""Serve the service-worker registration bootstrap script.
|
|
|
|
|
|