The problem: you ran a URL-based security header scanner and it returned simulated results
You typed your domain into a security headers scanner. The result page says "Demo mode: simulated analysis. Production use requires a backend API." The headers it scored weren't your headers — they were random. The tool's author is being honest with you: browsers can't read security headers cross-origin. The fetch() API can only read CORS-safelisted response headers (Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma). Security headers like Strict-Transport-Security, Content-Security-Policy, and X-Frame-Options are NOT on the safelist. A cross-origin fetch() to your site cannot read them unless your server sends Access-Control-Expose-Headers naming every security header — which almost no site does. The honest move is a checker that skips the fetch entirely: you paste the raw headers from DevTools or curl -I, the tool parses them client-side, runs 12 real checks with per-header misconfiguration detection, scores 0-100, and grades A+ through F.
Fastest path
Open the Security Headers Checker. In a terminal, run curl -I https://example.com. Copy the output. Paste it into the tool. Click Analyze.
Pasted headers (from curl -I https://example.com):
HTTP/2 200
content-type: text/html
strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
x-xss-protection: 1; mode=block
set-cookie: session=abc123; Path=/
→ Score: C+ (68/100)
→ Grade letter: C+ Pass: 5 Warn: 3 Fail: 4
Critical:
✓ Strict-Transport-Security — present (max-age=31536000, preload ✓)
⚠ Content-Security-Policy — misconfigured (script-src 'unsafe-inline' disables XSS protection)
✓ X-Content-Type-Options — present (nosniff)
High:
✓ X-Frame-Options — present (DENY)
✓ Referrer-Policy — present (strict-origin-when-cross-origin)
⚠ Set-Cookie — misconfigured (session cookie missing Secure, HttpOnly, SameSite)
Medium:
✗ Permissions-Policy — missing
✗ Cross-Origin-Opener-Policy — missing
✗ Cross-Origin-Embedder-Policy — missing
✗ Cross-Origin-Resource-Policy — missing
Low:
⚠ X-XSS-Protection — misconfigured (set to 1; mode=block, should be 0)
⚠ Cache-Control — missing (sensitive pages should use no-store)
→ Fix order: CSP unsafe-inline → Set-Cookie flags → X-XSS-Protection=0 → COOP/CORP/COEP
The tool parsed your pasted headers, ran 12 real checks (11 security headers + Set-Cookie), flagged 3 as misconfigured (present but wrong), scored 68/100, and sorted recommendations by severity. The rest of this guide is why the paste path beats the fetch path, why misconfigured earns 0.4 not 0.5, why ALLOW-FROM is a dead directive, why SameSite=None requires Secure, and why X-XSS-Protection=1 is worse than missing.
The substance: 12 checks, 3 states, real values
Why paste works where fetch fails
The tool has no fetch() call. You paste the raw HTTP headers — from curl -I https://example.com, from DevTools → Network → click the first request → Headers tab → "Copy as cURL" (then strip to just the response headers), or from any proxy that prints response headers. The tool's parseRawHeaders function splits on newlines, skips the HTTP status line via /^HTTP\/[\d.]+\s+\d+/, finds the first colon in each line, lowercases the key, and trims the value. The result is a ParsedHeader[] array. The 12 checks then run against that array via findHeader(parsed, 'strict-transport-security') — a linear scan with case-insensitive key match.
This works because the browser CORS safelist only restricts which headers JavaScript can READ from a cross-origin fetch() response. It does not restrict what JavaScript can parse from a string you pasted into a <textarea>. The headers in the textarea are your data, not a cross-origin response. The tool reads them like it reads any user input — no CORS, no preflight, no Access-Control-Expose-Headers negotiation. This is why the paste-based checker does real analysis while the URL-based Security Headers Analyzer runs in demo mode with Math.random(). The analyzer's value is its 13-header reference and server config generation; the checker's value is the actual scan of your actual headers.
Three states, not two: present, missing, misconfigured
The URL-based analyzer has three states too, but in demo mode you never see them work. The checker's 12 analyzers each return one of three HeaderStatus values:
- present — the header exists and its value passes the check. Full severity weight earned.
- missing — the header doesn't exist. Zero weight earned.
- misconfigured — the header exists but its value is wrong, partial, or deprecated. 40% of severity weight earned.
The 0.4 multiplier for misconfigured is the key insight. A header set incorrectly is not the same as a header not set — a misconfigured CSP with script-src 'unsafe-inline' at least blocks other attack vectors, while a missing CSP blocks nothing. But a misconfigured header is also not the same as a correct one — script-src 'unsafe-inline' disables CSP's XSS protection, making the header decorative. The 0.4 multiplier says "you tried, you got partial credit, but the protection you needed isn't there." A score of 68/100 with 5 present, 3 misconfigured, 4 missing reflects this: the misconfigured headers contributed 0.4 × their weight, not the 0.5 the URL-based analyzer uses for "warning" status. The 0.4 is harsher because "misconfigured" is a stronger claim than "warning" — the tool verified the value is wrong, not just that it might be.
The 12 checks and what each one verifies
Critical (weight 25):
- Content-Security-Policy — parses the header into a directive Map, checks for
default-src, flagsscript-srccontaining'unsafe-inline','unsafe-eval',data:, or*. Flags missingframe-ancestors,base-uri,form-action. A CSP withscript-src 'unsafe-inline'is misconfigured, not present — the inline-script allowance disables XSS protection. - Strict-Transport-Security — regex-extracts
max-age=(\d+), flags if< 31536000(1 year), flags missingincludeSubDomains, flags missingpreload. A 6-month HSTS withoutpreloadis misconfigured. - X-Content-Type-Options — the only valid value is
nosniff. Anything else is misconfigured. No partial credit for "nosniff; charset=utf-8" — that's not the header's syntax.
High (weight 15):
- X-Frame-Options —
DENYandSAMEORIGINpass.ALLOW-FROM <origin>is misconfigured — the directive is deprecated and unsupported by Chrome, Firefox, and Safari. The tool flags it and recommends CSPframe-ancestorsinstead. - Referrer-Policy — validates against the 8-value enum (
no-referrer,no-referrer-when-downgrade,origin,origin-when-cross-origin,same-origin,strict-origin,strict-origin-when-cross-origin,unsafe-url).unsafe-urlis flagged as misconfigured because it leaks the full URL including path and query string to any origin.no-referrer-when-downgradeis flagged because it leaks paths to same-protocol destinations — the tool recommendsstrict-origin-when-cross-origininstead. - Set-Cookie — per-cookie analysis. Each
Set-Cookieheader is split on;, the flags are lowercased, and the tool checks forSecure,HttpOnly, andSameSite. A cookie missingSecureis flagged ("sent over plain HTTP"). A cookie missingHttpOnlyis flagged ("accessible via JavaScript — XSS risk"). A cookie missingSameSiteis flagged ("vulnerable to CSRF").SameSite=NonewithoutSecureis flagged — the spec requiresSecurewhenSameSite=None, and modern browsers reject the cookie otherwise.
Medium (weight 10):
- Permissions-Policy — parses
feature=allowlistpairs, flags any feature with*orself *as unrestricted.camera=()passes;camera=*is misconfigured. - Cross-Origin-Opener-Policy (COOP) — validates against
unsafe-none,same-origin-allow-popups,same-origin.unsafe-noneis flagged as misconfigured (it's the default and provides no isolation). - Cross-Origin-Embedder-Policy (COEP) — validates against
unsafe-none,require-corp,credentialless.unsafe-noneis flagged. - Cross-Origin-Resource-Policy (CORP) — validates against
same-site,same-origin,cross-origin.cross-originis flagged (allows any origin to load the resource). - Cache-Control — flags
publicfor sensitive pages, flags missing bothno-storeandprivate. ACache-Control: public, max-age=86400on an authenticated page is misconfigured.
Low (weight 5):
- X-XSS-Protection — deprecated.
0passes (explicitly disables the buggy auditor).1or1; mode=blockis misconfigured — the auditor introduced new vulnerabilities and can be exploited to selectively disable scripts. Missing is also flagged, but with a note that0is the recommended explicit value.
The Set-Cookie per-cookie analyzer
The tool's analyzeSetCookies function handles a case the URL-based analyzer doesn't: multiple Set-Cookie headers in one response. findAllHeaders(parsed, 'set-cookie') returns an array, and each cookie is analyzed individually. A response with Set-Cookie: session=abc; Secure; HttpOnly; SameSite=Strict and Set-Cookie: theme=dark; Path=/ gets two results: the session cookie passes, the theme cookie is flagged for missing Secure, HttpOnly, and SameSite. The overall Set-Cookie result is misconfigured if any cookie has issues.
The per-cookie breakdown matters because cookie flags are independent. A site that gets the session cookie right but the preference cookie wrong is still leaking the preference cookie over HTTP and exposing it to JavaScript. The tool's detail lines show each cookie's flag status: Cookie "session": Secure, HttpOnly, SameSite=Strict vs Cookie "theme": NO Secure, NO HttpOnly, NO SameSite. The fix is per-cookie, not site-wide.
ALLOW-FROM and the deprecated-directive trap
The tool's analyzeXFrameOptions has a specific branch for ALLOW-FROM:
if (upper.startsWith('ALLOW-FROM')) {
return { ...base, status: 'misconfigured',
explanation: 'ALLOW-FROM is deprecated and not supported by modern browsers. Use CSP frame-ancestors instead.',
details: ['ALLOW-FROM is not supported by Chrome, Firefox, or Safari.'] };
}
ALLOW-FROM <origin> was the spec's attempt to allow framing from one specific origin. It never worked correctly — Chrome never supported it, Firefox dropped it, Safari never implemented it. A site setting X-Frame-Options: ALLOW-FROM https://trusted.com thinks it's allowing framing from trusted.com, but modern browsers ignore the directive entirely and fall back to allowing framing from anywhere. The header is worse than missing because it creates a false sense of restriction. The tool flags it as misconfigured and points to CSP frame-ancestors https://trusted.com — the modern equivalent that browsers actually enforce.
This is the same pattern as X-XSS-Protection=1 — a deprecated value that's worse than the header being absent. The tool's three-state model handles this: missing earns 0, misconfigured earns 0.4, present earns 1.0. A deprecated value that creates a false sense of security lands in misconfigured, not present.
The scoring math and grade thresholds
The weights: critical: 25, high: 15, medium: 10, low: 5, info: 0. The total possible weight across 12 checks is 25×3 + 15×3 + 10×5 + 5×1 = 235. The score is earned / 235 × 100, rounded. A site with all 3 criticals present, all 3 highs present, all 5 mediums missing, and X-XSS-Protection misconfigured scores (75 + 45 + 0 + 2) / 235 × 100 = 51.9 — a D. The mediums matter: COOP, CORP, COEP, Permissions-Policy, and Cache-Control add up to 50 weight points, and missing all of them caps your score at ~52 even with every critical and high header perfect.
The grade thresholds: A+ (95+), A (90+), A- (85+), B+ (80+), B (75+), B- (70+), C+ (65+), C (60+), D (50+), F (below 50). The 0.4 multiplier for misconfigured means a site with every header present but 6 of them misconfigured scores 12 × weight × 0.4 / total — roughly 40-60 depending on which headers. Misconfigured headers don't sink you the way missing ones do, but they don't pass you either. A B requires most headers present AND correctly configured.
CSV and text report exports
The tool exports two formats. The CSV uses RFC 4180 quoting — header values containing commas, quotes, or newlines are wrapped in quotes with doubled internal quotes. The text report is a human-readable summary with [PASS], [WARN], [FAIL] markers per header and a numbered recommendations list sorted by severity. Both are generated client-side from the results array; neither requires a backend. The CSV is for pasting into a spreadsheet or a security review doc; the text report is for pasting into a ticket or a Slack thread.
Gotchas
- The tool can't fetch your headers for you. You must paste them from
curl -I, DevTools, or a proxy. This is a browser security limitation (CORS safelist excludes security headers), not a tool limitation. If you want a URL-based scan, use the Security Headers Analyzer — but know it runs in demo mode with simulated results. curl -Isends a HEAD request. Some servers respond differently to HEAD vs GET — they may omit headers likeSet-Cookiethat are only on GET responses. If you need the full response, usecurl -sv https://example.com 2>&1 | grep -i '^<'to capture the response headers from a GET.- Misconfigured earns 0.4, not 0.5. A misconfigured header is worse than the URL-based analyzer's "warning" status suggests. The 0.4 multiplier reflects that the header is verified wrong, not just possibly wrong. A site with 6 misconfigured headers and 6 present scores lower than a site with 12 present.
ALLOW-FROMis a dead directive. Chrome, Firefox, and Safari all ignoreX-Frame-Options: ALLOW-FROM <origin>. The header creates a false sense of restriction — the page is framable by anyone. Use CSPframe-ancestors <origin>instead, which browsers actually enforce.X-XSS-Protection=1is worse than missing. The XSS Auditor introduced new vulnerabilities and can be exploited to selectively disable scripts. Set it to0(explicitly disable) or omit it. Setting1; mode=blockis misconfigured, not present.SameSite=NonerequiresSecure. The spec mandates it, and modern browsers rejectSameSite=Nonecookies withoutSecure. The tool flags this combination. If you need cross-site cookies (e.g., third-party embeds), useSameSite=None; Secure.- The
Set-Cookieanalyzer runs per-cookie. A response with 3Set-Cookieheaders gets 3 per-cookie results. One cookie with issues makes the overall Set-Cookie result misconfigured. Fix each cookie independently — the session cookie and the theme cookie have different security requirements. unsafe-urlReferrer-Policy leaks full URLs. It sends the complete URL (path, query string) to any origin.no-referrer-when-downgradeleaks paths to same-protocol destinations. The tool flags both as misconfigured and recommendsstrict-origin-when-cross-origin— full URL same-origin, origin-only cross-origin, no referrer on HTTPS→HTTP.- COOP/COEP
unsafe-noneis the default. The tool flags it as misconfigured because it provides no isolation. If you can't setsame-origin(COOP) orrequire-corp(COEP) because of third-party embeds, usecredentiallessfor COEP — butunsafe-noneis not a passing grade. - The score caps at ~52 without the medium headers. COOP, CORP, COEP, Permissions-Policy, and Cache-Control add up to 50 weight points. Missing all of them caps your score at D even with every critical and high header perfect. The medium headers are where sites without a security review lose the most points.
Permissions-Policy: camera=*is misconfigured. Any feature with*orself *as the allowlist is unrestricted. The tool flags it. Usefeature=()to deny, orfeature=(self "https://trusted.com")to allow specific origins.- The CSV export quotes values with commas. A CSP value like
default-src 'self'; script-src 'self' 'unsafe-inline'contains semicolons but no commas — it won't be quoted. ASet-Cookievalue with commas (rare) will be. The quoting follows RFC 4180: double internal quotes, wrap in quotes.
Summary
- Paste works where fetch fails. Browsers can't read security headers cross-origin (CORS safelist excludes them). The checker skips the fetch — you paste raw headers from
curl -Ior DevTools, the tool parses them client-side, and 12 real checks run against your actual values. The URL-based Security Headers Analyzer runs in demo mode because of the same CORS wall. - Three states: present, missing, misconfigured. Present earns full weight, missing earns zero, misconfigured earns 0.4. The 0.4 multiplier is harsher than the URL-based analyzer's 0.5 because "misconfigured" is a verified-wrong value, not a maybe-wrong one. A CSP with
script-src 'unsafe-inline'is misconfigured — present but decorative. - 12 checks: 11 security headers + Set-Cookie. Critical (25): CSP, HSTS, X-Content-Type-Options. High (15): X-Frame-Options, Referrer-Policy, Set-Cookie. Medium (10): Permissions-Policy, COOP, COEP, CORP, Cache-Control. Low (5): X-XSS-Protection. Total possible weight: 235. Score = earned / 235 × 100.
- ALLOW-FROM is a dead directive. Chrome, Firefox, and Safari ignore
X-Frame-Options: ALLOW-FROM. The header creates a false sense of restriction. Use CSPframe-ancestorsinstead. The tool flags ALLOW-FROM as misconfigured. - X-XSS-Protection=1 is worse than missing. The XSS Auditor introduced new vulnerabilities. Set it to
0(explicitly disable) or omit it.1; mode=blockis misconfigured — the tool flags it and recommends CSP as the modern replacement. - Set-Cookie is analyzed per-cookie. Each
Set-Cookieheader gets its own flag check (Secure, HttpOnly, SameSite). One bad cookie makes the overall result misconfigured.SameSite=NonewithoutSecureis flagged — the spec requires it, and browsers reject the cookie otherwise. - The medium headers cap your score at ~52 if missing. COOP, CORP, COEP, Permissions-Policy, and Cache-Control are 50 weight points. A site with every critical and high header perfect but all mediums missing scores D. Most sites without a dedicated security review land here.
- CSV and text report exports. RFC 4180 CSV for spreadsheets and security review docs; text report with
[PASS]/[WARN]/[FAIL]markers and numbered recommendations sorted by severity. Both generated client-side, no backend required. - Check real pasted headers at the Security Headers Checker; for the URL-based analyzer (demo mode) use Security Headers Analyzer, for generating a CSP header use CSP Header Generator, and for subresource integrity hashes use SRI Hash Generator.