security headers test: how to check your site
A step-by-step skill for verifying security headers on a live site: fetching headers, checking HSTS, CSP, framing, content types, and referrer policy, plus what each missing header means. Use when an agent audits a deployment or verifies a headers change took effect. Triggers: 'test security headers', 'check security headers', 'verify CSP header', 'headers audit'. Not for: writing the header configuration, TLS certificate checks, or CSP policy authoring.
security headers test: how to check your site
TL;DR
You can verify most of your security posture with one command that prints response headers. Fetch the headers, confirm HSTS, CSP, framing protection, content-type options, and referrer policy are present and sane, and treat every missing one as a ticket. Testing is separate from configuring: this skill proves what is actually served, not what the config file says.
security headers test: how to check your siteUse this when
- You want to verify headers on a staging or production deploy
- A headers change just shipped and you need proof it took effect
- An audit asks for the current header state of the site
- An agent is doing a deployment smoke test
- A scanner flagged a missing header and you want to confirm manually
Not for this skill when
- You are writing the web server config that sets the headers (different skill)
- You are authoring the CSP policy itself (policy design, separate topic)
- You are checking TLS certificates or cipher suites
- You need continuous monitoring rather than a point-in-time check
Steps
1. Fetch the raw response headers
Start with the simplest possible probe: request the homepage and print the headers exactly as served.
curl -sI https://example.comExpected: you see the full header list. If a header you expect is absent here, it is absent for every visitor; there is no "it works in the app" beyond this output.
2. Check HSTS
Look for strict-transport-security with a long max-age and includeSubDomains. It tells browsers to never use plaintext HTTP for your domain again.
curl -sI https://example.com | grep -i "strict-transport-security"Expected: a max-age of at least one year (31536000) and includeSubDomains. A missing HSTS header means the first visit and any downgrade attempt are still exposed.
3. Check the Content-Security-Policy
Confirm a content-security-policy header exists and read it critically: script-src should not allow unsafe-inline on pages rendering user content, and object-src should be none.
curl -sI https://example.com | grep -i "content-security-policy"Expected: the policy is present and restrictive. A policy that allows everything is worse than none, because it looks like protection in a checklist.
4. Check framing and content-type protections
Verify x-frame-options (DENY or SAMEORIGIN) or a frame-ancestors directive in the CSP, plus x-content-type-options set to nosniff. These stop clickjacking and MIME-sniffing attacks respectively.
curl -sI https://example.com | grep -i "x-frame-options\|x-content-type-options"Expected: both present with the strict values. Missing framing protection on pages with sensitive actions is a clickjacking finding.
5. Check referrer and permissions policies
Look for referrer-policy (something like strict-origin-when-cross-origin or stricter) and a permissions-policy that disables powerful browser features the site does not use, such as camera or geolocation.
curl -sI https://example.com | grep -i "referrer-policy\|permissions-policy"Expected: referrer leakage to third parties is limited and unused capabilities are switched off. Defaults that leave everything enabled are the common miss here.
6. Repeat across page types and subdomains
Headers set at the app layer may not apply to static assets, error pages, API responses, or sibling subdomains. Probe each surface; attackers use the weakest one.
for p in "/" "/login" "/api/v1/status" "/404-probe"; do echo "== $p"; curl -sI "https://example.com$p" | grep -ci "content-security-policy"; doneExpected: consistent coverage everywhere that serves your content. A login page without the headers the homepage has is the exact page an attacker wants.
Variant: testing behind a CDN or proxy
CDNs can add, strip, or override headers. Test the origin directly and the edge URL separately, and confirm which layer owns each header so a config change lands in the right place.
Variant: single-page apps
SPAs serve one HTML shell for every route, so test the shell plus the API responses. API JSON responses need nosniff and framing protection too, since they can be embedded or sniffed in older browsers.
Variant: grading the results
Online header scanners give letter grades that are handy for tracking over time, but read their individual findings rather than chasing the grade. A site can score well while missing the one header that matters for its threat model.
Why this happens
Headers are set in server config, CDN rules, framework middleware, and sometimes all three, so the running site drifts from what any single config file claims. Caching layers serve stale header sets, new routes miss the middleware, and nobody re-checks after the initial setup. The live response is the only source of truth, which is why testing beats reading config.
Edge cases and pitfalls
- Redirect chains can serve different headers at each hop; check the final destination, not just the first response.
- Report-only CSP headers do not enforce anything; confirm the enforcing header exists separately.
- Some frameworks set headers only on successful responses; error pages may go out unprotected.
- Header names are case-insensitive but some tooling is not; keep casing consistent anyway.
- Re-test after every infrastructure change: new CDN, new ingress, new framework version.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_oZnM08VUwNzGEdY3uMZXdQ
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.