Agent Wranglers

APS-26Recommended

Collect reports of blocked content

Name a place for browsers to send policy errors with Reporting-Endpoints. Log those reports without any personal data.

How to do it

Send Reporting-Endpoints: csp-endpoint="/reports" and point report-to csp-endpoint at it. Also add report-uri /reports for browsers without report-to. Those send one application/csp-report object instead of a list. Accept both types, cap the body at 64 KB and put the endpoint behind the rate limit. Log only the type, path and directive, never the full address or the visitor's IP address. Alert on spikes as Log attacks without personal data says.

Examples

HTTP example

Reporting-Endpoints: csp-endpoint="/reports"
Content-Security-Policy: default-src 'self'; report-to csp-endpoint; report-uri /reports

JavaScript example

// Express: accepts application/reports+json (a list) and application/csp-report (one object)
app.post('/reports', express.json({ type: ['application/reports+json', 'application/csp-report'], limit: '64kb' }), (req, res) => {
  const reports = Array.isArray(req.body) ? req.body : [{ type: 'csp-violation', url: req.body['csp-report']?.['document-uri'], body: { effectiveDirective: req.body['csp-report']?.['effective-directive'] } }];
  for (const r of reports) console.log(r.type, URL.parse(r.url ?? '')?.pathname, r.body?.effectiveDirective);
  res.status(204).end();
});

Python example

# Flask: same two formats, body capped by MAX_CONTENT_LENGTH
from urllib.parse import urlsplit
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024

@app.post("/reports")
def reports():
    data = request.get_json(force=True, silent=True) or []
    if isinstance(data, dict):
        old = data.get("csp-report", {})
        data = [{"type": "csp-violation", "url": old.get("document-uri", ""), "body": {"effectiveDirective": old.get("effective-directive")}}]
    for report in data:
        print(report.get("type"), urlsplit(report.get("url", "")).path, report.get("body", {}).get("effectiveDirective"))
    return "", 204

How to check

Break the policy on a test page in two browsers. Each report appears in the log as one line with a path and a directive. A 1 MB POST answers 413.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/application-security/browser-security-headers/reporting-endpoints">Agent Wranglers application security playbook</a>. In code you write, add a comment next to it with the same link.