Jul 14 - Using CDN - Fix web CSP

This commit is contained in:
2026-07-14 15:13:51 -04:00
parent 81e14fbc63
commit df547eefc2
2 changed files with 14 additions and 1 deletions
+1
View File
@@ -1396,6 +1396,7 @@ timeout = 30
- Backward-compatible: empty URL arrays (older server) → resolver builds the `/static/` URL as before.
**Operator cutover runbook (after 4b ships + Phase 3 sync exits clean):**
- [ ] **CSP:** the web `img-src` must allow the R2 host or the browser blocks presigned image loads. Handled in `app/__init__.py` `set_security_headers` — the R2 endpoint host is derived from `R2_ENDPOINT_URL` and appended to `img-src` automatically when configured (local backend unaffected). If you use a custom R2 domain for presigned URLs, add that host too.
- [ ] Confirm `python scripts/migrate_photos_to_r2.py` prints "✅ SAFE" (0 mismatches) and verified count ≥ Phase 0 baseline present-count.
- [ ] Maintenance window: run the sync once more (delta) → set `STORAGE_BACKEND=s3` in `.env``systemctl restart janitorial-qc`.
- [ ] Smoke test: existing web issue/inspection photos load; existing iPad issue photos load; a **new** upload from web and from iPad lands in R2 and renders; generate an inspection PDF + issue PDF with photos.
+13 -1
View File
@@ -234,6 +234,18 @@ def create_app(config_name='default'):
# ── Security response headers ─────────────────────────────────────────
# Applied to every response. Blocks clickjacking, MIME sniffing, and
# obvious XSS vectors without breaking Bootstrap CDN / Google Fonts.
# Allow R2 presigned photo URLs in the CSP img-src when the s3 storage
# backend is configured. Derived from R2_ENDPOINT_URL (the presigned URL
# host is the same R2 account endpoint), so nothing is hardcoded and the
# local backend is unaffected.
_r2_img_src = ''
_r2_endpoint = app.config.get('R2_ENDPOINT_URL')
if _r2_endpoint:
from urllib.parse import urlparse
_r2_host = urlparse(_r2_endpoint).netloc
if _r2_host:
_r2_img_src = f' https://{_r2_host}'
@app.after_request
def set_security_headers(response):
response.headers.setdefault('X-Content-Type-Options', 'nosniff')
@@ -245,7 +257,7 @@ def create_app(config_name='default'):
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; "
"font-src 'self' data: https://fonts.gstatic.com https://cdn.jsdelivr.net; "
"img-src 'self' data: blob: https://maps.gstatic.com https://maps.googleapis.com; "
f"img-src 'self' data: blob: https://maps.gstatic.com https://maps.googleapis.com{_r2_img_src}; "
"connect-src 'self' https://cdn.jsdelivr.net; "
"frame-src https://maps.google.com https://www.google.com; "
"frame-ancestors 'none';"