iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

CSP Reporting

A Content Security Policy isn’t deploy-and-forget. report-to / report-uri ship browser-side CSP violation reports to your endpoint — the only way to find blocked third-party scripts, inline-script regressions, and probe attempts before users complain.

report-to, endpoint, triage, dashboards

EXAMPLE
// 1) Set up CSP with report endpoint (report-only first)
// Headers from your origin (Express)
import express from 'express';
const app = express();

app.use((req, res, next) => {
    const nonce = crypto.randomBytes(16).toString('base64');
    res.locals.cspNonce = nonce;

    // Report-Only — observe without blocking
    res.setHeader('Content-Security-Policy-Report-Only', [
        "default-src 'self'",
        "script-src 'self' 'nonce-" + nonce + "' https://cdn.example.com",
        "style-src 'self' 'unsafe-inline'",
        "img-src 'self' data: https://cdn.example.com",
        "connect-src 'self' https://api.example.com",
        "frame-ancestors 'none'",
        "base-uri 'self'",
        "object-src 'none'",
        "report-to csp-endpoint",
        "report-uri /csp-report",         // legacy fallback for older browsers
    ].join('; '));

    // Report-To group (Reporting API)
    res.setHeader('Report-To', JSON.stringify({
        group:     'csp-endpoint',
        max_age:   10886400,                // 126 days
        endpoints: [{ url: 'https://reports.example.com/csp' }],
        include_subdomains: true,
    }));

    next();
});

// 2) Receive reports — JSON body parser
app.use('/csp-report', express.json({ type: ['application/csp-report', 'application/reports+json'] }));

app.post('/csp-report', (req, res) => {
    // Legacy report-uri format: { 'csp-report': { ... } }
    // New Report-To format: array of reports
    const reports = Array.isArray(req.body) ? req.body : [req.body['csp-report'] ?? req.body];
    for (const r of reports) {
        const body = r.body ?? r;             // new format wraps body
        log.warn({
            documentUri:  body['document-uri']  ?? body.documentURL,
            blockedUri:   body['blocked-uri']   ?? body.blockedURL,
            violatedDir:  body['violated-directive']  ?? body.effectiveDirective,
            disposition:  body.disposition,    // 'enforce' or 'report'
            sourceFile:   body['source-file']  ?? body.sourceFile,
            lineNumber:   body['line-number']  ?? body.lineNumber,
            sample:       body['script-sample'],
            referrer:     body.referrer,
            ip:           req.ip,
            ua:           req.get('user-agent'),
        }, 'csp violation');
    }
    res.sendStatus(204);
});

// 3) Sample report payload (browser sends)
//   {
//     "csp-report": {
//       "document-uri":      "https://app.example.com/page",
//       "referrer":          "https://app.example.com/dashboard",
//       "violated-directive": "script-src 'self' 'nonce-...'",
//       "effective-directive": "script-src-elem",
//       "original-policy":   "...full CSP...",
//       "blocked-uri":       "https://evil.example.com/x.js",
//       "status-code":       200,
//       "script-sample":     "alert(1)"
//     }
//   }

// 4) report-uri.com — managed service
// • Free tier + paid; aggregates reports, dedupes, dashboards
// • Add: report-uri https://yourgroup.report-uri.com/r/d/csp/enforce
// • Saves you building + scaling your own collector
// Alternatives: Sentry, Datadog RUM, Cloudflare Page Shield

// 5) Real-world report categories
// (a) Blocked scripts from EXTENSIONS — chrome-extension:// origins
//     → ignore; user-installed extensions inject scripts
// (b) Browser-injected inline scripts on iOS Safari
//     → 'inline' source; check user-agent
// (c) Third-party widgets you forgot to allow (analytics, payment)
//     → add to allowlist after security review
// (d) Genuine attack probes — eval, data:text/html, javascript:
//     → these are the signal you're looking for

// 6) Filter the noise (real-world stats: 80-95% is browser extensions)
function isNoise(report) {
    const blocked = report['blocked-uri'] ?? report.blockedURL ?? '';
    return blocked.startsWith('chrome-extension://')
        || blocked.startsWith('moz-extension://')
        || blocked.startsWith('safari-extension://')
        || blocked === 'inline'
        || blocked.includes('about:blank');
}

app.post('/csp-report', (req, res) => {
    const reports = Array.isArray(req.body) ? req.body : [req.body['csp-report'] ?? req.body];
    for (const r of reports) {
        if (isNoise(r.body ?? r)) continue;
        metrics.cspViolation.inc({ directive: (r.body ?? r)['effective-directive'] });
        log.warn(r.body ?? r, 'csp violation (filtered)');
    }
    res.sendStatus(204);
});

// 7) Aggregating: bucket by directive + blocked-uri
// Use Prometheus counters with low-cardinality labels
const cspViolation = new promClient.Counter({
    name: 'csp_violations_total',
    help: 'CSP violations',
    labelNames: ['directive', 'origin', 'environment'],
});

function labelify(uri) {
    try { return new URL(uri).origin; } catch { return 'unknown'; }
}

// 8) Promoting CSP from report-only to enforce
// • Run report-only for 1-2 weeks; analyse + add missing allowlist entries
// • Switch to Content-Security-Policy (enforce); keep report-to active
// • Continue monitoring for new third-party scripts / regressions

// 9) The Reporting API — broader than just CSP
// Same endpoint receives Network Error Logging, Crash Reports, Intervention, Deprecation, etc.
res.setHeader('Report-To', JSON.stringify([
    { group: 'csp-endpoint',    max_age: 10886400, endpoints: [{ url: 'https://reports.example.com/csp' }] },
    { group: 'errors-endpoint', max_age: 10886400, endpoints: [{ url: 'https://reports.example.com/errors' }] },
]));
res.setHeader('NEL', JSON.stringify({
    report_to: 'errors-endpoint', max_age: 10886400, include_subdomains: true,
}));

// 10) Validate your CSP
// • https://csp-evaluator.withgoogle.com — analyses risky directives
// • https://observatory.mozilla.org — grades your headers
// • Browser devtools → Application → Reporting API → see issued reports
// • Test policy with: header tool, curl, then in real browser sessions

// 11) Common pitfalls
// • Listing both report-uri and report-to — modern browsers prefer report-to; old fall back
// • Same-origin endpoint — browsers may not send when fetch is also blocked
// • CORS on the reports endpoint — accept * from any origin (browsers send no auth headers anyway)
// • Heavy report volume bringing down your endpoint — rate limit + drop sampling for very chatty origins
// • Enforcing a policy without going through report-only first → outages
// • Forgetting frame-ancestors equivalent to X-Frame-Options → clickjacking still possible

// 12) Dashboarding
// Grafana panels to build:
//   • Violations / minute (line)
//   • Top blocked URIs (bar)
//   • By directive (pie)
//   • Environment x site (heatmap)
//   • Alert: new directive appearing for first time

// 13) Production tips
// • Sample at the edge (1-10%) if volume is huge
// • Tag reports by app version (CSP includes original-policy → derive)
// • Retain raw reports 30-90 days for forensics
// • Don't store full HTML in samples — PII risk; truncate

// 14) Common bugs
// • Sending CSP from CDN but not origin — fragmented coverage
// • Reports never arrive → check 'Content-Type' on POST; some browsers send application/csp-report
// • report-uri but no Report-To group → only legacy browsers report
// • Inline scripts blocked but CSP says 'unsafe-inline' — nonce mismatch; trust the CSP, fix the script
// • Forgetting connect-src for your reporting domain — reports about reports!
// • Going straight to enforce → silent breakages; ALWAYS start with report-only
// • Tracking nonce mismatch in production → rotate nonce per response, not per session

Why it matters

CSP without reporting is half-deployed: set Report-To + report-uri, start in Content-Security-Policy-Report-Only mode, and triage the reports for 1–2 weeks (filtering browser-extension noise) before enforcing. Counters by directive plus alerts on new violations turn CSP from “set and forget” into a continuous defense.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
Content-Security-Policy-Report-Only:
    default-src 'self';
    report-to csp-endpoint;
Reporting-Endpoints: csp-endpoint="https://example.com/csp"
// Collect violations from real users, then ratchet the policy.
Try it Yourself »

Discussion

Loading…