Security

A CSP that actually stops XSS

Most content security policies are decorative. The directive that matters is script-src, and 'unsafe-inline' undoes the whole thing.

8 August 2026 3 min read Mohammad Aaquib Jawed

A Content Security Policy is a list of what a page is allowed to load and execute. Done properly it turns an injected <script> from a compromise into a console error.

Done the way most policies are written, it stops nothing at all.

One directive does the work

script-src is the directive that matters, because script execution is what turns markup injection into account takeover. Every other directive is defence in depth around it.

And there is exactly one thing that neutralises it: 'unsafe-inline'. With that keyword present, injected inline script executes. The policy still shows up in the response headers, still appears in a security scan as "CSP: present", and provides no protection against the attack it exists to stop.

A great many real policies contain it, because it was the fastest way to stop the console errors when the policy was first added.

Nonces are how you remove it

The reason people reach for 'unsafe-inline' is legitimate — applications have inline scripts, and blocking all of them breaks the page.

A nonce solves it. The server generates a random value per response, puts it in the header and on every inline <script> it trusts. Injected script has no nonce, so it does not run.

Two rules make it work. The nonce must be unpredictable and fresh per response — a fixed value, or one derived from something an attacker can obtain, is not a nonce. And it must never be applied to a tag whose content includes user input, because then you have signed the payload yourself.

In Rails the plumbing is a nonce generator plus the directives it applies to, and the framework threads it through the tag helpers. Importmap-based setups nonce their inline tags automatically.

A policy containing 'unsafe-inline' in script-src is documentation, not a control.

Style attributes are a different question

style-src covers stylesheets and inline styles, including style="..." attributes. Applications that pass values through style attributes — CSS custom properties for per-element theming, for example — will break under a strict style-src.

This is a much smaller risk than script. A style attribute cannot execute JavaScript in any current browser; the historical vector for that died with old IE. So allowing inline *attributes* via style-src-attr while keeping stylesheets locked to your own origin is a reasonable trade, and far better than the common shortcut of adding 'unsafe-inline' to style-src wholesale.

Keep the two separate. They are not the same risk and should not get the same concession.

The rest, briefly

object-src 'none' removes a legacy plugin surface with no modern cost. base-uri 'self' stops an injected <base> tag redirecting every relative URL on the page — cheap, and genuinely exploitable when missing. form-action 'self' stops an injected form posting credentials elsewhere. frame-ancestors controls who may embed you, and supersedes X-Frame-Options in modern browsers.

Deploy it without breaking the site

Report-only mode first. The browser evaluates the policy and reports violations without enforcing anything, so you learn what would break before it does. Run it long enough to cover real traffic, not just the paths you thought to click.

Then tighten. Every violation is either something to fix or something to allow deliberately, and writing down which is which is most of the work. The policy is finished when the report is empty and script-src has no 'unsafe-inline' in it.

All writing Reply by email