Skip to main content
Back to BlogSecurity Guides

How to Check Real Security Headers (and Why the Paste-From-DevTools Path Beats the URL Fetcher)

Check your website's real HTTP security headers by pasting them from DevTools or curl -I — and learn why the CORS safelist blocks URL-based fetchers, why misconfigured earns 0.4 not 0.5 in the scoring, why ALLOW-FROM is a dead directive, why SameSite=None requires Secure, and why X-XSS-Protection=1 is worse than missing.

The Toolbox TeamAugust 14, 20268 min read

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, flags script-src containing 'unsafe-inline', 'unsafe-eval', data:, or *. Flags missing frame-ancestors, base-uri, form-action. A CSP with script-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 missing includeSubDomains, flags missing preload. A 6-month HSTS without preload is 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-OptionsDENY and SAMEORIGIN pass. ALLOW-FROM <origin> is misconfigured — the directive is deprecated and unsupported by Chrome, Firefox, and Safari. The tool flags it and recommends CSP frame-ancestors instead.
  • 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-url is flagged as misconfigured because it leaks the full URL including path and query string to any origin. no-referrer-when-downgrade is flagged because it leaks paths to same-protocol destinations — the tool recommends strict-origin-when-cross-origin instead.
  • Set-Cookie — per-cookie analysis. Each Set-Cookie header is split on ;, the flags are lowercased, and the tool checks for Secure, HttpOnly, and SameSite. A cookie missing Secure is flagged ("sent over plain HTTP"). A cookie missing HttpOnly is flagged ("accessible via JavaScript — XSS risk"). A cookie missing SameSite is flagged ("vulnerable to CSRF"). SameSite=None without Secure is flagged — the spec requires Secure when SameSite=None, and modern browsers reject the cookie otherwise.

Medium (weight 10):

  • Permissions-Policy — parses feature=allowlist pairs, flags any feature with * or self * as unrestricted. camera=() passes; camera=* is misconfigured.
  • Cross-Origin-Opener-Policy (COOP) — validates against unsafe-none, same-origin-allow-popups, same-origin. unsafe-none is flagged as misconfigured (it's the default and provides no isolation).
  • Cross-Origin-Embedder-Policy (COEP) — validates against unsafe-none, require-corp, credentialless. unsafe-none is flagged.
  • Cross-Origin-Resource-Policy (CORP) — validates against same-site, same-origin, cross-origin. cross-origin is flagged (allows any origin to load the resource).
  • Cache-Control — flags public for sensitive pages, flags missing both no-store and private. A Cache-Control: public, max-age=86400 on an authenticated page is misconfigured.

Low (weight 5):

  • X-XSS-Protection — deprecated. 0 passes (explicitly disables the buggy auditor). 1 or 1; mode=block is misconfigured — the auditor introduced new vulnerabilities and can be exploited to selectively disable scripts. Missing is also flagged, but with a note that 0 is the recommended explicit value.

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 -I sends a HEAD request. Some servers respond differently to HEAD vs GET — they may omit headers like Set-Cookie that are only on GET responses. If you need the full response, use curl -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-FROM is a dead directive. Chrome, Firefox, and Safari all ignore X-Frame-Options: ALLOW-FROM <origin>. The header creates a false sense of restriction — the page is framable by anyone. Use CSP frame-ancestors <origin> instead, which browsers actually enforce.
  • X-XSS-Protection=1 is worse than missing. The XSS Auditor introduced new vulnerabilities and can be exploited to selectively disable scripts. Set it to 0 (explicitly disable) or omit it. Setting 1; mode=block is misconfigured, not present.
  • SameSite=None requires Secure. The spec mandates it, and modern browsers reject SameSite=None cookies without Secure. The tool flags this combination. If you need cross-site cookies (e.g., third-party embeds), use SameSite=None; Secure.
  • The Set-Cookie analyzer runs per-cookie. A response with 3 Set-Cookie headers 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-url Referrer-Policy leaks full URLs. It sends the complete URL (path, query string) to any origin. no-referrer-when-downgrade leaks paths to same-protocol destinations. The tool flags both as misconfigured and recommends strict-origin-when-cross-origin — full URL same-origin, origin-only cross-origin, no referrer on HTTPS→HTTP.
  • COOP/COEP unsafe-none is the default. The tool flags it as misconfigured because it provides no isolation. If you can't set same-origin (COOP) or require-corp (COEP) because of third-party embeds, use credentialless for COEP — but unsafe-none is 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 * or self * as the allowlist is unrestricted. The tool flags it. Use feature=() to deny, or feature=(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. A Set-Cookie value 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 -I or 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 CSP frame-ancestors instead. 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=block is misconfigured — the tool flags it and recommends CSP as the modern replacement.
  • Set-Cookie is analyzed per-cookie. Each Set-Cookie header gets its own flag check (Secure, HttpOnly, SameSite). One bad cookie makes the overall result misconfigured. SameSite=None without Secure is 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.