---
title: Scanner says "missing security headers"? How to check with the Security Headers Checker
description: A pentest report or a security questionnaire lists "missing HTTP security headers" and you need to know which ones, on which URL, and what to add. To check security headers, fetch the page the way a browser does and look for seven response headers. Here's how to do it with the free Security Headers Checker, what each missing header leaves open, and the config that adds them, even on a static host.
canonical: https://watchfor.io/blog/how-to-check-security-headers
---

# Scanner says "missing security headers"? How to check with the Security Headers Checker

2026-10-04 · WatchFor Team · Guides · guides, security, http

A pentest report or a security questionnaire lists "missing HTTP security headers" and you need to know which ones, on which URL, and what to add. To check security headers, fetch the page the way a browser does and look for seven response headers. Here's how to do it with the free Security Headers Checker, what each missing header leaves open, and the config that adds them, even on a static host.

To check security headers, request the page as a browser would and look for
seven response headers: `Strict-Transport-Security`,
`Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`,
`Referrer-Policy`, `Permissions-Policy` and the legacy `X-XSS-Protection`.
Each one present is a protection the browser switches on for your page; each
one missing is a finding a scanner will list.

The free [Security Headers Checker](/security-headers-checker) fetches the
URL from a WatchFor probe and lists all seven with the value sent, or
"missing", and one line on what each does. No signup.

## What this tool answers

*Which security headers does this URL actually send, and what does each
missing one leave open?* Reach for it when a penetration test, a vendor
security questionnaire or an automated scanner flags "missing security
headers", after moving a site to a new host or CDN, or after a framework
upgrade. It checks the exact response a visitor gets, from outside your
network, so a header set only in your local dev server or only on some paths
shows up as missing.

## A real example

### Checking security headers on a static site that a scanner flagged

A common finding lands on documentation and marketing sites: "6 of 7
security headers missing". These are often static sites on a host where
nobody has ever set a header. We checked one such site.

In the [Security Headers Checker](/security-headers-checker) we entered
`https://playwright.dev/`, a documentation site served by GitHub Pages
(`Server: GitHub.com`, behind a Fastly cache), kept **GET** and **Follow
redirects** on, and pressed **Fetch headers**. The run came from the Vilnius
probe:

![The Security Headers Checker result for https://playwright.dev/: status 200, TTFB 204 ms; strict-transport-security present with max-age=31556952, and content-security-policy, x-frame-options, x-content-type-options, referrer-policy, permissions-policy and x-xss-protection all marked missing in red, each with a one-line explanation](/blog/how-to-check-security-headers-result.webp)

Status **200** in 204 ms. Of the seven headers, **one is present**:
`Strict-Transport-Security: max-age=31556952`, which is one year. The other
six are missing:

- **`Content-Security-Policy`**: no limit on where scripts may load from,
  so an injected script runs. The most valuable of the six.
- **`X-Frame-Options`**: any site can frame the page (clickjacking). Low
  risk for read-only docs, high for anything with buttons that act.
- **`X-Content-Type-Options`**: browsers may guess a file's type and run it
  as script. Worth fixing, and it's one line.
- **`Referrer-Policy`**: browsers fall back to their default,
  `strict-origin-when-cross-origin` in current ones. Low; set it so older
  browsers behave the same.
- **`Permissions-Policy`**: embedded code could ask for the camera, location
  and so on. Low unless you embed third parties.
- **`X-XSS-Protection`**: nothing; modern browsers ignore it. Don't add it,
  or send `0`.

**The answer:** five real gaps and one non-issue. GitHub Pages has no
setting for custom response headers, so on a host like that the fix goes in
front of it or in the page:

- **A CDN or proxy in front** adds headers to every response (on Cloudflare,
  a Response Header Transform Rule; on any reverse proxy, a line per header).
- **CSP can go in the HTML** as
  `<meta http-equiv="Content-Security-Policy" content="default-src 'self'">`,
  but a meta policy can't use `frame-ancestors`, so clickjacking still needs
  a real header.
- **Hosts with header files** do it in config: Netlify `_headers`, Vercel
  `headers` in `vercel.json`, Cloudflare Pages `_headers`.

On a server you control, nginx needs one line each:

```nginx
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'" always;
```

Start CSP as `-Report-Only`, watch what it would block, then enforce it.
[Content Security Policy explained](/blog/content-security-policy-explained)
walks through that rollout.

## How to read the results

| What you see | What it means |
| --- | --- |
| **Status and TTFB** | The final response's status code and time to first byte. Headers on a 301 or a 404 can differ from the 200 page, so check the status first. |
| **DNS / connect / TLS / TTFB** | Where the request's time went. |
| **Header name in green, PRESENT** | The header came back; its exact value is shown, so you can check it's the value you meant. |
| **Header name in red, MISSING** | Not in the response. The line under it says what that leaves open. |
| **Full header list** | Below the table, every response header as sent, security headers in green. |

Present is not the same as correct: `max-age=0` in HSTS switches the policy
off, and a CSP with `'unsafe-inline'` in `script-src` allows the inline
scripts it's meant to stop. Read the values.

## More ways to use it

- **Answering a security questionnaire.** Run the main pages (home, login,
  app) and paste the values, not a grade.
- **After a CDN or host migration.** Headers set at the old origin or old CDN
  often don't survive the move; compare the list before and after.
- **Checking error pages and redirects.** Run a URL that 404s, and one that
  redirects with **Follow redirects** off. In nginx, `add_header` without
  `always` skips error responses, a common gap.
- **Vendor and embed review.** Check whether a third-party widget's page can
  be framed before you embed it, or whether yours can be.
- **HSTS in depth.** The [HSTS guide](/blog/how-to-check-hsts) covers
  max-age, `includeSubDomains` and the preload list.

## Tips and gotchas

- **One URL is one response.** Headers can differ per path, per status and
  per server behind a load balancer. Check the pages that matter, not only
  the homepage.
- **Headers in a `location` block replace the server's.** In nginx, any
  `add_header` inside a `location` drops all `add_header` lines inherited
  from the `server` block for that location.
- **`X-XSS-Protection` is legacy.** The checker lists it because scanners
  still do. Missing is fine; if sent, `0` is the safe value.
- **Not every header matters for every URL.** An API that only returns JSON
  to other servers gets little from CSP or frame protection; the pages
  people open in a browser need them. The
  [security headers checklist](/blog/security-headers-checklist) has values
  for each.
- **The checker reads headers; it doesn't test exploits.** A missing header
  is a missing defence, not proof of a vulnerability.

## Check it from the command line too

`curl -sI` prints the response headers; filter for the seven:

```bash
curl -sI https://playwright.dev/ | grep -iE '^(strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|x-xss)'
```

```text
strict-transport-security: max-age=31556952
```

One line out of seven, the same as the checker. For comparison, a site that
sends all of them:

```bash
curl -sI https://www.cloudflare.com/ | grep -iE '^(strict-transport|x-frame|x-content-type|referrer-policy|permissions-policy|x-xss)'
```

```text
strict-transport-security: max-age=31536000; includeSubDomains
permissions-policy: geolocation=(), camera=(), microphone=()
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
```

(It sends a long `Content-Security-Policy` too, left out here.) Note the
last line: present, but `1; mode=block` is the value current advice says to
replace with `0`.

## Keep watching it

Security headers vanish without anyone noticing: a new `location` block, a
CDN change, a framework upgrade. A
[page integrity monitor](/page-integrity-monitoring) keeps the security
headers of each watched page in its baseline and raises a critical **Security
header weakened** finding when `Content-Security-Policy`, HSTS,
`X-Frame-Options` or `X-Content-Type-Options` is removed, HSTS gets shorter,
or the CSP starts allowing inline, eval or any-host scripts.

## FAQ

**How do I check what security headers a website has?**
Fetch the page and read its response headers: the Security Headers Checker
lists the seven common ones with their values, `curl -sI https://example.com`
prints them all, and the Network tab in browser developer tools shows them
under the document request.

**Which security headers are required?**
None is required by browsers, but scanners and auditors expect
`Strict-Transport-Security`, `Content-Security-Policy`,
`X-Content-Type-Options`, frame protection (`X-Frame-Options` or CSP
`frame-ancestors`) and `Referrer-Policy`. `Permissions-Policy` is
increasingly expected. `X-XSS-Protection` is obsolete.

**Can I add security headers on GitHub Pages?**
Not as response headers: GitHub Pages has no setting for them. Put a CDN or
proxy in front that adds them, or set a CSP with a `<meta>` tag, which works
for most directives but not `frame-ancestors`.

---

Canonical page: https://watchfor.io/blog/how-to-check-security-headers · All articles: https://watchfor.io/blog/all.md · Site guide: https://watchfor.io/llms.txt
