Content-Security-Policy from scratch
Published May 27, 2026. About a seven-minute read.
Content-Security-Policy is the most powerful security header available, and the most frequently missed. The reason for both is the same. CSP works by enumerating exactly which sources the browser is allowed to load resources from. Done correctly, it shuts down cross-site scripting, mixed-content sneaks, and most third-party script-inclusion attacks. Done wrong, it breaks the site for real users. The fear of breaking the site is why so many teams stop at the report-only stage and never finish.
This guide is the path from no CSP to an enforced CSP, in the order you should walk it.
Step 1: understand what CSP actually controls
A CSP is a list of directives. Each directive controls one kind of resource and tells the browser which sources are allowed. The most important directives:
default-srcsets the fallback for any directive you do not explicitly list.script-srccontrols where JavaScript can be loaded from.style-srccontrols where CSS can be loaded from.img-srccontrols images.font-srccontrols font files.connect-srccontrols fetch, XHR, WebSocket, and similar network calls from JavaScript.frame-srccontrols what your page can embed in iframes.frame-ancestorscontrols what pages can embed your page in iframes. This replaces X-Frame-Options.form-actioncontrols where forms can submit.base-uricontrols what values are allowed for the<base>tag (subtle but important against certain injection attacks).object-srccontrols plugins. Almost always set tonone.
Each value is a source list: 'self' (same origin), a specific domain (https://cdn.example.com), a scheme (https:, data:), a keyword ('none', 'unsafe-inline', 'unsafe-eval'), or a nonce or hash.
Step 2: deploy in report-only mode first
The single best decision you can make is to start with Content-Security-Policy-Report-Only instead of Content-Security-Policy. Report-only sends violation reports but does not block anything. The site keeps working even when your policy is wrong, which gives you time to fix the policy without an outage.
A starter report-only policy:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
report-to csp-endpointPair it with a Report-To header pointing at a reporting endpoint (your own or a service like report-uri.com or Sentry). The browser POSTs a structured JSON document every time a resource on a real page would have been blocked.
Step 3: read the reports for a week
Real users will hit corners of the site you forgot about. Marketing pixels, A/B testing scripts, embedded videos, a sentry.io script tag added by another team, a Google Maps iframe on the contact page. Every one of these will show up as a report.
Refine the policy by adding the legitimate sources to the relevant directives. Resist the urge to lump everything into default-src. Listing each domain under the correct directive (script-src, style-src, etc.) keeps the policy tight.
Step 4: kill inline scripts and styles
Inline scripts (<script>...</script>) and inline styles (<style>...</style> or style="...") cannot be allowed by source alone. You have three options:
- Refactor them out of the HTML into external files. This is the cleanest answer but the most work.
- Add a
nonceto each inline block and to the CSP'sscript-srcandstyle-src. Every response gets a different nonce. Frameworks like Next.js have built-in middleware support for this. - Hash each inline block and list the hash in the policy. Workable only when the inline content is static.
Avoid 'unsafe-inline'. It defeats most of CSP's value against XSS, because an attacker who can inject a <script> tag can now run their code.
Step 5: switch to enforcement
Once the reports have been quiet for a week or two, change the header name from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep the report-to directive so genuine new violations still get logged. Watch the error rate and the support channel for 24 hours.
Common pitfalls
- Forgetting
connect-src. Modern SPAs make API calls to many places. If you do not list them, fetch and XHR will be blocked silently. - Setting
frame-ancestorson a<meta>CSP. The directive only works when delivered as an HTTP header. Meta-tag CSPs ignore it. - Adding
'unsafe-eval'just to make something work. It is rarely required. If it is, isolate the code that needs it. - Treating CSP as a one-time setup. Every new third-party tool added to the site (analytics, chat widgets, video embeds) needs a CSP update. Wire it into the change- management process.
A reasonable end-state policy
For a typical content site or marketing page:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{REQUEST_NONCE}' https://www.googletagmanager.com;
style-src 'self' 'nonce-{REQUEST_NONCE}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.yourdomain.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
upgrade-insecure-requestsThe exact shape depends on what you load. The discipline is to list every domain explicitly, never lean on 'unsafe-inline' as a permanent state, and use report-only any time you change something significant.