How to check CSP header

Read the header and split it on semicolons so each directive lands on its own line: curl -sI https://developer.mozilla.org/en-US/docs/Web/HTTP | tr ';' '\n'. Then answer two questions about what came back. Is the header enforcing or report-only, and does script-src contain unsafe-inline. Those two answers decide whether the policy stops anything.

Checker offline. Follow the manual steps below, they give the same answer.

Why check this

A Content-Security-Policy is the control that keeps an injected script from executing. Check it when the front end ships a new third-party tag, and again before a release turns a trial policy into the real one. The failure it prevents is a policy that has been in report-only mode since the pilot, so every violation is logged, nothing is blocked, and the team reads the header name in a scan report as proof of protection.

Prerequisites

const http = require('node:http');
const policy = "default-src 'self'";
const routes = {
  '/enforce': { 'content-security-policy': policy },
  '/report-only': { 'content-security-policy-report-only': policy },
  '/unsafe-inline': { 'content-security-policy': policy + "; script-src 'self' 'unsafe-inline'" },
};
http.createServer((req, res) => {
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', ...routes[req.url] });
  res.end(`<!doctype html><meta charset="utf-8"><title>csp target</title>
<p id="p">inline script did not run</p>
<script>document.getElementById('p').textContent = 'inline script ran';</script>`);
}).listen(8100, () => console.log('csp target on http://localhost:8100/'));

Steps

  1. Step 1.

    Find out which of the two header names the response carries.

    curl -sI https://developer.mozilla.org/en-US/docs/Web/HTTP | tr -d '\r' | grep -io '^content-security-policy[a-z-]*:'
    
    content-security-policy:

    A name ending in -report-only means the browser logs violations and blocks nothing.

  2. Step 2.

    Split the policy into one directive per line so it can be read.

    curl -sI https://developer.mozilla.org/en-US/docs/Web/HTTP | tr -d '\r' | grep -i '^content-security-policy:' | tr ';' '\n' | sed 's/^ //'
    
    content-security-policy: default-src 'self'
    script-src 'report-sample' 'self' 'wasm-unsafe-eval' assets.codepen.io …
    script-src-elem 'report-sample' 'self' 'wasm-unsafe-eval' assets.codepen.io …
    style-src 'report-sample' 'self' 'unsafe-inline' transcend-cdn.com
    object-src 'none'
    base-uri 'self'
    connect-src 'self' developer.allizom.org bcd.developer.mozilla.org …
    font-src 'self'
    frame-src 'self' mdn.github.io *.mdnplay.dev jsfiddle.net codepen.io …
    img-src 'self' data: *.githubusercontent.com *.gravatar.com …
    manifest-src 'self'
    media-src 'self' archive.org videos.cdn.mozilla.net …
    child-src 'self'
    worker-src 'self'

    default-src 'self' is the fallback for every fetch directive the policy does not name. There is no frame-ancestors line here, so this policy says nothing about framing.

  3. Step 3.

    Look for the two keywords that switch script protection off. Keep the quotes in the pattern.

    curl -sI https://www.cloudflare.com/ | tr -d '\r' | grep -i '^content-security-policy:' | tr ';' '\n' | sed 's/^ //' | grep -E "'unsafe-inline'|'unsafe-eval'" | cut -c1-58
    
    script-src 'self' 'unsafe-inline' 'unsafe-eval' https://st…
    style-src 'self' 'unsafe-inline'

    The same command against developer.mozilla.org returns only the style-src line, so that policy still controls script execution.

  4. Step 4.

    Start the local target, open http://localhost:8100/enforce in Chrome, and read the Console tab.

    http://localhost:8100/enforce
    
    Executing inline script violates the following Content Security Policy directive
    'default-src 'self''. … The action has been blocked.

    The paragraph on the page still reads inline script did not run, which is the DOM proof that the block happened.

  5. Step 5.

    Open http://localhost:8100/report-only and compare the last sentence of the same message.

    http://localhost:8100/report-only
    
    Executing inline script violates the following Content Security Policy directive
    'default-src 'self''. … The policy is report-only, so the violation has been
    logged but no further action has been taken.

    The paragraph now reads inline script ran. Same policy text, same violation, opposite outcome.

  6. Step 6.

    Open http://localhost:8100/unsafe-inline, where the policy adds that one keyword.

    http://localhost:8100/unsafe-inline
    
    (no console messages)

    No violation is reported at all. The inline script ran, and the policy raised nothing, which is what an injected inline script would also get.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | content-security-policy: | The policy is enforced | Read the directives. Step 3 decides whether enforcement bites. | | content-security-policy-report-only: | Violations are logged, nothing is blocked | Treat the page as having no CSP until the header is renamed. | | Both header names present | The report-only copy is a candidate policy under trial | Normal during a rollout. Compare the two, and confirm which one ships. | | 'unsafe-inline' in script-src | Any inline script on the page executes | Replace with a nonce or a hash. Until then the policy does not stop injection. | | 'unsafe-eval' in script-src | eval and new Function are permitted | Find the library that needs it. This is usually one templating dependency. | | No default-src and no script-src | Script loading is unrestricted | The policy covers only the directives it names. Everything else is open. |

Common mistakes

Sign: A scan report lists Content-Security-Policy as present and the site is still injectable.Cause: The header name was content-security-policy-report-only. Step 1 separates the two names. Step 5 shows that the browser produces an almost identical console message for both, differing only in the final sentence.
Sign: Grepping the policy for unsafe-eval matches a policy that does not allow eval.Cause: 'wasm-unsafe-eval' contains the string 'unsafe-eval' and permits only WebAssembly compilation. Step 3 keeps the single quotes in the pattern so the two keywords cannot be confused.
Sign: The policy looks strict in curl and the browser reports no violations at all.Cause: A meta tag in the HTML can add a second policy, and a policy delivered by meta cannot carry frame-ancestors or report-uri. Read the document source as well as the response header when the two disagree.
Sign: The policy blocks a script on staging that works in production.Cause: Host allow lists are written per environment. A CDN hostname that differs by one subdomain between environments turns into a blocked script, not a failed request, so the network tab shows the fetch and the console shows the refusal.

What to check next

FAQ

How to test content security policy?

Steps 4 to 6. A policy is code the browser runs, so reading the header is only half the check. Load the page in Chrome, trigger the behaviour the policy restricts, and read the Console tab for refusals.

How to check CSP header in Chrome?

Open DevTools, Network tab, reload, click the document request, then Headers and Response Headers. The Console tab lists each violation, and the Issues tab groups them by directive with the offending URL.

How to check if CSP is enabled?

Run step 1. An empty result means no policy on that response. A name ending in -report-only means the policy is present and enforces nothing.

How to check CSP violations?

Violations appear in the Chrome Console as refusal messages, and are posted as JSON to the endpoint in report-to or report-uri when the policy sets one. Without a reporting endpoint, only the browsers of people who hit the violation see it.

Does report-only mode protect anything?

No. Step 5 executed the inline script the enforcing policy blocked in step 4. Report-only exists to collect violations while a policy is tuned, and it is a rollout stage, not an outcome.

Verified

Verified by Maks Vernycurl 8.21.0node 22.23.2

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate9 minpublished updated Maks Verny