How the grade is calculated
The score is out of 100 and the rules never change between sites. Every point is tied to an observed header value, so two people checking the same URL get the same grade.
| Header | Max | Full marks require |
|---|---|---|
| Strict-Transport-Security | 25 | max-age of at least 15552000 (180 days) on an HTTPS response |
| Content-Security-Policy | 25 | An enforcing policy with no unsafe-inline/unsafe-eval and no wildcard in script-src |
| Frame protection | 15 | CSP frame-ancestors, or X-Frame-Options: DENY/SAMEORIGIN |
| X-Content-Type-Options | 10 | Exactly nosniff |
| Referrer-Policy | 10 | no-referrer, same-origin, strict-origin, or strict-origin-when-cross-origin |
| Permissions-Policy | 15 | Present and non-empty |
Grades: A at 90 or above, B at 75, C at 60, D at 45, E at 30, F below that. Partial credit exists where a header is present but weakened — a short HSTS max-age, a CSP that still allows unsafe-inline, a Referrer-Policy that leaks full URLs cross-origin.
Read the full explanation of security headers checker results ↓Show less ↑
What each header is actually defending against
Strict-Transport-Security
HSTS tells the browser to refuse plaintext HTTP to this host for max-age seconds. It closes the window where a user types example.com, gets an HTTP request on the wire, and an attacker on the same network intercepts the redirect. Set max-age=31536000; includeSubDomains, and add preload plus a submission to hstspreload.org only once you are certain every subdomain — including ones you have forgotten — serves valid HTTPS. Preload is close to irreversible: removal takes months to propagate through browser releases.
HSTS on an HTTP response is ignored entirely by browsers, which is why this tool scores it zero when the final URL is plaintext. Fix the redirect first.
Content-Security-Policy
CSP is the only header on this list that stops injected script from executing. Everything else mitigates; this one prevents. It is also the hardest to deploy, because a policy that is slightly wrong breaks your own site.
The rollout that works: publish the policy in Content-Security-Policy-Report-Only with a report-uri/report-to endpoint, leave it for a fortnight, fix everything the reports surface, then move the identical value to the enforcing header. Note that report-only mode blocks nothing — a site with only the report-only header gets no points here, deliberately.
A policy containing script-src 'unsafe-inline' gives up most of the protection, because injected inline script is precisely what XSS delivers. The modern pattern is a per-response nonce with 'strict-dynamic', which lets your bootstrap script load its own dependencies without enumerating every CDN host.
Frame protection
Clickjacking overlays your page in an invisible iframe on an attacker's site so clicks land on your buttons. CSP frame-ancestors is the standardised control and supports an allowlist of origins; X-Frame-Options is the legacy header with only DENY and SAMEORIGIN (the ALLOW-FROM variant was removed from every browser). Send frame-ancestors, and keep X-Frame-Options: SAMEORIGIN alongside it for very old clients.
X-Content-Type-Options
One value, nosniff, one job: stop browsers second-guessing your Content-Type. Without it, a file a user uploaded as text/plain can be sniffed as HTML or JavaScript and executed in your origin. There is no downside and no compatibility risk — this is the cheapest 10 points on the page.
Referrer-Policy
Controls how much of the current URL is sent in the Referer header on outbound requests. It matters because password-reset links, invite tokens, and session identifiers regularly live in query strings, and unsafe-url sends the lot to every third-party script on the page. strict-origin-when-cross-origin is the sensible default and is what browsers now use when you send nothing — but send it explicitly so the behaviour cannot drift.
Permissions-Policy
Denies powerful browser APIs — camera, microphone, geolocation, payment — to your page *and to every iframe it embeds*. That second part is the point: a compromised third-party widget cannot prompt your users for camera access if the policy forbids it. It replaces the obsolete Feature-Policy header, which used different syntax and is no longer honoured.
A starting configuration
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
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'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;add_header in a nested block silently discards every add_header inherited from the parent. If your headers vanish on one path, look for an add_header inside that location. The always flag is also required, or the headers are omitted on error responses — exactly where they still matter.curl -sL -o /dev/null -D - https://example.com \
| grep -Ei 'strict-transport-security|content-security-policy|x-frame-options|x-content-type-options|referrer-policy|permissions-policy'Headers are only meaningful over a working TLS connection, so confirm the certificate and chain with the SSL certificate checker first — HSTS in particular is actively dangerous to enable on a host whose certificate renewal is unreliable, because browsers will refuse to let users click through the error. To see the full response including caching and CDN headers, use the HTTP headers checker.