How to Test Security Headers Before You Ship

Last updated 2026-08-05

Security headers are unusually unforgiving to deploy. A Content Security Policy that is slightly too strict does not degrade, it blanks your page. Setting the header locally first turns a production incident into a five-minute experiment against the real site, with the real third-party scripts, in your own browser.

Content Security Policy

Start by setting the policy you intend to ship as a response header on your staging domain, then click through the app with the DevTools console open. Every violation is logged with the directive that blocked it, which is a much better source of truth than guessing which analytics vendor needs which host.

Match:  domain is staging.example.com
Action: set response header  Content-Security-Policy:
          default-src 'self';
          script-src 'self' https://plausible.io;
          style-src 'self' 'unsafe-inline';
          img-src 'self' data: https:;
          connect-src 'self' https://api.example.com;
          frame-ancestors 'none';
          base-uri 'self'
  • Test in report-only mode first: set Content-Security-Policy-Report-Only instead. Violations are reported and nothing is blocked, which is the right first step on a site you cannot afford to break.
  • Expect inline styles to be the hard part: many frameworks inject inline styles. unsafe-inline for styles is a common, pragmatic compromise. unsafe-inline for scripts is not.
  • Watch for a meta tag CSP: a policy delivered in HTML combines with the header, and the result is the intersection of both. If your header seems ignored, check the document.
  • Remember multiple policies stack: two CSP headers do not override each other. A resource must satisfy every policy present, which is why appending a second CSP only ever makes things stricter.

Framing: X-Frame-Options and frame-ancestors

To check whether your app can be embedded, set the header locally and load your page in an iframe on a scratch page. To check the opposite, that a partner can embed you, remove the header locally and confirm the embed works before you weaken anything on the server.

# Cannot be framed at all
set response header  X-Frame-Options: DENY

# Only a named partner may frame it (CSP is the modern control)
set response header  Content-Security-Policy: frame-ancestors https://partner.example.com

X-Frame-Options is the legacy header and frame-ancestors supersedes it. Browsers that support both prefer the CSP directive, so when they disagree, the CSP wins in modern browsers and the legacy header still matters for old ones.

HSTS: the one to be careful with

Strict-Transport-Security is not a header you should casually inject, because the browser remembers it. Setting a long max-age for a host pins that host to HTTPS in your profile for the duration, whether or not your rule is still enabled, and any plain-http development on that hostname stops working.

  • Test with a short max-age, such as 300 seconds, not the year you plan to ship.
  • Never test includeSubDomains against a hostname whose subdomains you use over http locally.
  • To undo it, send Strict-Transport-Security: max-age=0 for that host, or clear the entry from chrome://net-internals/#hsts.
  • Preloading is a separate, slow-to-reverse commitment. Nothing about it can be sensibly tested with a local header.

The rest of the set

HeaderTryWhat to watch for
X-Content-Type-Optionsnosniffbreaks resources served with the wrong content type, which is the point
Referrer-Policystrict-origin-when-cross-originattribution and analytics that depend on a full referrer path
Permissions-Policycamera=(), microphone=(), geolocation=()any feature your app genuinely uses will start failing
Cross-Origin-Opener-Policysame-originOAuth popups and payment windows that need a handle on the opener
Cross-Origin-Embedder-Policyrequire-corpevery cross-origin resource now needs CORP headers. Test before shipping, always
Cache-Controlno-store on authenticated pagesconfirm nothing sensitive is served from the back-forward cache

A workable sequence

  1. Set the header locally against staging and use the app normally for a few minutes.
  2. Read every console violation and decide, one by one, whether to allow the resource or remove it.
  3. Deploy in report-only mode where the header supports it, and watch real traffic.
  4. Enforce, starting with a short max-age or a narrow scope where the header has one.
  5. Verify from outside your browser with curl -I, so you are reading the server and not your own rule.

Turn these rules off when you finish. A forgotten local CSP that blocks a script makes a site look broken for you and nobody else, and that is a genuinely painful hour of debugging.

Common questions

Can I test a CSP without deploying it?

Yes. Setting Content-Security-Policy as a response header in your browser applies the real policy to the real site, so you see exactly which resources it blocks. It is the closest thing to a dry run, and using Content-Security-Policy-Report-Only makes it non-blocking.

Why did my site stop loading over http after I tested HSTS?

Because the browser cached the Strict-Transport-Security policy for that host and now upgrades every request. Send max-age=0 for the host, or delete the entry in chrome://net-internals/#hsts. Testing with a short max-age avoids the problem.

Do local header rules test the header the way a real deployment would?

For everything the browser enforces, yes: CSP, framing, referrer policy, and permissions all behave as they would from the server. What local rules cannot test is anything outside the browser, such as a CDN rewriting headers or a scanner grading your domain.

Should I remove security headers to make development easier?

Only for a specific test, and only briefly. Removing a CSP locally to confirm it is the cause of a problem is reasonable. Leaving it removed means you are developing against a configuration none of your users have.