What is a Content Security Policy?
A Content Security Policy is a response header that tells the browser which origins are allowed to load scripts, styles, images, fonts, and frames on your page. When a page tries to load something from an origin the policy does not list, the browser blocks it. That is how CSP stops cross-site scripting (XSS) — even if an attacker manages to inject a <script> tag, the browser refuses to execute it because the script came from an origin the policy does not allow.
CSP is one of the few security headers that actually prevents attacks rather than just detecting them. It is the single most effective defence against content-injection bugs, which is why browsers, security scanners, and audit checklists all expect to see one on production sites.
The directives that matter most
A CSP can have dozens of directives, but in practice you only need to set about a dozen to get real protection. The table below explains each one and why it matters.
| Directive | What it controls | Recommended starting value |
|---|---|---|
| default-src | Fallback for any directive you don't set explicitly | 'self' |
| script-src | Where JavaScript can be loaded from | 'self' + nonces/hashes |
| style-src | Where stylesheets and inline styles can come from | 'self' + nonces if needed |
| img-src | Where images can be loaded from | 'self' data: https: |
| connect-src | Where fetch/XHR/WebSocket requests can go | 'self' + your API origin |
| font-src | Where web fonts can be loaded from | 'self' + your CDN |
| frame-src | Where iframes can be embedded from | 'self' + embed domains |
| frame-ancestors | Who is allowed to iframe your page | 'none' unless you're embeddable |
| base-uri | Where <base> can point to | 'self' |
| form-action | Where forms can submit to | 'self' |
| object-src | Where <object>, <embed>, <applet> can load from | 'none' |
| worker-src | Where Web Workers and Service Workers can be loaded from | 'self' |
| manifest-src | Where the PWA manifest can load from | 'self' |
Common mistakes when writing a CSP
- Using
'unsafe-inline'inscript-src. This allows any inline script to run, which is exactly what CSP is meant to block. If you need inline scripts, use a nonce (a random token generated per request) or a hash of the script contents instead. - Forgetting
base-uriandform-action. These are not covered bydefault-src, so leaving them out opens up base-tag hijacking and form-submission redirects. Set both to'self'. - Not listing WebSocket origins.
https://api.example.comdoes not coverwss://api.example.com. If you use WebSockets, list thewss://origin explicitly inconnect-src. - Enforcing before testing. A strict CSP that has never been tested in production will almost always block something legitimate — analytics, fonts, hot-reload scripts, error trackers. Run in report-only mode first.
- Ignoring
object-src. Adobe Flash is dead, but<object>and<embed>can still execute plugins. Setobject-src 'none'and forget about it. - Missing third-party widgets. Chat widgets, review widgets, payment iframes, and embedded videos all need their origins listed in
frame-srcorscript-src. If your support chat disappears after adding CSP, this is why. - Setting a CSP but not checking the report endpoint. Report-only mode sends violations to a configured endpoint. If you never look at the reports, you will not know what is broken until you switch to enforce.
Report-only mode — always start here
Content-Security-Policy-Report-Only is the same header with a different name, and the browser treats it completely differently: instead of blocking violations, it allows everything but logs them to the browser console and sends them to your reporting endpoint. This is how you roll out CSP safely.
The standard workflow is:
- Build a policy here and toggle Report-only.
- Deploy it with a reporting endpoint (a service like report-uri.com, or your own logging endpoint via the
report-todirective). - Use the site normally for a week or two. Every block that would have happened shows up in the console or your report stream.
- Add the missing origins to your policy — this is the slow, boring part, but it is what makes CSP work.
- Once the reports are quiet for a full week, toggle back to Enforce and redeploy.
- Keep the reporting endpoint active so you catch any future regressions.
Nonces and hashes — the safe alternative to 'unsafe-inline'
Most single-page frameworks (React, Next.js, Vue, Svelte, Nuxt) inject inline scripts for hydration. This is why developers often reach for 'unsafe-inline' — but that undoes almost all of CSP's protection. The correct approach is one of two things:
- Nonce. Generate a random string per request, add it to the CSP header as
'nonce-abc123', and add the same value to every<script>tag asnonce="abc123". Next.js and Nuxt have built-in support for this in recent versions. - Hash. Compute the SHA-256, SHA-384, or SHA-512 of the exact inline script contents, base64-encode it, and add it to the header as
'sha256-...'. The hash has to match the script byte-for-byte, so this works best for small, stable inline scripts that rarely change.
Both approaches allow your specific inline scripts to run while blocking anything an attacker injects. This tool supports both — just paste the nonce or hash into the custom source field for script-src.