Header guide · Content-Security-Policy

Content-Security-Policy (CSP): what it is and how to deploy it safely

Content-Security-Policy tells the browser which scripts, styles and frames a page may load. Learn the key directives, strict nonce policies and a safe rollout.

Scripts from 'self' and cdn.example pass the policy and run in the page; a script from evil.example is blocked.'self'cdn.exampleevil.exampleSCRIPT SOURCESPOLICYYOUR PAGE
script-src 'self' cdn.example — anything else is refused.
In short5 points
  • Content-Security-Policy is 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-ancestors it also replaces X-Frame-Options against clickjacking.
  • Domain allowlists are easy to bypass. A strict policy based on nonces or hashes, with 'strict-dynamic', object-src 'none' and base-uri, is what holds up.
  • Never put 'unsafe-inline' in script-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-Only and 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:

HTTP
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' in script-src are ignored.

The policy recommended by Google’s security team, and our default for applications:

HTTP
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):

HTTP
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-requests

A 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:

HTTP
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:

HTTP
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:

  1. 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 and on* attributes.

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

    report-to works in all major browsers since 2026; report-uri covers older ones and is ignored by browsers that understand report-to.

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

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

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

  6. 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' in script-src. It re-allows inline scripts and onerror= handlers, the most common XSS payloads. Use nonces or hashes.
  • 'unsafe-eval'. It allows eval(), new Function() and string timers. Most modern libraries no longer need it.
  • Wildcards and schemes. *, https: or data: in script-src allow scripts from almost anywhere.
  • Allowing a whole CDN. https://cdn.jsdelivr.net or https://unpkg.com serves any package anyone has published, including an attacker’s. Self-host the file or use a nonce.
  • Forgetting the directives default-src does not cover. Without base-uri, form-action and frame-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 set frame-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-Only and <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-ancestors directive also makes the X-Frame-Options check pass, worth another 15 points, even if you do not send X-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

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

About the author

Portrait of Joshua Martinelle

Security engineer at Tenable

Security engineer at Tenable and web security researcher. He has disclosed more than 60 CVEs since 2020 and builds WebSec0.

Verify the change

Deployed it? Check it.

WebSec0 grades your TLS setup and security headers, including Content-Security-Policy, in a few seconds. Free, no account.

Scan your site