Content Security Policy

How to Set Up a CSP Report Endpoint and Monitor Violations

A complete guide to CSP violation monitoring. Learn how to configure a CSP report endpoint with report-uri and report-to, filter browser extension noise, safely roll out policies using report-only mode, and choose the right CSP reporting service for production.

SiteSecurityScore Team·12 min read·Updated Apr 14, 2026
Analytics dashboard displaying data visualizations representing security monitoring and violation reporting

You spent hours building the perfect Content Security Policy. You tested it locally. You deployed it. Everything looked fine. Then on Monday morning, your support inbox is full of users saying the checkout page is broken. Turns out a marketing team member added a new analytics snippet over the weekend that your CSP silently blocked.

This happens all the time. And it is completely preventable. CSP has a built in reporting mechanism that tells you exactly what is being blocked, on which page, and why. Instead of finding out from angry users, you get a JSON report sent to an endpoint you control. You can catch violations in staging, during gradual rollouts, and in production long before anyone notices a problem.

This guide covers how CSP reporting actually works under the hood. The two reporting directives, what violation reports look like, how to set up your own reporting endpoint, how to deal with the avalanche of noise from browser extensions, and the step by step workflow for safely rolling out a CSP in production.

The two reporting directives: report-uri and report-to#

CSP gives you two ways to receive violation reports. The original directive, report-uri, has been around since CSP Level 2 and works in every browser that supports CSP. The newer directive, report-to, is part of the Reporting API specification and is the intended replacement.

Here is the reality: report-uri is technically deprecated, but it still has the broadest browser support. Firefox still does not support report-to for CSP as of early 2026. Chrome and Edge support both. Safari supports report-uri but has limited report-to support. So the practical advice is simple: use both.

Using both directives for maximum compatibility
Content-Security-Policy: default-src 'self'; script-src 'self' 'strict-dynamic'; report-uri /api/csp-report; report-to csp-endpoint Report-To: {"group":"csp-endpoint","max_age":86400, "endpoints":[{"url":"/api/csp-report"}]}

When both directives are present, browsers that support report-to will use it and ignore report-uri. Browsers that only understand report-uri will fall back to it. You point both at the same endpoint, and everything works. You can check the latest browser support on MDN and Can I Use.

How report-to differs under the hood

With report-uri, the browser sends each violation immediately as an individual POST request. With report-to, the browser can batch multiple reports together and send them with a slight delay. This means fewer network requests but slightly delayed delivery. For most use cases, the difference is negligible.

What a violation report actually looks like#

When the browser blocks something (or would have blocked it in report only mode), it sends a JSON POST request to your reporting endpoint. The payload tells you everything you need to diagnose the issue.

Example CSP violation report (report-uri format)
{ "csp-report": { "document-uri": "https://example.com/checkout", "referrer": "https://example.com/cart", "violated-directive": "script-src", "effective-directive": "script-src", "original-policy": "default-src 'self'; script-src 'self'", "blocked-uri": "https://cdn.analytics.com/tracker.js", "status-code": 200, "source-file": "https://example.com/checkout", "line-number": 42, "column-number": 8 } }

Let's break down the fields that matter most:

  • document-uri tells you which page triggered the violation. This is how you know it is the checkout page and not the homepage.
  • violated-directive tells you which CSP directive was violated. In this example, the script-src directive blocked an external script.
  • blocked-uri is the resource that was blocked. This is the most actionable field. It tells you exactly what URL needs to be added to your policy or removed from your page.
  • source-file and line-number point you to the exact location in your code that caused the violation. Not always present, but incredibly helpful when it is.

The report-to format wraps this in a slightly different structure with a body field and uses camelCase keys instead of hyphenated ones, but the information is the same. Your endpoint can handle both formats easily.

Content-Security-Policy-Report-Only: your safety net#

This is the single most important header for CSP deployment. Content-Security-Policy-Report-Only does exactly what the name says. It evaluates your policy against every resource on the page, generates violation reports for anything that does not comply, but does not actually block anything. Your users see zero impact while you collect complete data about what would break.

Report only header (nothing gets blocked)
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; report-uri /api/csp-report

Here is what makes this header so powerful: you can run it alongside an existing enforcing policy. Say you already have a basic CSP in production and you want to tighten it. Deploy the stricter version as Content-Security-Policy-Report-Only while keeping your current policy enforcing. The reports will show you exactly what the stricter policy would block, without affecting any real users.

Running enforcing and report only policies simultaneously
# Current policy (enforcing, keeps your site working) Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' # Stricter policy (report only, testing what would break) Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'strict-dynamic' 'nonce-abc123'; report-uri /api/csp-report

Meta tags do not support reporting

If you deliver your CSP via a <meta> tag, the report-uri and report-to directives are ignored. There is also no meta tag equivalent of Content-Security-Policy-Report-Only. Reporting requires the CSP to be delivered as an HTTP response header.

Building your own reporting endpoint#

You do not need a paid service to start collecting CSP reports. A simple endpoint that accepts POST requests and logs the payload is enough to get started. Here is a minimal Express.js example:

Express.js CSP reporting endpoint
const express = require('express'); const app = express(); // CSP reports come as application/csp-report // or application/reports+json (report-to format) app.use('/api/csp-report', express.json({ type: ['application/csp-report', 'application/json', 'application/reports+json'] })); app.post('/api/csp-report', (req, res) => { const report = req.body['csp-report'] || req.body; // Log the essentials console.log('CSP Violation:', { page: report['document-uri'], blocked: report['blocked-uri'], directive: report['violated-directive'], source: report['source-file'], line: report['line-number'] }); // Store it however you like // database, file, logging service, etc. res.status(204).end(); });

A few things to note about this endpoint:

  • Accept multiple content types. Browsers send reports with different content types depending on whether they use report-uri or report-to. Parse all of them.
  • Return 204 immediately. The browser does not care about the response body. Return an empty 204 as fast as possible so you do not slow down page loads.
  • Rate limit the endpoint. A single broken page can generate thousands of reports per minute across all visitors. Add rate limiting to prevent your reporting endpoint from becoming a performance bottleneck or a vector for abuse.
  • Do not trust report data blindly. Anyone can POST to your reporting endpoint. Do not insert report data directly into a database without sanitizing it. Treat it like any other untrusted user input.

Adding reporting to your server config#

If you set your CSP at the web server level rather than in application code, here is how to add reporting directives in Nginx and Apache:

Nginx configuration
# nginx.conf or site config add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; report-uri /api/csp-report;" always; # When ready to enforce add_header Content-Security-Policy "default-src 'self'; script-src 'self'; report-uri /api/csp-report;" always;
Apache .htaccess
# Report only mode Header set Content-Security-Policy-Report-Only \ "default-src 'self'; script-src 'self'; report-uri /api/csp-report;" # Enforcing mode Header set Content-Security-Policy \ "default-src 'self'; script-src 'self'; report-uri /api/csp-report;"

For platforms like Vercel, Netlify, or Cloudflare Pages, you typically configure headers in a config file (vercel.json, _headers, or the dashboard). The syntax varies, but the CSP string is the same. Just make sure your reporting endpoint is accessible from the same origin or is configured with appropriate CORS headers.

The noise problem: browser extensions and false positives#

The first time you turn on CSP reporting, you will be shocked by the volume. You might see hundreds or thousands of reports within hours. Most of them are not real violations. They are noise from browser extensions.

Here is why: browser extensions like ad blockers, password managers, dark mode tools, and Grammarly inject scripts and styles into every page the user visits. Your CSP does not know the difference between a malicious injection and a legitimate extension. It reports all of them.

This is the number one reason teams give up on CSP reporting. They turn it on, get overwhelmed by noise, and turn it off. Do not do that. Instead, filter the noise.

Filtering browser extension noise in your endpoint
function isExtensionNoise(report) { const blocked = report['blocked-uri'] || ''; const source = report['source-file'] || ''; // Browser extension protocols const extensionPatterns = [ 'chrome-extension://', 'moz-extension://', 'safari-extension://', 'ms-browser-extension://', 'about:', // Firefox internal 'safari-web-extension://' ]; // Check blocked URI and source file return extensionPatterns.some(pattern => blocked.startsWith(pattern) || source.startsWith(pattern) ); } app.post('/api/csp-report', (req, res) => { const report = req.body['csp-report'] || req.body; if (isExtensionNoise(report)) { return res.status(204).end(); // Silently drop } // This is a real violation, log it logViolation(report); res.status(204).end(); });

Beyond extensions, there are a few other common sources of noise to filter:

  • Inline speculative parsing. Some browsers report inline as the blocked URI when they speculatively parse scripts. These are not actual violations on your page.
  • Bookmarklets. Users running bookmarklets trigger violations with blocked-uri set to eval or inline.
  • ISP and hotel wifi injection. Some networks inject their own scripts into HTTP pages. If your site is HTTPS only (and it should be), this should not be an issue. But if you see blocked URIs from unfamiliar domains, this could be the cause.
  • Old cached pages. If a user has a cached version of your page with an old CSP while your server now returns a different policy, the reports may reference directives or resources that no longer apply.

The safe rollout workflow#

Now that you understand the mechanics, here is the step by step process for safely deploying a CSP in production. This is the workflow used by companies that have done this at scale.

Phase 1

Build your initial policy

Use the CSP Generator to create a policy based on what your site actually loads. Start strict. It is easier to loosen a policy after seeing reports than to tighten one that is already too permissive.

Phase 2

Deploy in report only mode

Set the policy as Content-Security-Policy-Report-Only with a report-uri pointing to your endpoint. Deploy to production. Nothing breaks. Reports start flowing.

Phase 3

Monitor and refine for one to two weeks

Filter out browser extension noise and review the real violations. For each one, decide: is this a legitimate resource that needs to be allowlisted, or is this something that should not be on the page? Update your policy accordingly and redeploy (still in report only mode).

Phase 4

Switch to enforcing with reporting

Once your filtered reports are consistently clean, change the header from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep the report-uri directive so you continue receiving reports about anything that gets blocked in production.

Phase 5

Keep monitoring forever

CSP reporting is not a one time setup. New features, updated third party scripts, and team changes will introduce new resources over time. Your reporting endpoint catches these before they become user facing issues. Set up alerts for spikes in violation reports so you are notified quickly.

Dedicated reporting services vs. building your own#

Building your own endpoint works for prototyping and low traffic sites, but it comes with real maintenance overhead. You need to handle rate limiting, storage, deduplication, noise filtering, and some kind of dashboard to make sense of the data. For production sites, a dedicated CSP violation monitoring service saves significant time and gives you better visibility.

SiteSecurityScore CSP Violation Monitoring

SiteSecurityScore's CSP violation monitoring is built specifically for this use case. It collects reports from real browsers visiting your site, automatically filters out browser extension noise, deduplicates identical violations, and presents everything in a dashboard grouped by directive, blocked resource, and page. Setup takes about two minutes: enable reporting for your monitored site, copy the report-uri value from your dashboard, and add it to your CSP header.

Adding SiteSecurityScore as your CSP report endpoint
Content-Security-Policy: default-src 'self'; script-src 'self' 'strict-dynamic'; report-uri https://www.sitesecurityscore.com/api/csp-reports/YOUR-TOKEN

Because SiteSecurityScore already monitors your security headers, TLS, cookies, and DNS, adding CSP violation reporting means all of your security visibility lives in one place. You can correlate header changes detected by scheduled scans with spikes in CSP violations, which makes it easy to trace regressions back to specific deploys.

Other options

  • Report URI is one of the original dedicated services. It aggregates reports, deduplicates them, and provides dashboards for trend analysis. It also supports other reporting headers beyond CSP.
  • Your existing logging stack. If you already run Elasticsearch, Datadog, or CloudWatch, you can pipe CSP reports there with a forwarding endpoint. This works, but you lose the security-specific grouping and filtering that dedicated tools provide.
  • A custom Express/Node.js endpoint (covered earlier in this guide) is fine for development and low traffic sites. For production, plan to add rate limiting, deduplication, and a way to review reports before the volume becomes unmanageable.

Common mistakes with CSP reporting#

Skipping report only mode entirely

Deploying an enforcing CSP without testing it in report only mode first is the fastest way to break your site. Always start with Content-Security-Policy-Report-Only. There is no downside to this step and the data it gives you is invaluable.

Not filtering extension noise

If you do not filter browser extension reports, you will drown in noise and miss the real violations. Set up filtering from day one. It takes five minutes and saves you hours of sorting through irrelevant data.

Removing reporting after enforcement

Some teams remove the report-uri directive once they switch to enforcing mode. This is a mistake. Your site will change over time. New scripts, new CDN providers, new features. Keep reporting active so you know immediately when something new gets blocked.

Using a reporting endpoint without rate limiting

A page with a CSP violation can generate a report for every single visitor. If that page gets a thousand visitors per minute, your reporting endpoint gets a thousand POST requests per minute. Without rate limiting, this can overwhelm your server or rack up costs on managed infrastructure.

Blindly allowlisting everything that gets reported

Not every violation report means you should add the blocked resource to your policy. Some violations are legitimate blocks. A report showing an unfamiliar domain in blocked-uri might be your CSP doing exactly what it should: stopping a suspicious resource from loading. Investigate each violation before loosening your policy.

Testing your reporting setup#

After setting up your endpoint, you want to verify it actually works. Here is a quick way to trigger a test violation:

Triggering a test violation from the browser console
// Open DevTools on a page with your CSP active // Try to inject an inline script (this should be blocked) const s = document.createElement('script'); s.textContent = 'console.log("test violation")'; document.head.appendChild(s); // Check your reporting endpoint for the violation report

You can also test the endpoint directly with a curl command:

Testing the endpoint directly with curl
curl -X POST https://yoursite.com/api/csp-report \ -H "Content-Type: application/csp-report" \ -d '{ "csp-report": { "document-uri": "https://yoursite.com/test", "violated-directive": "script-src", "blocked-uri": "https://evil.example.com/malicious.js", "original-policy": "script-src self" } }'

If your endpoint logs the violation correctly, you are good to go. Run a scan with SiteSecurityScore to confirm the reporting directives are present in your CSP header.

Quick reference: reporting directives at a glance#

DirectiveBrowser supportBehavior
report-uri /endpointAll browsers with CSP supportSends individual POST per violation immediately
report-to group-nameChrome, Edge (not Firefox, limited Safari)Batches reports via Reporting API
Content-Security-Policy-Report-OnlyAll browsers with CSP supportReports violations without blocking anything

The bottom line#

CSP without reporting is like a firewall without logs. You know it is doing something, but you have no idea what it is blocking, what it is missing, or what it is about to break. Reporting transforms CSP from a scary deployment risk into a controlled, data driven process.

The workflow is straightforward. Generate a policy. Deploy it in report only mode. Set up your reporting endpoint. Filter the noise. Review real violations. Refine the policy. Then, and only then, switch to enforcing mode. Keep the reporting active after enforcement so you catch issues as your site evolves.

Every team that has successfully deployed CSP at scale follows some version of this process. The ones who struggle are the ones who skip the reporting step and deploy blind. Do not be that team.

Frequently asked questions#

What is the difference between report-uri and report-to in CSP?

report-uri is the original directive that sends each violation as an individual POST request with application/csp-report content type. report-to uses the newer Reporting API and can batch reports. Use both directives together for maximum browser compatibility.

How does Content-Security-Policy-Report-Only work?

It evaluates your CSP against every resource on the page and sends violation reports for anything that does not comply, but it never blocks anything. This lets you test a policy in production with real traffic without any risk of breaking the user experience.

Why am I getting thousands of CSP violation reports from browser extensions?

Browser extensions inject scripts and styles into every page. These injections violate your CSP and generate reports. Filter these by checking the blocked-uri and source-file fields for extension protocols like chrome-extension:// and moz-extension://.

How long should I run CSP in report only mode before enforcing?

One to two weeks minimum. This covers different pages, user flows, browsers, and edge cases. The signal to move forward is when your filtered violation reports (after removing extension noise) consistently show near zero real violations.

Can I use CSP reporting with a meta tag?

No. The report-uri and report-to directives are ignored when set via a <meta> tag. Reporting only works when the CSP is delivered as an HTTP response header. The Content-Security-Policy-Report-Only header also has no meta tag equivalent.

What is the best CSP violation monitoring tool?

It depends on your setup. For teams that already scan their security headers, SiteSecurityScore combines CSP violation reporting with header, TLS, cookie, and DNS monitoring in one dashboard. Report URI is a standalone option focused solely on reporting headers. For teams with existing observability stacks, forwarding reports to Elasticsearch or Datadog can work, though you lose the security-specific filtering and grouping that dedicated tools provide.

Was this helpful?
Share

Is your CSP reporting set up correctly?

Scan your website to verify your CSP includes reporting directives, or start collecting real violation reports from browsers.