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.
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.