Skip to main content
Back to BlogSecurity Guides

How to Check Security Headers (and Why the Browser Can't Read Them Without a Backend)

Check your website's HTTP security headers — and learn why browsers can't read security headers cross-origin (CORS safelist), why HSTS preload opts into a browser-wide HTTPS list, why CSP allows 'unsafe-inline' for styles, why X-XSS-Protection is deprecated and set to 0, and why COOP, CORP, and COEP exist because of Spectre.

The Toolbox TeamAugust 14, 20268 min read

The problem: your site loads over HTTPS but a user got hijacked on a coffee-shop Wi-Fi

You deployed your site with HTTPS. You have an SSL certificate. A user visits your site on a coffee-shop Wi-Fi, and an attacker downgrades the connection to HTTP before the first response arrives — a SSL strip attack. The user's browser loads the page over HTTP, the attacker reads the session cookie, and the account is compromised. The fix isn't a better certificate — it's a header the browser has never seen: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload. This header tells the browser "always use HTTPS for this domain, never downgrade, and I've opted into your built-in list so you enforce this even on the first visit." But you didn't set it, because nobody checked your headers. The honest move is an analyzer that checks 13 security headers, scores them by severity, tells you which attacks each one prevents, and generates the server config to fix the gaps.

Fastest path

Open the Security Headers Analyzer, enter your URL, click Analyze. (Or: open your site in a browser tab, open DevTools → Network, click the first request, copy the response headers, and paste them into the Security Headers Checker — that one works on real headers because you're pasting them, not fetching cross-origin.)

URL: example.com

 Score: B (78/100)
 Present: 7  ·  Warning: 2  ·  Missing: 4

Critical headers:
   Strict-Transport-Security      missing  (critical: prevents SSL strip)
   Content-Security-Policy        present  (critical: prevents XSS)
   X-Content-Type-Options         present  (critical: prevents MIME sniffing)

High headers:
   X-Frame-Options                missing  (high: prevents clickjacking)
   Referrer-Policy                present  (high: controls referrer leakage)

 Copy the Nginx config from the Server Configs tab
 Add to nginx.conf, restart, re-analyze

The tool checked 13 headers, scored them by severity weights (critical=20, high=15, medium=10, low=5), computed the percentage, assigned a letter grade, and generated server configs for Nginx, Apache, Express.js, and Next.js. The rest of this guide is why browsers can't read security headers cross-origin, why HSTS preload matters, why CSP allows unsafe-inline for styles, why X-XSS-Protection is deprecated, and why COOP/CORP/COEP exist because of Spectre.

The substance: 13 headers, 4 severity tiers, one CORS wall

The CORS safelist and why the analyzer is in demo mode

The tool's analyzer tab says "Demo mode: simulated analysis. Production use requires a backend API." This isn't a missing feature — it's a browser security limitation. The fetch() API can only read "CORS-safelisted response headers" from cross-origin responses: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma. Security headers like Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, and Referrer-Policy are NOT on the safelist. A cross-origin fetch() to example.com cannot read these headers unless example.com explicitly sends Access-Control-Expose-Headers: Strict-Transport-Security, Content-Security-Policy, ... — which almost no site does.

This is why the tool simulates results with Math.random() — the browser genuinely cannot read security headers from another origin. The workaround is the sibling Security Headers Checker, which lets you paste raw headers from DevTools or curl -I. When you paste the headers, the tool parses them client-side — no cross-origin fetch, no CORS wall. The analyzer tool's value isn't the simulated scan; it's the 13-header reference, the scoring system, and the server config generation.

The 13 headers and what attacks they prevent

The tool checks 13 headers, each mapped to a severity and an attack:

Critical (20 points each):

  • Strict-Transport-Security (HSTS) — prevents SSL strip / man-in-the-middle downgrades. Tells the browser to always use HTTPS for max-age seconds. The preload directive opts the domain into the browser's built-in HSTS list (submitted at hstspreload.org), so HTTPS is enforced even on the first visit — before the first HSTS header is received.
  • Content-Security-Policy (CSP) — prevents XSS and data injection. The most complex header: a semicolon-separated list of directives (default-src, script-src, style-src, img-src, etc.) that control which sources the browser is allowed to load from. The tool's recommendation: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; ...
  • X-Content-Type-Options — prevents MIME type sniffing. Set to nosniff. Without it, the browser can interpret a response as a different MIME type than declared — e.g., rendering a text file as HTML, enabling XSS.

High (15 points each):

  • X-Frame-Options — prevents clickjacking. Set to DENY (never allow framing) or SAMEORIGIN. Largely superseded by CSP frame-ancestors, but still recommended because old browsers don't support CSP frame-ancestors.
  • Referrer-Policy — controls referrer information sent with requests. Set to strict-origin-when-cross-origin — sends the full URL for same-origin requests, but only the origin (not the path) for cross-origin requests, and no referrer for HTTPS→HTTP downgrades.

Medium (10 points each):

  • Permissions-Policy — controls which browser features the page can access (camera, microphone, geolocation, etc.). Set to accelerometer=(), camera=(), geolocation=(), ... to deny all features by default.
  • Cross-Origin-Opener-Policy (COOP) — isolates the browsing context. Set to same-origin. Prevents other origins from accessing window.opener — mitigates Spectre-style side-channel attacks.
  • Cross-Origin-Resource-Policy (CORP) — controls cross-origin resource loading. Set to same-origin. Prevents other origins from loading this origin's resources via <img>, <script>, etc.
  • Cross-Origin-Embedder-Policy (COEP) — requires cross-origin resources to opt-in via CORP. Set to require-corp. Together with COOP, enables cross-origin isolation, which gives access to SharedArrayBuffer and high-resolution timers.
  • Cache-Control — controls caching of sensitive pages. Set to no-store, no-cache, must-revalidate, private for authenticated pages. Without it, a shared computer could serve a cached authenticated page to the next user.

Low (5 points each):

  • X-XSS-Protection — deprecated. Set to 0 (turn it off). The old XSS Auditor in IE/Chrome was found to introduce new vulnerabilities. Modern guidance: disable it and rely on CSP.
  • X-Permitted-Cross-Domain-Policies — controls Flash/Acrobat cross-domain access. Set to none. Largely irrelevant now that Flash is dead, but harmless.
  • X-DNS-Prefetch-Control — controls DNS prefetching. Set to off. Prevents the browser from prefetching DNS for links on the page, which could leak which external domains your page references.

The weighted scoring system

The tool scores each header by severity weight:

const weights = { critical: 20, high: 15, medium: 10, low: 5 };
let totalWeight = 0, earnedWeight = 0;
simulatedHeaders.forEach(h => {
  const w = weights[h.severity];
  totalWeight += w;
  if (h.status === 'present') earnedWeight += w;
  else if (h.status === 'warning') earnedWeight += w * 0.5;
});
const score = Math.round((earnedWeight / totalWeight) * 100);

A "present" header earns full weight. A "warning" (partially configured) earns half. A "missing" header earns zero. The total possible weight is 20×3 + 15×2 + 10×6 + 5×3 = 165. The score is earned / 165 × 100, rounded. A site with all 3 critical headers present but everything else missing scores 60 / 165 × 100 = 36 — an F. The critical headers are necessary but not sufficient; a B requires most of the high and medium headers too.

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 thresholds are aggressive — a D at 50 means half the weighted headers are present. Most sites without a dedicated security review score C or D because they're missing COOP, CORP, COEP, and Permissions-Policy.

HSTS preload and the first-visit problem

HSTS has a first-visit gap: the browser only learns about HSTS when it receives the header over HTTPS. On the very first visit, if the connection is downgraded to HTTP before the header arrives, the browser never sees HSTS. The preload directive fixes this by opting the domain into a browser-compiled list (Chrome's, Firefox's, Safari's) that ships with the browser. A preloaded domain is forced to HTTPS even on the first visit — no SSL strip possible.

To preload, you submit your domain at hstspreload.org after setting Strict-Transport-Security: max-age=31536000; includeSubDomains; preload. The requirements are strict: max-age must be at least 31536000 (1 year), includeSubDomains must be present, and the domain must serve HTTPS on the apex and all subdomains. Once preloaded, removal is slow (months) — you can't just turn it off. Preload only domains you control and are confident will always be HTTPS.

CSP and the 'unsafe-inline' compromise

The tool's recommended CSP includes style-src 'self' 'unsafe-inline'. The 'unsafe-inline' allows inline styles — <style> tags, style attributes, and JavaScript-injected styles. Without it, most modern frameworks break: React injects inline styles for some components, CSS-in-JS libraries (styled-components, Emotion) inject <style> tags at runtime, and many UI libraries use style attributes for dynamic positioning.

script-src 'self' does NOT include 'unsafe-inline' — inline scripts are blocked. This is the right default: inline scripts are the primary XSS vector, and blocking them forces all JavaScript into external files with explicit <script src> tags. If you need inline scripts, use a CSP nonce: script-src 'self' 'nonce-RANDOM_VALUE' and add nonce="RANDOM_VALUE" to each <script> tag. The nonce changes per request, so an attacker can't guess it.

The compromise is that style-src 'unsafe-inline' is often necessary, but script-src 'unsafe-inline' never should be. If your CSP has script-src 'unsafe-inline', you've disabled CSP's XSS protection — the header is decorative.

COOP, CORP, COEP and the Spectre mitigation

These three headers were added to the web platform after the Spectre and Meltdown CPU vulnerabilities (2018). Spectre allows a malicious page to read memory from other origins via side-channel timing attacks — measuring how long cache reads take to infer the contents of memory the page shouldn't be able to access.

  • COOP (Cross-Origin-Opener-Policy: same-origin) — isolates the browsing context. Other origins can't access window.opener or manipulate your window. This prevents a malicious opener from accessing your context.
  • CORP (Cross-Origin-Resource-Policy: same-origin) — your resources (images, scripts, responses) can only be loaded by same-origin pages. A cross-origin page that tries to <img src="your-site.com/photo.jpg"> is blocked.
  • COEP (Cross-Origin-Embedder-Policy: require-corp) — your page only loads cross-origin resources that have opted in via CORP. This ensures every resource your page loads is explicitly allowed.

Together, COOP + COEP enable "cross-origin isolation" — a browser state where SharedArrayBuffer and high-resolution timers are available (they were restricted after Spectre). Cross-origin isolation is required for features like performance.measureUserAgentSpecificMemory() and threaded WebAssembly. The trade-off: COEP require-corp breaks third-party embeds (iframes, scripts, images) that don't send CORP headers. If you need third-party resources, use Cross-Origin-Embedder-Policy: credentialless instead, which allows cross-origin resources without CORP but strips credentials from them.

Server config generation for 4 platforms

The tool's Server Configs tab generates ready-to-use configs for Nginx, Apache, Express.js, and Next.js. The pattern is the same across all four: iterate over the 13 headers and emit the platform-specific directive.

Nginx: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; Apache: Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" Express.js: uses the helmet middleware, which sets all headers in one call. Next.js: headers() in next.config.js, returning an array of { key, value } objects.

The configs are generated from the same SECURITY_HEADERS array — there's one source of truth, and the server-specific syntax is a template. Copy the config for your platform, paste it into your server config file, restart, and re-analyze.

Gotchas

  • The analyzer is in demo mode. Browsers can't read security headers cross-origin (CORS safelist excludes them). The tool simulates results with Math.random(). For real header analysis, paste your raw headers into the Security Headers Checker, or use curl -I https://example.com and paste the output.
  • HSTS preload is hard to undo. Once your domain is on the browser preload list, removal takes months. Preload only domains you own and are confident will always be HTTPS. The requirements: max-age ≥ 31536000, includeSubDomains, and HTTPS on the apex + all subdomains.
  • CSP script-src 'unsafe-inline' disables XSS protection. If your CSP allows inline scripts, the header is decorative — an attacker who can inject HTML can inject <script>alert(1)</script>. Use nonces ('nonce-VALUE') or hashes ('sha256-...') instead.
  • X-XSS-Protection is deprecated — set it to 0, not 1. The old XSS Auditor introduced new vulnerabilities. The tool's recommendation is 0 (turn it off). Setting it to 1; mode=block is worse than not setting it at all.
  • X-Frame-Options is superseded by CSP frame-ancestors. Set both — old browsers only understand X-Frame-Options, modern browsers prefer CSP. X-Frame-Options: DENY + Content-Security-Policy: ... frame-ancestors 'none' is the belt-and-suspenders approach.
  • COEP require-corp breaks third-party embeds. If your page loads iframes, scripts, or images from third-party origins that don't send CORP headers, COEP will block them. Use credentialless instead of require-corp if you need third-party resources.
  • The scoring weights are opinionated. Critical headers are weighted 20, low headers 5. A site with all 3 criticals but no COOP/CORP/COEP scores 36 (F) — because the medium headers add up. If you disagree with the weights, adjust the weights object in the tool's source.
  • Cache-Control for static assets should be permissive, not restrictive. The tool recommends no-store, no-cache, must-revalidate, private — but that's for authenticated pages. For static assets (CSS, JS, images), use public, max-age=31536000, immutable with cache-busting filenames. Don't apply the sensitive-page Cache-Control to your static CDN.
  • The server configs don't handle CSP nonces. The generated CSP uses 'self' and 'unsafe-inline' for styles. If you need nonces for scripts, you must modify the generated config to inject per-request nonces — the static config can't do that.
  • Permissions-Policy syntax uses parentheses. camera=() means "disable camera." camera=* means "allow all origins." camera=(self "https://trusted.com") means "allow self and trusted.com." The tool's recommendation denies all features with =().

Summary

  • Browsers can't read security headers cross-origin. The CORS safelist excludes HSTS, CSP, X-Frame-Options, and other security headers. The analyzer is in demo mode (simulated results) because of this browser limitation. For real analysis, paste raw headers from DevTools or curl -I into the Security Headers Checker.
  • 13 headers across 4 severity tiers. Critical (20 pts): HSTS, CSP, X-Content-Type-Options. High (15): X-Frame-Options, Referrer-Policy. Medium (10): Permissions-Policy, COOP, CORP, COEP, Cache-Control. Low (5): X-XSS-Protection (deprecated, set to 0), X-Permitted-Cross-Domain-Policies, X-DNS-Prefetch-Control. Present = full weight, warning = half, missing = zero.
  • HSTS preload fixes the first-visit gap. The preload directive opts the domain into the browser's built-in HSTS list, forcing HTTPS even on the first visit. Submit at hstspreload.org. Requirements: max-age ≥ 1 year, includeSubDomains, HTTPS everywhere. Removal takes months — preload only domains you control.
  • CSP allows 'unsafe-inline' for styles but never for scripts. Modern frameworks need inline styles; inline scripts are the XSS vector. Use nonces ('nonce-VALUE') for scripts that must be inline. If your CSP has script-src 'unsafe-inline', the XSS protection is disabled.
  • X-XSS-Protection is deprecated — set it to 0. The old XSS Auditor introduced new vulnerabilities. Modern guidance: disable it and rely on CSP. Setting it to 1; mode=block is worse than 0.
  • COOP, CORP, COEP exist because of Spectre. These three headers enable cross-origin isolation, mitigating side-channel attacks. COEP require-corp breaks third-party embeds — use credentialless if you need them. Cross-origin isolation unlocks SharedArrayBuffer and high-resolution timers.
  • Server configs generated for 4 platforms. Nginx (add_header), Apache (Header always set), Express.js (helmet), Next.js (headers() in next.config.js). One source of truth — the 13-header array — rendered into platform-specific syntax. Copy, paste, restart.
  • Analyze at the Security Headers Analyzer; for generating a CSP header use CSP Header Generator, for subresource integrity hashes use SRI Hash Generator, and for checking real pasted headers use Security Headers Checker.