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

    How to detect changes to your Content Security Policy

    A content security policy is rarely removed on purpose. It gets loosened for a quick fix, switched to report-only for debugging, or replaced by a plugin or a CDN rule, and the site keeps working. This guide shows how to notice: a header diff you can run in CI, the violation reports the browser sends you, and what a scan comparison can and cannot tell you.

    Scan my headers All security headers

    01 Why it matters

    How a policy changes without a code review

    The policy is often assembled in more than one place. The application sets it, a CMS plugin adds its own, a CDN or WAF rule rewrites or appends headers, and the hosting panel has a field for it too. Any of them can change it without the change ever passing through the repository where your policy is reviewed.

    Some changes make the policy stricter without anyone noticing: when two Content-Security-Policy headers arrive, the browser enforces both, so a resource has to pass each of them. The dangerous changes go the other way: a new wildcard, an added unsafe-inline, or the switch from the enforcing header to report-only. After such a change the page looks exactly as before, it is only no longer protected.

    02 An example

    A policy switched from enforcing to report-only

    A typical case: a payment widget on the checkout stops loading after a provider update. To find the cause quickly, the header name is changed to Content-Security-Policy-Report-Only, the widget works again, and switching back is forgotten. Before and after, curl shows this:

    $ curl -sI https://www.example.com/checkout | grep -i '^content-security-policy' content-security-policy: default-src 'self'; script-src 'self' 'nonce-Q2hlY2tvdXQ'; style-src 'self'; img-src 'self' data:; frame-src https://pay.example-psp.com; object-src 'none'; base-uri 'self'; form-action 'self' https://pay.example-psp.com; frame-ancestors 'none'; report-to csp
    $ curl -sI https://www.example.com/checkout | grep -i '^content-security-policy' content-security-policy-report-only: default-src 'self'; script-src 'self' 'nonce-V2lkZ2V0Zml4'; style-src 'self'; img-src 'self' data:; frame-src https://pay.example-psp.com; object-src 'none'; base-uri 'self'; form-action 'self' https://pay.example-psp.com; frame-ancestors 'none'; report-to csp

    The value is the same except for the nonce, which is why a quick look at the policy text misses it. Only the header name changed, and with it the whole effect: the browser now reports violations and blocks none. This is the change every method below should catch.

    03 Method 1

    Diff the header against the policy you expect

    Keep the policy you expect in a file in your repository, ci/csp-expected.txt, in the same normalised form the script produces: header name in lower case, nonce replaced by a placeholder. After every deploy the script fetches the live header and compares. Because diff exits with a non-zero status when the files differ and set -e stops the script there, the pipeline job fails.

    #!/bin/sh set -eu curl -fsS -o /dev/null -D headers.txt "$1" tr -d '\r' < headers.txt \ | sed -E "s/'nonce-[^']+'/'nonce-X'/g" \ | awk -F': ' 'tolower($1) ~ /^content-security-policy(-report-only)?$/ { print tolower($1) ": " substr($0, length($1) + 3) }' \ | sort > csp-live.txt diff -u ci/csp-expected.txt csp-live.txt

    For the example above, the job fails with this output, which names the change exactly:

    --- ci/csp-expected.txt 2026-09-18 10:12:03.000000000 +0200 +++ csp-live.txt 2026-09-25 14:31:47.412803113 +0200 @@ -1 +1 @@ -content-security-policy: default-src 'self'; script-src 'self' 'nonce-X'; style-src 'self'; img-src 'self' data:; frame-src https://pay.example-psp.com; object-src 'none'; base-uri 'self'; form-action 'self' https://pay.example-psp.com; frame-ancestors 'none'; report-to csp +content-security-policy-report-only: default-src 'self'; script-src 'self' 'nonce-X'; style-src 'self'; img-src 'self' data:; frame-src https://pay.example-psp.com; object-src 'none'; base-uri 'self'; form-action 'self' https://pay.example-psp.com; frame-ancestors 'none'; report-to csp

    Run the same script on a schedule to catch changes that do not come through your pipeline, such as a CDN rule or a plugin update. Our guide on monitoring security headers has a cron version that mails the diff instead of failing a job.

    Monitoring security headers

    04 Method 2

    Let the browser report: report-to and report-uri

    The header diff sees what your server sends. Violation reports see what happens in your visitors' browsers. Name an endpoint with Reporting-Endpoints, refer to it with report-to, and keep report-uri as well for browsers that do not support report-to yet.

    add_header Reporting-Endpoints 'csp="https://www.example.com/csp-reports"' always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; report-to csp; report-uri https://www.example.com/csp-reports" always;

    Each report carries two fields that reveal a change to the policy itself. disposition is enforce for an enforcing policy and report for a report-only one, so a stream of enforce reports turning into report is the switch from the example above. originalPolicy contains the full policy text the browser received, so a new source or a lost directive shows up there as soon as the first violation is reported.

    [{ "age": 0, "type": "csp-violation", "url": "https://www.example.com/checkout", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", "body": { "documentURL": "https://www.example.com/checkout", "blockedURL": "https://cdn.example-widget.net/loader.js", "effectiveDirective": "script-src-elem", "disposition": "report", "originalPolicy": "default-src 'self'; script-src 'self' 'nonce-V2lkZ2V0Zml4'; ...; report-to csp", "referrer": "", "sample": "", "statusCode": 200 } }]

    Reports sent through report-to arrive as application/reports+json in batches, those sent through report-uri as application/csp-report with the same information in a csp-report object with hyphenated field names. Reports only arrive when something violates the policy, so they complement the header diff rather than replace it: a policy that is loosened until nothing violates it any more goes quiet instead of raising an alarm.

    05 Method 3

    What a scan comparison shows, and what it does not

    Our scan comparison sets two scans of the same site side by side and shows which findings were resolved, which appeared and how the scores moved. It compares findings, not header text. A change to the policy shows up there only if it creates or removes a finding.

    That covers the changes that matter most. The switch to report-only in the example appears as the new finding Content-Security-Policy is only delivered in report-only mode. A removed policy appears as Content-Security-Policy missing, and a clean policy that gains unsafe-inline, a wildcard or loses form-action appears as Content-Security-Policy is present but weak.

    What stays invisible: a new host in a policy that is still strong, and a further weakness in a policy that is already reported as weak, because the finding exists before and after. Use the scan as an alarm when your policy gets worse, not as a diff. The monitor in the Agency plan runs the comparison on a schedule and mails you when a finding appears.

    06 Questions

    What people ask about CSP changes

    How can I tell whether my CSP has changed?

    Compare the live header with the policy you expect. Fetch it with curl, replace the nonce with a placeholder and diff it against a file in your repository, after every deploy and on a schedule. Violation reports via report-to show a change from the browser side, because every report carries the policy text and whether it was enforced.

    Why does my CSP look different on every request?

    Because of the nonce, which has to be new for every response. Replace it with a fixed placeholder before comparing, for example with sed -E "s/'nonce-[^']+'/'nonce-X'/g", otherwise every comparison reports a change.

    What happens if two Content-Security-Policy headers are sent?

    The browser enforces both: a resource is only loaded if every policy allows it. A second header therefore never loosens the first, but it can block resources unexpectedly. A Content-Security-Policy-Report-Only header next to an enforcing one blocks nothing and only reports.

    Does the Erseni Scan comparison show the changed CSP value?

    No. It shows the findings that were resolved or appeared and how the scores changed. A switch to report-only, a removed policy or a new weakness in a previously clean policy shows up as a finding. A changed host list in an otherwise strong policy does not; for that, diff the header value.

    07 In depth

    Related security topics

    Detecting a change is one part. These guides explain what the policy and its neighbours should look like in the first place.

    HTTP security headers

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

    Read guide

    Content Security Policy

    The strongest defence against cross-site scripting: which directives to set, how to roll a policy out in report-only mode and how to reach an enforcing policy without breaking your site.

    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

    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 Content Security Policy HSTS Cookie security Clickjacking and X-Frame-Options Referrer-Policy Permissions-Policy SPF, DMARC and DNSSEC security.txt Monitoring security headers 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

    Know what your policy looks like today

    A free scan reads your live Content-Security-Policy, the report-only variant and a meta tag policy, and names every weakness. That is the state to diff against, and the first scan a later comparison starts from.

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