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:
- Visit your site over HTTPS — Chrome will store the HSTS policy
- In the address bar, manually try
http://yourdomain.com— the browser should redirect to HTTPS before a request even hits the network - 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:
- Try loading an inline script: inject
<script>alert(1)</script>into the page (as a developer testing your own CSP) — ifscript-srcdoesn’t include'unsafe-inline', it should be blocked and appear as a CSP violation in the console - 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-ageof at least 31536000 - HSTS includes
includeSubDomainsif you’re settingSecurecookies on subdomains - CSP is in report-only mode during development; console shows no unexpected violations
- CSP doesn’t include
'unsafe-inline'inscript-srcunless you’ve accepted that tradeoff -
X-Frame-Optionsis verified by actually trying to embed the page in an iframe -
X-Content-Type-Options: nosniffis present on all responses - CSP
frame-ancestorsandX-Frame-Optionsare 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.