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
Related rules
- APS-49 Limit how often anyone can call your APIs in Application security
- APS-80 Log attacks without personal data in Application security
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.