CSP Violation Reports
Production responses ship with both legacy (report-uri) and modern
(report-to + Reporting-Endpoints) CSP reporting. Browsers POST violations
to /api/public/csp-report, which writes them to the csp_reports table.
- Endpoint:
src/routes/api/public/csp-report.ts(usessupabaseAdmin) - Table:
public.csp_reports(RLS — onlyowner-role users can read) - Wiring:
src/lib/security-headers.tsaddsreport-uri,report-to, and theReporting-Endpointsheader. Reporting is production-only — dev responses skip it to avoid noise from HMR.
Why we collect them
CSP changes can silently break features (a new third-party script, an inline style added by a dependency, a redirect to a new auth provider). Reports let us catch these regressions in production without users telling us.
Reviewing reports
You need an owner role on at least one tenant. Run via Lovable Cloud →
Database → SQL editor (or any psql session):
1. What's broken right now? (last 24 h)
SELECT
effective_directive,
blocked_uri,
COUNT(*) AS hits,
MAX(received_at) AS last_seen
FROM public.csp_reports
WHERE received_at > now() - INTERVAL '24 hours'
GROUP BY effective_directive, blocked_uri
ORDER BY hits DESC
LIMIT 50;
2. Drill into a specific violation
SELECT received_at, document_uri, source_file, line_number, user_agent, raw
FROM public.csp_reports
WHERE blocked_uri = 'https://example.com/some-script.js'
ORDER BY received_at DESC
LIMIT 20;
3. Per-directive volume (last 7 days)
SELECT effective_directive, COUNT(*) AS hits
FROM public.csp_reports
WHERE received_at > now() - INTERVAL '7 days'
GROUP BY effective_directive
ORDER BY hits DESC;
Triage playbook
For each high-volume violation, decide one of:
| Decision | When | Action |
|---|---|---|
| Allow | Legitimate dependency we just added (e.g. new font CDN, OAuth provider). | Add the source to the matching directive in baseCspProd (src/lib/security-headers.ts). Re-run tests. |
| Block (intentional) | Browser extension, malicious injection, or removed feature. | Do nothing — the block is correct. Optionally add a SQL filter to ignore the noise. |
| Refactor | Inline <script> or <style> we control. | Move to an external file or a hashed inline. Avoid loosening script-src to 'unsafe-inline'. |
| Per-route override | Only one sensitive page needs a different policy. | Add to securityOverrides in src/lib/security-headers.ts instead of widening the global CSP. |
Common false positives to ignore
blocked_uri = 'about','data','inline','eval'from browser extensions (especially on Chrome with Grammarly, password managers, etc.).- Old client tabs that loaded a previous CSP version — the violation will stop after they refresh.
effective_directive = 'script-src-elem'fromchrome-extension://...sources — always extension noise.
Filter them out of dashboards with:
AND blocked_uri NOT IN ('about', 'data', 'inline', 'eval')
AND blocked_uri NOT LIKE 'chrome-extension://%'
AND blocked_uri NOT LIKE 'moz-extension://%'
AND blocked_uri NOT LIKE 'safari-extension://%'
Operational hygiene
- Retention: prune reports older than 90 days. Run monthly:
DELETE FROM public.csp_reports WHERE received_at < now() - INTERVAL '90 days'; - Volume guard: the endpoint rejects payloads > 32 KB and silently drops
invalid JSON. If
csp_reportsrow count spikes, check for an attacker POSTing junk and rate-limit at the edge. - Pre-deploy: after any change to
baseCspProd, watchcsp_reports.received_at > now() - INTERVAL '15 minutes'for ~30 min post-publish. A burst of new directives means the change broke something.
Testing the endpoint locally
curl -X POST http://localhost:3000/api/public/csp-report \
-H 'Content-Type: application/csp-report' \
-d '{"csp-report":{"document-uri":"http://localhost:3000/","violated-directive":"script-src","blocked-uri":"https://evil.example.com/x.js","effective-directive":"script-src","original-policy":"default-src self","disposition":"enforce"}}'
Expect HTTP 204. Then:
SELECT * FROM public.csp_reports ORDER BY received_at DESC LIMIT 1;