How to check security headers
Send one HEAD request, save the response headers, then print a value or the word MISSING for each header you care about. The six that carry weight are strict-transport-security, content-security-policy, x-frame-options, x-content-type-options, referrer-policy and permissions-policy. The printed list is your verdict.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
Security headers are set by the CDN, the reverse proxy or a framework middleware, and all three change without touching application code. Run this sweep at release sign-off and again after any infrastructure change. It catches the case where a proxy rule is scoped to one path prefix, so the marketing pages keep their content-security-policy while the authenticated dashboard, served from another upstream, ships with none.
Prerequisites
- curl 8 or later. Any TLS build works; nothing here needs HTTP/2. See the curl manual.
- The URL of the route you are signing off. A header set on
/is not evidence about/app/settings. - The MDN reference on HTTP headers for the meaning of each value.
Steps
- Step 1.
Sweep a route that sets nothing, so you know what an absent header looks like.
curl -sI https://example.com/ | tr -d '\r' > h.txt; for n in strict-transport-security content-security-policy x-frame-options x-content-type-options referrer-policy permissions-policy; do v=$(grep -i "^$n:" h.txt | cut -d: -f2- | sed 's/^ //' | cut -c1-38); printf '%-26s %s\n' "$n" "${v:-MISSING}"; donestrict-transport-security MISSING content-security-policy MISSING x-frame-options MISSING x-content-type-options MISSING referrer-policy MISSING permissions-policy MISSING - Step 2.
Run the same sweep against the route you are signing off.
curl -sI https://www.cloudflare.com/ | tr -d '\r' > h.txt; for n in strict-transport-security content-security-policy x-frame-options x-content-type-options referrer-policy permissions-policy; do v=$(grep -i "^$n:" h.txt | cut -d: -f2- | sed 's/^ //' | cut -c1-38); printf '%-26s %s\n' "$n" "${v:-MISSING}"; donestrict-transport-security max-age=31536000; includeSubDomains content-security-policy default-src 'self'; script-src 'self' x-frame-options SAMEORIGIN x-content-type-options nosniff referrer-policy strict-origin-when-cross-origin permissions-policy geolocation=(), camera=(), microphone=The last two values are cut at 38 characters by
cut -c1-38. Read a full value with a plaingreponce a name is on your list. - Step 3.
Read the HSTS lifetime in full, because its number decides whether the header does anything.
curl -sI https://www.cloudflare.com/ | tr -d '\r' | grep -i '^strict-transport-security:'strict-transport-security: max-age=31536000; includeSubDomains - Step 4.
Repeat the sweep on a response that is not a 200, using a GET so the redirect itself is shown.
curl -s -D - -o /dev/null https://www.cloudflare.com/h2check-no-such-page | tr -d '\r' > r.txt; head -1 r.txt; for n in strict-transport-security content-security-policy x-frame-options x-content-type-options; do v=$(grep -i "^$n:" r.txt | cut -d: -f2- | sed 's/^ //' | cut -c1-38); printf '%-26s %s\n' "$n" "${v:-MISSING}"; doneHTTP/2 301 strict-transport-security MISSING content-security-policy MISSING x-frame-options MISSING x-content-type-options MISSINGThe same host that passed step 2 sets none of the four on its 301. Record the status code next to every verdict.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| A value on every line | The route sets all six | Read the values. Presence is not correctness, and step 3 shows why. |
| MISSING on one line | That header is absent on this route | Find the layer that should set it: CDN rule, proxy config, or middleware. |
| MISSING everywhere | No header layer is applied to this route | The route is served by an upstream the header config never reached. |
| Values differ between two routes | The header rule is path scoped | Sweep every route class: HTML, API, static asset, error page. |
| max-age=0 in HSTS | The policy is switched off | Someone disabled HSTS deliberately. Confirm it was intended before release. |
Common mistakes
Thresholds
is the minimum HSTS lifetime accepted for the preload list, which is one year in seconds. A shorter value leaves a window where the first request of a session can still go out over plain HTTP.
Source: hstspreload.org submission requirementsWhat to check next
- How to check CSP header: the one header in the sweep whose value needs reading directive by directive.
- How to check if HSTS is enabled: the number from step 3, and what
includeSubDomainsadds to it. - How to check X-Frame-Options: why
SAMEORIGINin step 2 may not be the rule the browser applies. - How to check X-Content-Type-Options: the shortest value in the sweep and the attack it removes.
- Security headers checker: the same sweep against a URL you paste.
FAQ
How to check security headers of a website?
Run step 1 and step 2 against the URL. Repeat for each route class the site serves: an HTML page, an API endpoint, a static asset and an error page. A single sweep of the domain root describes one route, not the site.
How to check security headers in Chrome DevTools?
Open DevTools, Network tab, reload, click the document request, then the Headers panel and the Response Headers section. DevTools shows the headers the browser received, including those added by a service worker, which curl cannot see.
Which headers should be in the sweep?
The six in step 1 cover transport, script execution, framing, MIME sniffing, referrer leakage and browser feature access. Add cross-origin-opener-policy and cross-origin-resource-policy when the application uses cross-origin isolation.
Does a missing header always mean a defect?
No. permissions-policy on a static documentation site changes nothing. Decide per route what the header would protect, then file the absence as a defect only where something is exposed.
Can I run this against a staging host behind basic auth?
Yes. Add -u user:pass to the curl command. The header sweep reads the response to the authenticated request, which is the response the release will serve.
Verified
Verified by Maks Vernycurl 8.21.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
- Checker: security-headers CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
- Security headers review
- Website launch checklist
- All security headers and tls checks
basic6 minpublished updated Maks Verny