Content-Security-Policyis an HTTP response header listing the sources a page may load scripts, styles, frames and other resources from. Anything else is blocked by the browser.- Its main job is to limit cross-site scripting (XSS). With
frame-ancestorsit also replacesX-Frame-Optionsagainst clickjacking. - Domain allowlists are easy to bypass. A strict policy based on nonces or hashes, with
'strict-dynamic',object-src 'none'andbase-uri, is what holds up. - Never put
'unsafe-inline'inscript-src: it allows exactly the injected scripts CSP is meant to stop, and WebSec0 only gives half the points for it. - Roll out with
Content-Security-Policy-Report-Onlyand a reporting endpoint, fix what it reports, then switch to the enforcing header.
What is Content-Security-Policy?
Content-Security-Policy (CSP) is an HTTP response header that tells the browser which sources a page is allowed to load scripts, styles, images, fonts, frames and other resources from. When the page tries to load or run anything the policy does not allow, the browser blocks it and logs a violation. Its main purpose is to limit cross-site scripting (XSS): a script injected into your page does not run if it does not match the policy.
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'; script-src 'self' https://cdn.example.com; object-src 'none'; frame-ancestors 'none'This policy loads everything from the page’s own origin ('self'), also allows
scripts from cdn.example.com, forbids plugins, and stops other sites from
framing the page. Inline <script> blocks, onclick attributes and
javascript: URLs are blocked because the policy does not allow
'unsafe-inline'.
CSP is a second line of defence. It does not fix the injection bug, but it
decides what the injected code can do. Adoption is still low: in Scott Helme’s
June 2026 crawl, 170,057 of the 819,002 top sites that responded sent a
Content-Security-Policy header (about 21%), and 46.8% of those policies
contained 'unsafe-inline'.
What CSP protects against
| Threat | Directive that stops it | How |
|---|---|---|
| Cross-site scripting (XSS) | script-src |
Only scripts from allowed sources, or carrying a valid nonce or hash, run. |
| Clickjacking | frame-ancestors |
Decides which sites may embed your page in a frame. |
<base> tag injection |
base-uri |
Stops an attacker from repointing every relative URL on the page. |
| Form hijacking | form-action |
Limits where forms can submit, including injected ones. |
| Plugin-based script execution | object-src |
'none' disables <object> and <embed>. |
| Mixed content | upgrade-insecure-requests |
Rewrites http:// subresource URLs to https://. |
| DOM-based XSS | require-trusted-types-for |
Makes sinks such as innerHTML reject plain strings. |
The directives that matter
Fetch directives control where each type of resource can come from. When one is
missing, the browser falls back to default-src.
| Directive | Controls | Typical value |
|---|---|---|
default-src |
Fallback for every fetch directive not set | 'self' |
script-src |
JavaScript: files, inline blocks, event handlers, eval() |
'nonce-…' 'strict-dynamic' or 'self' |
style-src |
Stylesheets and inline styles | 'self' |
img-src |
Images and favicons | 'self' data: |
connect-src |
fetch(), XHR, WebSocket and EventSource |
'self' plus your API origins |
font-src |
Web fonts | 'self' |
frame-src |
Pages you embed in frames | 'none' or the embed origins |
object-src |
<object> and <embed> |
'none' |
These directives are not covered by default-src, so set them explicitly:
| Directive | Controls | Typical value |
|---|---|---|
base-uri |
URLs allowed in <base href> |
'none' or 'self' |
form-action |
Where forms may submit | 'self' |
frame-ancestors |
Who may frame your page | 'none' or 'self' |
upgrade-insecure-requests |
Upgrades http:// subresources |
No value |
report-to |
Endpoint that receives violation reports | An endpoint name |
Source values are matched exactly. 'self' means the page’s own scheme, host
and port, not its subdomains. Keywords such as 'self', 'none' and
'unsafe-inline' keep their single quotes; host names do not.
Allowlists versus strict policies
The first instinct is to list every domain your scripts come from. That is called an allowlist policy, and it is the weakest way to use CSP.
Google researchers analysed policies from 1.68 million hosts in 2016. They found that 94.72% of distinct policies could be bypassed, and that 14 of the 15 domains most often allowed for scripts hosted endpoints an attacker could abuse, such as JSONP callbacks or old AngularJS copies. If a domain on your list serves anything an attacker can steer, they load their payload from it and your policy waves it through.
A strict policy trusts individual scripts instead of whole domains:
- Nonces. The server generates a fresh random value for each response
(at least 128 bits), puts it in the header as
'nonce-<value>'and on each legitimate<script nonce="<value>">. Injected scripts cannot guess it. This suits pages rendered by an application. - Hashes. The header lists the SHA-256, SHA-384 or SHA-512 hash of each
inline script, such as
'sha256-…'. Only those exact scripts run. This suits static sites, where the content does not change per request. 'strict-dynamic'. Scripts that were trusted by nonce or hash may load further scripts, so you do not have to list every bundle a tag manager or framework pulls in. In browsers that support it, allowlisted hosts and'self'inscript-srcare ignored.
The policy recommended by Google’s security team, and our default for applications:
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'You will often see https: 'unsafe-inline' appended as a fallback for old
browsers. Browsers that understand nonces ignore those two values, which is
every browser with CSP Level 2 support. We recommend leaving them out: WebSec0
reads 'unsafe-inline' in script-src as a weakness, nonce or not.
Since early 2026, Trusted Types work in all major browsers. Adding
require-trusted-types-for 'script' to a strict policy also blocks DOM-based
XSS, where your own JavaScript writes attacker-controlled strings into
innerHTML and similar sinks. It needs code changes, so test it in report-only
mode first.
Three policies to start from
Pick the one closest to your site, then adjust the sources to what you actually load.
A static site or documentation with no inline scripts (or with hashes for the few it has):
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requestsA server-rendered application that can add a nonce to each response.
Replace {RANDOM} with a new value for every response, and never cache a page
together with its nonce:
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'A JSON API or anything that never renders HTML:
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'Roll it out without breaking your site
A policy that is too strict breaks scripts, fonts or analytics the moment it is enforced. Test it first:
-
Inventory what you load. Open your main pages with the browser’s network panel and note every script, style, font, image and API origin, plus any inline
<script>blocks andon*attributes. -
Ship it in report-only mode. Send the policy as
Content-Security-Policy-Report-Only. The browser blocks nothing and reports every violation to the console and to your endpoint:HTTP Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports" Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; report-to csp-endpoint; report-uri https://example.com/csp-reportsreport-toworks in all major browsers since 2026;report-uricovers older ones and is ignored by browsers that understandreport-to. -
Fix the violations. Move inline scripts and event handlers into files, add nonces or hashes to the inline scripts you keep, and allow the specific origins you rely on rather than broad schemes like
https:. -
Leave it running for a while. A week of real traffic catches pages, browser extensions and third-party scripts that a manual test misses. Extension noise (
chrome-extension://,moz-extension://) can be ignored. -
Enforce it. Rename the header to
Content-Security-Policy. You can keep a stricter candidate running in report-only mode next to it: the browser enforces one and reports on the other. -
Check every response. Error pages, redirects and API routes need the header too. Scan the site with WebSec0 to confirm it is served and graded.
Common mistakes
'unsafe-inline'inscript-src. It re-allows inline scripts andonerror=handlers, the most common XSS payloads. Use nonces or hashes.'unsafe-eval'. It allowseval(),new Function()and string timers. Most modern libraries no longer need it.- Wildcards and schemes.
*,https:ordata:inscript-srcallow scripts from almost anywhere. - Allowing a whole CDN.
https://cdn.jsdelivr.netorhttps://unpkg.comserves any package anyone has published, including an attacker’s. Self-host the file or use a nonce. - Forgetting the directives
default-srcdoes not cover. Withoutbase-uri,form-actionandframe-ancestors, those protections are off. - Only setting it on the home page. Error pages, login flows and older sections are often served by a different rule or application.
- Reusing or caching nonces. A nonce that is the same on every response, or frozen in a CDN cache, gives an attacker the key.
- Relying on a
<meta>tag. It cannot setframe-ancestors, cannot report, and only covers what is parsed after it.
How WebSec0 grades Content-Security-Policy
CSP is the heaviest header in the WebSec0 headers grade: 25 of the 100
points. The scanner reads the Content-Security-Policy response header of
your page and scores it like this:
| What WebSec0 sees | Status | Points |
|---|---|---|
No Content-Security-Policy header |
Fail | 0 / 25 |
'unsafe-inline' in script-src |
Warning | 12 / 25 |
No script-src, and 'unsafe-inline' in default-src |
Warning | 12 / 25 |
| Any other policy | Pass | 25 / 25 |
A few details follow from how the check works:
Content-Security-Policy-Report-Onlyand<meta>policies are not graded. Report-only is a testing mode, not protection.- When the response carries several CSP headers, the first one is graded.
- A
frame-ancestorsdirective also makes the X-Frame-Options check pass, worth another 15 points, even if you do not sendX-Frame-Options. - The grade does not yet look at
'unsafe-eval', wildcards or bypassable allowlists. A passing grade is a floor, not proof the policy is strict: aim for the strict policy above.
Step by step
Configure it on your server
Nginx
Guide ↗Add a Content-Security-Policy header in Nginx, test it in report-only mode, keep it on every location and error page, and add nonces or hashes. Tested configs.
Apache
PlannedNot written yet. The overview’s policy and rollout steps apply to every server.
Caddy
PlannedNot written yet. The overview’s policy and rollout steps apply to every server.
Traefik
PlannedNot written yet. The overview’s policy and rollout steps apply to every server.
Questions
Frequently asked questions
What is a Content-Security-Policy header?
It is an HTTP response header that tells the browser which origins a page may load scripts, styles, images, fonts, frames and other resources from, and where it may be framed or submit forms. Resources that do not match the policy are blocked and reported.
Does CSP replace input validation and output encoding?
No. CSP is a second layer that limits what an injected script can do when the first layer fails. You still need to escape output and validate input. A strict CSP makes most remaining XSS bugs unexploitable, which is why it is worth deploying.
Should I set CSP in a meta tag or an HTTP header?
Use the HTTP header. A <meta http-equiv> policy only applies to content parsed after it, cannot use frame-ancestors, sandbox or reporting, and cannot be report-only. WebSec0 reads response headers, so a meta policy is not graded.
Is 'unsafe-inline' in style-src a problem?
It is much less dangerous than in script-src, and WebSec0 does not penalise it. Injected CSS can still deface a page or leak data through attribute selectors, so move styles to files or use nonces when you can.
Can I send more than one Content-Security-Policy header?
Yes. The browser enforces every policy it receives and a resource must pass all of them, so a second policy can only make things stricter. Note that WebSec0 grades the first Content-Security-Policy header in the response.
What is the difference between report-uri and report-to?
report-uri posts each violation to a URL and is the legacy mechanism. report-to names an endpoint declared in the Reporting-Endpoints header and uses the Reporting API; it has worked in all major browsers since 2026. Browsers that support report-to ignore report-uri, so sending both is safe.
Does CSP replace X-Frame-Options?
For current browsers, yes: frame-ancestors supersedes X-Frame-Options when both are present. WebSec0 counts the X-Frame-Options check as passed when your CSP contains frame-ancestors, even without the older header.
References
Sources
- Content Security Policy (CSP) guideMDN Web Docs
- CSP: report-to directiveMDN Web Docs
- CSP: require-trusted-types-for directiveMDN Web Docs
- CSP Is Dead, Long Live CSP! On the Insecurity of Whitelists and the Future of Content Security PolicyWeichselbaum, Spagnuolo, Lekies and Janc, ACM CCS 2016
