Guides Create account 🇬🇧 🇩🇪
  • Guides
  • Create account
  • Sign in
  • 🇬🇧 🇩🇪
  • Security guide

    Content Security Policy, explained

    A content security policy is the one header that has to be shaped around your own site. This guide covers the directives that carry the weight, how to check the policy your visitors actually receive, how to fix what the check finds, and a rollout that will not break your pages.

    Scan my headers All security headers

    01 Why it matters

    The last line of defence against injected scripts

    A Content-Security-Policy header tells the browser which sources a page is allowed to load code, styles, images and other resources from. Anything outside that allow-list is refused, no matter how it got onto the page. That is what makes it the strongest defence against cross-site scripting: even when an attacker manages to inject a script tag, the browser will not run it.

    It is also the header most likely to break something, because it describes your site rather than a general rule. A policy copied from somewhere else will either block resources you need or allow so much that it protects nothing. The way through is to measure first and enforce second, which is exactly what report-only mode is for.

    02 The directives

    Seven directives that carry the policy

    default-src

    What it does. Sets the fallback source list for every resource type that does not have a directive of its own, so anything you forget inherits this value instead of being unrestricted.

    Why it matters. It is what turns a policy from a list of exceptions into a closed door. Without it, a resource type you never thought about stays wide open.

    default-src 'self'

    script-src

    What it does. Lists the sources the browser may execute JavaScript from. With a nonce or a hash it can allow exactly the inline scripts you ship and refuse every other one.

    Why it matters. This is the directive that actually stops cross-site scripting. Allowing unsafe-inline here gives up most of the protection, which is why a nonce is worth the effort.

    script-src 'self' 'nonce-{RANDOM}'

    style-src

    What it does. Does the same job for stylesheets and inline style blocks, including style attributes when you tighten it further.

    Why it matters. Injected CSS can read out form values through attribute selectors and can hide or fake interface elements. It is a smaller risk than script injection, not a theoretical one.

    style-src 'self'

    frame-ancestors

    What it does. Names the sites that may embed your pages in a frame or iframe. Set to none, nobody may.

    Why it matters. It is the modern replacement for X-Frame-Options and stops clickjacking, where your site is layered invisibly over an attacker page so that real clicks land on your buttons.

    frame-ancestors 'none'

    object-src

    What it does. Controls the legacy object, embed and applet elements, which can load plugin content that ignores the rest of your policy.

    Why it matters. Almost no site needs these any more, so setting none costs nothing and removes a bypass that older browsers still honour.

    object-src 'none'

    base-uri

    What it does. Restricts what a base element on the page may set as the document base URL.

    Why it matters. Without it, a single injected base tag can silently repoint every relative script and link on the page at an attacker host, even while script-src looks correct.

    base-uri 'self'

    form-action

    What it does. Names the targets a form on your page may submit to. Forms have no fallback of their own here: default-src does not cover them, so without this directive a form may post anywhere.

    Why it matters. An injected form, or an altered action attribute on one of yours, sends everything a visitor typed straight to a host you do not own, and the page looks unchanged while it happens. Almost every site posts only to itself, so self costs nothing.

    form-action 'self'

    03 Checking it

    How to check your CSP

    Three ways to see the policy a browser actually receives, from a quick look to a full assessment. Always check the live page rather than your configuration file: a CDN, a CMS plugin or a second server block can add, replace or drop the header on the way out.

    In the browser: DevTools

    Open the page, press F12 and switch to the Network tab. Reload, select the first request of type document and look under Response Headers for content-security-policy and content-security-policy-report-only. A policy set in a meta tag does not appear there; search the Elements panel for http-equiv instead.

    The Console tab shows what the policy refuses while the page runs. Every blocked resource gets a line naming the directive that stopped it, and violations of a report-only policy carry the prefix [Report Only]. A console without such lines on the pages that matter, including login and checkout, is the first sign the policy fits your site.

    Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-4f9GqkTb0yXw'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution. [Report Only] Refused to load the script 'https://cdn.example.net/widget.js' because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-4f9GqkTb0yXw'".

    On the command line: curl

    curl -sI requests only the headers and prints them as the server sent them, without a browser cache, extensions or a logged-in session in the way. The output below comes from a site that enforces one policy and tests a stricter one in report-only mode at the same time, which is exactly how the two headers are meant to be combined.

    $ curl -sI https://www.example.com/ | grep -i '^content-security-policy' content-security-policy: default-src 'self'; script-src 'self' 'nonce-4f9GqkTb0yXw'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none' content-security-policy-report-only: default-src 'self'; script-src 'self' 'nonce-4f9GqkTb0yXw'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; require-trusted-types-for 'script'

    curl -I sends a HEAD request, and some servers and applications answer HEAD with different headers than a normal page view. If the policy is missing here but the browser shows one, repeat the check with curl -s -o /dev/null -D - followed by the URL: that makes a normal GET request and prints only the headers. Check the final URL after any redirect, because a policy on the redirect response protects nothing.

    With the scanner

    Our free scan reads the header, the report-only variant and a meta tag policy, and it assesses the policy instead of only noting that one exists: unsafe-inline or unsafe-eval, wildcard sources, missing frame-ancestors, base-uri or form-action, and an object-src that is not none. Each weakness is named in the finding, so you know which part of the policy to change.

    A policy that passes today can be loosened by the next deploy or plugin update. How to notice that is covered in two separate guides.

    Detecting CSP changes Monitoring security headers

    04 Reading the result

    Five findings and how to fix them

    These are the results our scanner reports for a Content-Security-Policy, with what each one means for the protection and the change that clears it. Weaknesses of a policy that is present are grouped into one finding, Content-Security-Policy is present but weak, which lists each of them by name.

    'unsafe-inline'

    What the scanner reports. Content-Security-Policy is present but weak, with unsafe-inline in the list.

    What it means. The policy allows every inline script or style block, including one an attacker injects. In script-src that gives up most of the protection against cross-site scripting. We do not count it when the same directive also carries a nonce, a hash or strict-dynamic, because browsers then ignore unsafe-inline.

    How to fix it. Give every inline script a nonce that is generated fresh for each response, then remove unsafe-inline. Inline event handlers such as onclick cannot carry a nonce, so move them into your script files first.

    script-src 'self' 'nonce-{RANDOM}'

    *, https:, http:

    What the scanner reports. Content-Security-Policy is present but weak, with wildcard-source in the list.

    What it means. A source list contains *, https: or http:. Any host on the internet then qualifies, including one the attacker controls, so the directive restricts nothing but the scheme.

    How to fix it. Replace the wildcard with the origins you actually load from. The Network tab, or a week of report-only reports, gives you the list.

    script-src 'self' https://cdn.example.net

    form-action, base-uri, frame-ancestors

    What the scanner reports. Content-Security-Policy is present but weak, with no-form-action, no-base-uri, no-frame-ancestors or object-src-not-none in the list.

    What it means. form-action, base-uri and frame-ancestors do not fall back to default-src. However strict the rest of the policy is, without them a form may post anywhere, an injected base tag can repoint every relative URL, and any site may frame yours. object-src-not-none means plugin content is still allowed.

    How to fix it. Name them explicitly. Almost every site can use the values below unchanged.

    object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'

    Content-Security-Policy-Report-Only

    What the scanner reports. Content-Security-Policy is only delivered in report-only mode.

    What it means. The policy exists but blocks nothing. We rate it high because it passes a quick look while no protection is in force.

    How to fix it. Once the reports contain nothing you consider legitimate, send the same value under the enforcing header name. Keep the report-only header only for testing the next, stricter version.

    add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;

    <meta http-equiv>

    What the scanner reports. Content-Security-Policy is delivered only through a meta tag.

    What it means. A meta tag works for most directives, but frame-ancestors, report-uri and sandbox are ignored there, and anything the browser parsed before the tag is not covered.

    How to fix it. Send the policy as a response header from your web server or CDN and remove the meta tag once the header is live.

    add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;

    05 Rolling it out

    Report-only first, enforcing second

    Send Content-Security-Policy-Report-Only with the policy you want. The browser does not block anything, it only reports what it would have blocked, so you can watch a real policy against real traffic. Once the reports go quiet, send the same policy in the enforcing Content-Security-Policy header and remove the report-only one.

    add_header Content-Security-Policy-Report-Only "default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; report-uri /csp-report" always;
    
    add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;

    Two things to watch. Send only one of the two headers once you switch, otherwise the report-only version keeps collecting for a policy that is already live. And when you generate nonces, generate a fresh one per response: a nonce reused across requests is no better than allowing inline scripts outright.

    06 Questions

    What people ask about CSP

    Can I set a Content Security Policy in a meta tag?

    You can, and it works for most directives, but frame-ancestors, report-uri and sandbox are ignored in a meta tag. A response header is the more complete option, and our scanner reads both.

    Do I have to remove every inline script first?

    No. A nonce lets you keep the inline scripts you control: the server puts a random value in the header and the same value on each script tag it emits. What you do have to give up is unsafe-inline, because that allows injected scripts as well as your own.

    What happens to third-party embeds like maps or videos?

    They need their hosts listed in the directives they use, usually script-src and frame-src. Report-only mode is the cheapest way to find out which ones, because the report names every source that would have been blocked.

    Is a weak policy better than none?

    A policy built around default-src self is a real improvement even if it still allows some inline code. A policy that allows unsafe-inline and unsafe-eval everywhere mostly produces a header that looks good in a checker without stopping an attack.

    How do I check whether my Content Security Policy is working?

    Look at two places in the browser developer tools: the response headers of the document request show the policy the browser received, and the console lists every resource it refused. If the header carries the enforcing name and not only Report-Only, and the console stays free of violations on your important pages, the policy is working. Our free scan checks the same header from outside and rates how strong the policy is.

    Why does curl show no Content-Security-Policy when the browser has one?

    Usually for one of three reasons: the policy sits in a meta tag in the HTML rather than in a header, the server answers the HEAD request of curl -I differently from a normal GET, or the URL redirects and you saw only the headers of the redirect. curl -s -o /dev/null -D - with the final URL rules out the last two.

    My policy has unsafe-inline next to a nonce. Is that a weakness?

    No. Browsers that support nonces ignore unsafe-inline in a directive that also carries a nonce or a hash, so it only takes effect in very old browsers that would otherwise block your inline scripts. Our scanner does not count it as a weakness in that case. Without a nonce or hash in the same directive, it is one.

    07 In depth

    Related security topics

    A policy is one piece. These guides cover the neighbours it works with.

    HTTP security headers

    What each response header does, which ones your site should send and how to configure them correctly.

    Read guide

    HSTS

    Strict-Transport-Security explained: what max-age, includeSubDomains and preload actually do, and why the preload list is a decision you cannot quickly undo.

    Read guide

    Cookie security

    Secure, HttpOnly and SameSite: the cookie attributes that keep a session out of reach of scripts and cross-site requests, with the settings for a typical stack.

    Read guide

    Clickjacking and X-Frame-Options

    How an invisible overlay turns a visitor click into an action on your site, and the two headers that stop it: X-Frame-Options for older clients, frame-ancestors for everything else.

    Read guide

    Referrer-Policy

    Which part of your URLs travels to other sites when a visitor clicks away: the five policy values that matter, what each one gives up and the one to set by default.

    Read guide

    Permissions-Policy

    Switch off camera, microphone, geolocation and payment for your site and everything it embeds: the allow-list syntax, and why a feature you did not name is not a feature you turned off.

    Read guide

    SPF, DMARC and DNSSEC

    The records that stop someone sending mail in your name, and the one that keeps your DNS answers honest: what each does and the order to introduce them in.

    Read guide

    security.txt

    The file that tells a researcher where to report a vulnerability. Two mandatory fields, ten minutes of work, and the difference between a private report and a public one.

    Read guide

    Monitoring security headers

    A header that is right today can be gone after the next deploy. Three ways to notice: a cron job with curl, a check in your CI pipeline and a scheduled scan, with what each of them catches and misses.

    Read guide

    Detecting CSP changes

    How a policy changes quietly through deploys, plugins and CDN rules, and how to catch it: a header diff, violation reports through report-to, and what a scan comparison can and cannot show.

    Read guide

    Keep reading

    Every guide stands on its own, and together they cover what our scanner looks at. Pick the one closest to your next question.

    Is your website affected? BFSG check Accessibility statement BFSG for online shops BFSG for medical practices BFSG for hotels BFSG for tradespeople HTTP security headers Privacy signals HSTS Cookie security Clickjacking and X-Frame-Options Referrer-Policy Permissions-Policy SPF, DMARC and DNSSEC security.txt Monitoring security headers Detecting CSP changes Security headers in nginx Security headers in Apache Security headers in WordPress Security headers in IIS Security headers in TYPO3 Security headers in Shopware 6 Security headers in Plesk All guides

    See whether your policy is doing its job

    Our free Quickscan reads your live Content-Security-Policy, including the report-only variant and a policy set in a meta tag, and explains what it found in plain language.

    Scan a website Latest scans
    Recent scans Guides Local leagues Pricing Methodology For hosters API Data protection Imprint Accessibility Terms Cancel contracts here © 2026 Erseni