Skip to content

security: Content-Security-Policy (report-only) + /api/csp-report - #174

Merged
snackman merged 1 commit into
masterfrom
security/csp-report-only
Oct 1, 2026
Merged

snackman merged 1 commit into
masterfrom
security/csp-report-only

Conversation

@snackman

@snackman snackman commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a Content-Security-Policy-Report-Only header (and Reporting-Endpoints) in next.config.ts headers(), next to the existing security headers. Nothing gets blocked. Browsers only report violations.
  • Adds POST /api/csp-report, which accepts application/csp-report (from report-uri) and application/reports+json (from report-to). It rejects other content types (415) and bodies over 10KB (413, checked from Content-Length and again while streaming). It parses at most 10 reports, cuts each field to 200 chars and removes query strings from URLs, then logs one compact [csp] {...} line per violation via console.warn. There are no DB writes.
  • Unit tests in src/lib/__tests__/csp-report.test.ts.

Policy

default-src 'self';
script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.googletagmanager.com https://maps.googleapis.com https://accounts.google.com https://cdn.jsdelivr.net https://vercel.live;
connect-src 'self' https://*.supabase.co wss://*.supabase.co https://api.mapbox.com https://events.mapbox.com https://*.google-analytics.com https://*.analytics.google.com https://www.google.com https://*.googletagmanager.com https://maps.googleapis.com https://places.googleapis.com https://accounts.google.com https://www.googleapis.com https://cdn.jsdelivr.net https://vercel.live wss://ws-us3.pusher.com;
img-src 'self' data: blob: https:;
style-src 'self' 'unsafe-inline' https://accounts.google.com https://fonts.googleapis.com;
font-src 'self' data: https://fonts.gstatic.com https://vercel.live;
frame-src 'self' https://lu.ma https://luma.com https://*.luma.com https://accounts.google.com https://vercel.live;
worker-src 'self' blob:;
child-src 'self' blob:;
manifest-src 'self';
media-src 'self' https:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
report-uri /api/csp-report;
report-to csp-endpoint

Why the less obvious entries are there:

  • 'unsafe-inline' scripts: Next's inline bootstrap and RSC payload, the theme script and JSON-LD in layout.tsx, and the inline gtag config. Using nonces would mean adding a proxy and rendering every page dynamically, so that is left for later.
  • www.google.com: GA4 sends its /g/collect beacons there.
  • cdn.jsdelivr.net: Chart.js on /admin/analytics.
  • vercel.live and pusher: the Vercel toolbar on preview deployments.

Verification

  • Ran next build && next start with Playwright (Chromium) and collected [Report Only] console messages on:
    • /
    • /kbw2026 in Map, List, Table and Gallery views (scrolled)
    • /kbw2026 with the sign-in modal open
    • /itinerary, /admin, /admin/analytics, /sponsors, /orgs
    • a page with an injected Luma embed iframe
  • First pass: the only violations were GA beacons to www.google.com, which are now allowed. Final pass: 0 violations.
  • Sent test reports to the endpoint with curl: both formats return 204 and log [csp] lines, a 20KB body (plain and chunked) returns 413, and text/plain returns 415.
  • npx tsc --noEmit passes. npm test passes (286 tests).

Rollout plan

  1. Merge and run report-only for about 1 week.
  2. Check Vercel logs for [csp] lines. Add legitimate sources to the policy, and ignore noise from browser extensions (chrome-extension:, moz-extension:, etc.).
  3. Once the logs are clean, rename the header in next.config.ts from Content-Security-Policy-Report-Only to Content-Security-Policy to enforce it. Keep the reporting directives.
  4. Later: tighten script-src with nonces or hashes to remove 'unsafe-inline'.

🤖 Generated with Claude Code

…report

Adds a Content-Security-Policy-Report-Only header (plus Reporting-Endpoints)
built from what the app actually loads (Next inline scripts, gtag, Mapbox,
Supabase https/wss, Google Maps/Places/GIS, Chart.js CDN, Luma iframes,
Vercel toolbar). Violations POST to /api/csp-report, which accepts both
application/csp-report and application/reports+json, caps bodies at 10KB,
strips query strings, and logs compact [csp] lines via console.warn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sheeets Ready Ready Preview Oct 1, 2026 11:35am UTC

Request Review

@snackman
snackman merged commit 3e064b1 into master Oct 1, 2026
3 checks passed

This branch was successfully deployed

1 active deployment
Preview — 5cc17d0c Deployed Oct 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant