How to Test Security Headers Are Working — HSTS, CSP, X-Frame-Options — HeaderSnap ModHeader was removed from Chrome & Edge over a hidden data collector — what happened and what to do
HeaderSnap
March 21, 2026

How to Verify Your Security Headers Are Actually Working (HSTS, CSP, X-Frame-Options)

Most developers learn to check that security headers are present. You open Chrome DevTools, look at the Network tab, and confirm Strict-Transport-Security or Content-Security-Policy appears in the response headers. Box ticked.

But “present” and “working correctly” are different things. A CSP that’s misconfigured allows exactly the attacks you were trying to block. An HSTS policy with a short max-age provides almost no protection. An X-Frame-Options header that’s being overridden by a more permissive Content-Security-Policy frame-ancestors directive may not do what you expect.

This guide covers how to verify that each security header is actually functioning — not just there.


HSTS: Verify It’s Enforcing HTTPS

Strict-Transport-Security tells browsers to only access your domain over HTTPS. Once a browser has seen your HSTS header, it won’t make plain HTTP requests to your domain for the duration of the max-age.

What to check:

First, confirm the header is present with reasonable values:

Strict-Transport-Security: max-age=31536000; includeSubDomains

max-age=31536000 is one year. Shorter values (30 seconds, or even 300 seconds — seen more often than you’d think) mean the protection expires quickly and users are vulnerable if they visit your site on a fresh browser.

Verifying enforcement:

  1. Visit your site over HTTPS — Chrome will store the HSTS policy
  2. In the address bar, manually try http://yourdomain.com — the browser should redirect to HTTPS before a request even hits the network
  3. Confirm the redirect in DevTools Network tab: you’ll see a (from HSTS) label rather than a normal 301/302 redirect

The real failure mode: HSTS without includeSubDomains protects your root domain but leaves subdomains vulnerable. A Secure cookie on app.yourdomain.com can be stolen via an HTTP connection to login.yourdomain.com if subdomains aren’t included. If you’re setting HttpOnly; Secure cookies, your HSTS policy should include includeSubDomains.


CSP: Test That It’s Blocking Violations

Content-Security-Policy restricts what resources the page can load — scripts, styles, images, fonts, frames. A CSP is only useful if it actually blocks what you intend to block.

The right way to develop a CSP:

Start with Content-Security-Policy-Report-Only instead of Content-Security-Policy. The report-only header enforces nothing — it only logs violations to the browser console. This lets you see what your policy would block without breaking anything.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://cdn.example.com

Open the browser console (F12 → Console tab) and reload the page. Any CSP violations appear as warnings like:

Refused to load script from 'https://analytics.example.com' because it violates the following Content Security Policy directive: "script-src 'self'"

This tells you exactly what sources you need to add to your policy before switching to enforcement mode.

Testing enforcement:

Once you’ve switched to the enforcing Content-Security-Policy header, test that it actually blocks:

  1. Try loading an inline script: inject <script>alert(1)</script> into the page (as a developer testing your own CSP) — if script-src doesn’t include 'unsafe-inline', it should be blocked and appear as a CSP violation in the console
  2. Try loading a script from an unlisted domain: if it’s blocked, you’ll see a violation; if it loads, your policy is too permissive

A common misconfiguration: 'unsafe-inline' in script-src defeats the XSS protection CSP was designed to provide. If you’re using 'unsafe-inline' to support inline scripts or event handlers, the header gives the appearance of security but doesn’t block the attack vectors that matter most. The correct approach is nonces or hashes.


X-Frame-Options: Verify It Actually Blocks Iframes

X-Frame-Options prevents your page from being embedded in iframes on other domains — which blocks clickjacking attacks.

What to check:

X-Frame-Options: DENY
# or
X-Frame-Options: SAMEORIGIN

DENY prevents all iframe embedding. SAMEORIGIN allows embedding from the same origin only.

Verifying it works:

Create a simple HTML test page on a different origin (or use a local file, which has its own origin):

<iframe src="https://yourapp.com/login" width="500" height="300"></iframe>

Open that page in Chrome. If X-Frame-Options is configured correctly, the iframe will refuse to load. DevTools Console will show:

Refused to display 'https://yourapp.com/login' in a frame because it set 'X-Frame-Options' to 'DENY'.

The interaction with CSP: If you have both X-Frame-Options and Content-Security-Policy: frame-ancestors, the frame-ancestors directive takes precedence in modern browsers. If your CSP sets frame-ancestors 'self' but your X-Frame-Options says DENY, the CSP wins. Test with both headers present to confirm the actual behavior — don’t assume.


X-Content-Type-Options: Test MIME Sniffing Is Disabled

X-Content-Type-Options: nosniff tells browsers not to guess the MIME type of a response — they must use whatever Content-Type the server returns. This prevents “MIME sniffing” attacks where a browser interprets a response as JavaScript even though the server labeled it as text or image.

What to check:

X-Content-Type-Options: nosniff

There’s no value variation here — it’s always nosniff.

Verifying it blocks type confusion:

The practical test: serve a JavaScript file with the Content-Type: text/plain header and try to include it as a script tag:

<script src="/file-served-as-text-plain.js"></script>

With nosniff, Chrome should refuse to execute it and report in the console:

Refused to execute script from '...' because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled.

Without nosniff, some browsers would have sniffed the content and run it as JavaScript despite the wrong MIME type. With nosniff set correctly, the browser trusts the Content-Type header and blocks execution.


Using a Header Editor to Test Response Headers

Part of verifying security headers is testing how your application behaves when it receives them — not just whether your server is sending them. A header editor extension like HeaderSnap can inject response headers directly in the browser, letting you test your app’s behavior before configuring the actual server.

For example, to test how your page behaves when X-Frame-Options: DENY is active, create a HeaderSnap response rule that injects the header on responses from your development server. This lets you catch framing issues in your own UI before deploying.

The same applies to testing CSP: inject a Content-Security-Policy-Report-Only response header via HeaderSnap during local development and watch the console for violations. No server configuration changes needed.


The Checklist

Before marking security headers as “done”:

  • HSTS is present with max-age of at least 31536000
  • HSTS includes includeSubDomains if you’re setting Secure cookies on subdomains
  • CSP is in report-only mode during development; console shows no unexpected violations
  • CSP doesn’t include 'unsafe-inline' in script-src unless you’ve accepted that tradeoff
  • X-Frame-Options is verified by actually trying to embed the page in an iframe
  • X-Content-Type-Options: nosniff is present on all responses
  • CSP frame-ancestors and X-Frame-Options are not contradicting each other

Security headers are 30 minutes of server configuration with multi-year protection value. But only if they’re configured correctly and you’ve verified they’re doing what you intend.