HTTP header checker
- Our server fetches the URL you enter
- Nothing is stored
- Free, no sign-up
ToolPetal's server requests the URL once as ToolPetalBot from a Cloudflare datacenter, reads only the response headers, and stores nothing. Cookie values are redacted before they reach your browser. Grading runs in your browser.
Grade pasted headers instead
How the 100 points are split
Seven checks, graded on the final response only. HSTS and CSP carry half the score because they block the attacks that actually happen to ordinary sites: downgrade to HTTP on public Wi-Fi, and injected script. The rest are one-line headers with no reason to be missing.
| Check | What it stops | Full points for | Partial credit | Points |
|---|---|---|---|---|
| Strict-Transport-Security | Downgrade to plain HTTP on later visits | max-age of 31536000 (one year) or more | 15 for one day up to a year, 5 under a day, 0 for max-age=0 | 25 |
| Content-Security-Policy | Injected scripts running on your pages | script-src (or default-src) with no 'unsafe-inline' unless a nonce or hash is present, and no *, https:, http:, data: or blob: script sources | 20 with 'unsafe-eval', 12 with 'unsafe-inline' or wide sources, 10 if scripts are not restricted at all, 5 for Report-Only | 25 |
| frame-ancestors or X-Frame-Options | Clickjacking through a hidden iframe | CSP frame-ancestors without wildcards, or X-Frame-Options DENY or SAMEORIGIN | 5 for wildcard frame-ancestors or the dead ALLOW-FROM | 15 |
| X-Content-Type-Options | Browsers guessing a file is script or HTML | nosniff | None | 10 |
| Referrer-Policy | Paths and query strings leaking to other sites | no-referrer, same-origin, strict-origin or strict-origin-when-cross-origin | 7 for origin or origin-when-cross-origin, 5 when missing, 4 for no-referrer-when-downgrade, 0 for unsafe-url | 10 |
| Permissions-Policy | Embedded content asking for camera, mic or location | Any non-empty policy | 2 for the old Feature-Policy name | 5 |
| Set-Cookie flags | Session cookies sent over HTTP or on cross-site requests | Every cookie Secure with a SameSite value, or no cookies at all | 6 if all are Secure but some lack SameSite | 10 |
Grades: A is 90 to 100, B is 75 to 89, C is 60 to 74, D is 45 to 59, F is under 45. A final page served over plain HTTP gets an F whatever the points say, because every other header can be stripped in transit.
Worked example: a C that becomes an A
A typical shop on Express behind a CDN returns this:
HTTP/2 200
strict-transport-security: max-age=15552000
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
set-cookie: session=[redacted]; Path=/; Secure; HttpOnly
x-powered-by: Express
| Check | Earned | Why |
|---|---|---|
| Strict-Transport-Security | 15 / 25 | 180 days is under a year |
| Content-Security-Policy | 12 / 25 | 'unsafe-inline' with no nonce or hash, so any injected inline script runs |
| Clickjacking | 15 / 15 | X-Frame-Options SAMEORIGIN |
| X-Content-Type-Options | 10 / 10 | nosniff |
| Referrer-Policy | 5 / 10 | Missing, so it rides on the browser default |
| Permissions-Policy | 0 / 5 | Missing |
| Set-Cookie flags | 6 / 10 | Secure, but no SameSite |
That is 63 points, a C. The X-Powered-By line costs nothing but shows up as a note. Five changes take it to 100:
- max-age=31536000; includeSubDomains (+10)
- Swap 'unsafe-inline' for a per-request nonce, and add object-src 'none'; base-uri 'self' (+13)
- Referrer-Policy: strict-origin-when-cross-origin (+5)
- Permissions-Policy: camera=(), microphone=(), geolocation=() (+5)
- SameSite=Lax on the session cookie (+4)
Paste both versions into Grade pasted headers to watch the score move. The nonce is the only change that needs code; everything else is config.
Reading the redirect hops
The hop table lists every response in order: its status code, the URL that was requested, and where the Location header pointed. Each redirect hop's own headers sit in a collapsed block under the final header list. For longer chains, loops and www or http mix-ups, the redirect chain checker follows up to 10 hops and says which ones to collapse.
- http:// to https:// with a 301 or 308, then a 200, is the normal setup. A 302 there works but tells caches and crawlers the move is temporary.
- HSTS sent on a plain HTTP hop is ignored by browsers. It only counts on HTTPS responses.
- HSTS sent by an earlier HTTPS hop on the same host still counts in browsers, but this grade looks only at the final response. If you set HSTS at the redirect layer only, open that hop's headers before trusting the score.
- After 5 redirects the check stops and shows the chain so far.
Flagged but not scored
These show up as notes under the grade. They are worth fixing, or worth knowing, but they do not change the letter:
X-Powered-Byat any value, and aServerheader that includes a version number.X-XSS-Protectionset to anything except 0.Access-Control-Allow-Origin: *, which lets any site read the response with fetch().Cross-Origin-Opener-Policy,Cross-Origin-Embedder-PolicyandCross-Origin-Resource-Policyare listed when present, since they matter for cross-origin isolation, not for a basic grade.- Cookies without HttpOnly are named in the cookie row. That flag is right for session cookies and wrong for cookies your own JavaScript reads, so it is not scored.
Setting the headers on Nginx, Apache and _headers files
Ship CSP as Content-Security-Policy-Report-Only first if you have inline scripts or third-party tags. A strict policy will break them, and Report-Only tells you where.
Nginx. Use always so the headers are also sent on 4xx and 5xx responses. An add_header inside a location block replaces every add_header inherited from the server block, so repeat them there or keep them all at one level.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
Apache with mod_headers:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header unset X-Powered-By
A _headers file (Cloudflare Pages, Netlify) at the root of the build output. On Cloudflare Pages these rules apply to static files, not to responses from Pages Functions, which have to set their own headers.
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Add cookie flags where the cookie is created, in your framework or session middleware, not in the web server.
Where this check can be wrong
- One request, from a Cloudflare datacenter, as ToolPetalBot, with no cookies. Bot rules, WAF challenges, geo rules, A/B tests and logged-in pages can all return different headers than your browser gets. A 403 or challenge page usually means the site blocked the bot, and its headers are the block page's.
- Only the final response is graded. Images, scripts and API routes on the same site can send different headers; check them by URL one at a time.
- The CSP check is shallow. It reads script-src or default-src for 'unsafe-inline', 'unsafe-eval' and wide sources, reads frame-ancestors, and notes a missing object-src or base-uri. It does not judge host allowlists (a whitelisted CDN can still host a bypass), style-src, reporting directives or a CSP set in a meta tag.
- The body is never read, so nothing set in HTML meta tags is seen, and cookies set by JavaScript are invisible.
- HSTS preload list membership is not checked, and includeSubDomains is noted but not scored.
- Public hosts on ports 80 and 443 only, 5 redirects, 8 seconds, 20 checks per minute. Up to 100 headers per response are shown; values longer than 8192 characters are cut.
- The weights are ToolPetal's own. Other graders score differently, and none of them is a security audit.
FAQ
Why is my grade different on securityheaders.com or Mozilla Observatory?
Each grader uses its own weights. This one splits 100 points across seven checks, listed in the points table on this page, and grades only the final response. It also fetches from a Cloudflare datacenter as ToolPetalBot, so a site that treats bots differently can send different headers here.
Does the checker send my cookies or log in?
No. ToolPetal's server makes one plain GET request with no cookies and no credentials, so you see what a logged-out visitor gets. Set-Cookie values are replaced with [redacted] before the result reaches your browser. Only cookie names and flags are shown.
Which response gets the grade when a URL redirects?
The last one, the response that did not redirect. Every hop is listed with its status code and Location, and each hop's own headers can be opened separately. Up to 5 redirects are followed.
Do I need X-Frame-Options if my CSP has frame-ancestors?
Not for current browsers. They use frame-ancestors and ignore X-Frame-Options when both are sent. This checker gives full clickjacking points for either one. Keeping X-Frame-Options as well only helps very old browsers.
Why is X-XSS-Protection not scored?
Current browsers no longer have the XSS filter that header controlled, so it does nothing in them. MDN and OWASP recommend sending 0 or leaving it out and relying on Content-Security-Policy. The checker adds a note for any value other than 0 but gives it no points.
Can I check a staging site or an internal URL?
Not by URL. The fetch refuses private, loopback and internal addresses and only uses ports 80 and 443. Copy the response headers instead, for example from curl -sI or your browser's network panel, and use Grade pasted headers. That part runs entirely in your browser.