A Content Security Policy (CSP) is an allowlist, delivered as an HTTP header, that tells the browser which sources of scripts, styles, images, frames and connections your page may use. Its main job is to limit the damage from cross-site scripting (XSS): even if an attacker injects markup into your page, the browser refuses to run script that the policy does not permit. The HTTP security headers guide introduces CSP alongside the other headers. This guide goes into policy design, a staged rollout, and debugging, which is where most teams get stuck.
How a policy is read
A policy 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'; img-src 'self' https://cdn.example.com data:; script-src 'self' https://www.googletagmanager.com
Key rules for reading it:
default-srcis the fallback for any fetch directive you do not list explicitly.- Source keywords must be quoted:
'self','none','unsafe-inline','nonce-...'. Hostnames and schemes are not:https://cdn.example.com,https:,data:. - A more specific directive replaces
default-srcentirely for that type; it does not add to it. 'none'blocks everything for that directive.- Policies from multiple headers are all enforced, and the result is the intersection. You can tighten a policy but never loosen it with a second header.
The directives you will actually use
| Directive | Controls | Typical value |
|---|---|---|
default-src | Fallback for fetch directives | 'self' |
script-src | JavaScript, including inline and event handlers | 'self' 'nonce-...' 'strict-dynamic' |
style-src | Stylesheets and inline styles | 'self' 'nonce-...' |
img-src | Images | 'self' data: https: |
font-src | Web fonts | 'self' https://fonts.gstatic.com |
connect-src | fetch, XHR, WebSocket, EventSource | 'self' https://api.example.com |
frame-src | Where you may embed frames from | https://www.youtube.com |
frame-ancestors | Who may embed you (replaces X-Frame-Options) | 'self' or 'none' |
form-action | Where forms may submit | 'self' |
base-uri | Allowed <base> values | 'self' or 'none' |
object-src | Plugins such as Flash and <object> | 'none' |
upgrade-insecure-requests | Rewrite http:// subresources to https:// | (no value) |
report-to / report-uri | Where violation reports go | endpoint name or URL |
Three of these cost almost nothing and are worth adding to nearly any site: object-src 'none', base-uri 'self', and frame-ancestors 'self'. Note that frame-ancestors, base-uri and form-action do not fall back to default-src, so they have to be listed on their own.
Why 'unsafe-inline' defeats the purpose
Most XSS payloads are inline: a <script> tag, an onerror= attribute, a javascript: URL. If script-src contains 'unsafe-inline', the browser allows all of them, and the policy offers little protection. An allowlist of hostnames alone is also weak, because many popular hosts serve JSONP endpoints or user-supplied scripts that can be abused to run arbitrary code from an "allowed" origin.
The robust approaches are nonces and hashes.
Nonces
A nonce is a random, single-use token generated per response. You put it in the header and on every script tag you trust:
Content-Security-Policy: script-src 'nonce-r4nd0mBase64Value' 'strict-dynamic'; object-src 'none'; base-uri 'none'
<script nonce="r4nd0mBase64Value" src="/app.js"></script>
Requirements: the nonce must be unpredictable (at least 128 bits from a cryptographic random generator, base64-encoded) and different on every response. A static nonce in a template or a cached page gives no protection, and a full-page cache that stores the HTML with one nonce will serve that same value to everyone. If you use a CDN or page cache, either generate nonces at the edge or use hashes.
Hashes
For small, fixed inline scripts you can allow by content hash instead. Compute it:
printf '%s' "console.log('hello')" | openssl dgst -sha256 -binary | openssl base64
Then allow it: script-src 'sha256-<base64-value>'. Any change to the script content, including whitespace, invalidates the hash. Hashes suit static sites; nonces suit server-rendered apps.
strict-dynamic
'strict-dynamic' says: trust scripts that carry a valid nonce or hash, and also trust any scripts those scripts load dynamically. This lets a nonced tag manager or bootstrap script load further libraries without listing every host. In browsers that support it, 'strict-dynamic' makes host allowlists and 'unsafe-inline' in the same directive be ignored. That allows you to keep https: and 'unsafe-inline' as a harmless fallback for old browsers that do not understand it.
Roll out in stages with Report-Only
Do not deploy an enforcing policy on a production site you have not tested. CSP has a built-in dry-run mode: the Content-Security-Policy-Report-Only header. The browser evaluates the policy, logs violations to the console and sends reports, but blocks nothing.
- Inventory. Open the site with DevTools and list every external domain: analytics, fonts, chat widgets, payment forms, video embeds. The third-party script checker lists what your page loads.
- Write a first draft based on that list. Start tight; you will loosen on evidence.
- Ship it as Report-Only and set a reporting endpoint.
- Collect violations for one to two weeks across real traffic, including logged-in pages, checkout, and admin screens, which are the ones people forget.
- Fix at the source where possible: move inline scripts to files or give them nonces, replace inline event handlers with
addEventListener, update code that useseval. - Add sources only for things you actually want. Treat each new origin as a review decision.
- Switch the header from
Content-Security-Policy-Report-OnlytoContent-Security-Policy. Keep Report-Only running with a stricter next-version policy if you want to keep iterating.
A starter policy for a mostly static site might look like this:
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 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; report-to csp-endpoint
Collecting reports
report-uri is the older directive, and report-to is its successor, used together with a Reporting-Endpoints header:
Reporting-Endpoints: csp-endpoint="https://reports.example.com/csp"
Content-Security-Policy-Report-Only: ...; report-to csp-endpoint
Browser support for each differs, so many sites send both report-uri and report-to during the transition. A report is a JSON document containing fields such as blocked-uri, violated-directive, document-uri, and source-file. You can store them yourself or use a hosted collector.
Reports are noisy. Browser extensions inject scripts and styles into pages, and those generate violations that are not your fault. Filter out reports whose source-file is a chrome-extension:// or moz-extension:// URL, and look for patterns across many users rather than single events.
Common breakage and what to do about it
- Google Tag Manager, analytics and ad tags. These load scripts that load other scripts. With a nonce on the GTM snippet and
'strict-dynamic', subsequent loads work. Custom HTML tags that inject inline script need nonces too, or are best removed. - Inline styles.
style-srcblocksstyle="..."attributes unless you allow'unsafe-inline'or use'unsafe-hashes'. Many teams keep'unsafe-inline'for styles while locking down scripts, since style injection is a much smaller risk than script injection. evaland friends.eval(),new Function()and string-basedsetTimeoutneed'unsafe-eval'. Some frameworks and template engines use them; look for an alternative build or precompile templates.- Embedded video, maps and payment forms. Add the provider to
frame-srcandscript-srcas the provider's documentation says. - Images as
data:URIs needdata:inimg-src. - WebSocket connections go under
connect-srcaswss://. - Mixed content.
upgrade-insecure-requestshelps while you fix hard-codedhttp://links. See mixed content: how to fix. - CSP set by both app and server. If two layers each emit a header, the browser enforces both and the result is stricter than you intended. Check the real response with the HTTP header checker.
Setting the header
nginx:
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'self'; object-src 'none'; base-uri 'self'" always;
Apache:
Header always set Content-Security-Policy "default-src 'self'; frame-ancestors 'self'; object-src 'none'; base-uri 'self'"
A <meta http-equiv="Content-Security-Policy" content="..."> tag works for most directives when you cannot set headers, but it cannot express frame-ancestors, report-uri or sandbox, and it only applies after the browser reads that part of the document.
Verify and maintain
After deployment, confirm the policy is served on the pages you care about with the security headers checker. Make CSP review part of your release process: new third-party widgets should arrive with a policy change in the same pull request. A policy nobody updates eventually gets replaced with unsafe-inline.
Next steps
- Grade your current headers with the security headers checker and inspect the raw response with the HTTP header checker.
- List what your page pulls from other domains with the third-party script checker.
- Clean up insecure subresources with the mixed content checker.
- Pair CSP with HSTS and cookie hardening: HSTS preload guide and cookie flags guide.
FAQ
Can CSP break my site?
Yes, if you enforce it without testing. That is why Report-Only exists. Run it for a couple of weeks on real traffic before enforcing.
Is a CSP a substitute for fixing XSS bugs?
No. It is a second layer that limits impact. Escape output and validate input first, and treat CSP as defense in depth.
What is the difference between report-uri and report-to?
report-uri is the older directive with a plain URL. report-to references a named endpoint declared in the Reporting-Endpoints (or Report-To) header. Sending both is common while support is uneven.
Do I need CSP if I have a static site with no user input?
The risk is lower, but CSP still protects against compromised third-party scripts and gives you a clear picture of what loads on your pages. A short policy with object-src 'none' and frame-ancestors is cheap insurance.
Get notified when this guide updates
Plus occasional practical tips on uptime and monitoring, nothing else. Unsubscribe anytime.
This page is general information written by our editorial team and updated when documentation changes. Servers and providers differ, so test changes safely first. See our editorial policy and disclaimer.