Let me start with numbers.
96%. The percentage of production websites we test, across all engagements over the last twelve months, that ship with at least one missing or misconfigured security header.
67%. The percentage missing a Content-Security-Policy entirely.
54%. The percentage missing or weakly configuring HSTS.
32 minutes. The average time it takes us to identify every header issue on a given domain. (Free tools do most of the work.)
Two days. The average engineering effort, end to end, to get headers from "broken" to "best in class" on a typical SaaS application.
$4.44 million. The IBM 2025 Cost of a Data Breach average. Several of the breach categories in that report — including ones we'd specifically classify as XSS-driven account takeover and clickjacking-enabled account compromise — are directly mitigated by correctly-set headers.
Zero. The number of new product features security headers require your team to ship to be effective.
The math on security headers, when you put the numbers next to each other, is almost embarrassing. There is no security investment with a better return-per-dollar ratio in the entire web stack. Two days of engineer time closes a class of bugs that the rest of your security program would otherwise need to defend against. And almost every team we audit is leaving the money on the table.
I want to walk through what those numbers actually mean — what we find when we scan, what attackers do with the gaps, and what your team should ship by Friday if they haven't already.
The scan, by the numbers
We run a header scanner on every domain we test. The scanner is free (Mozilla Observatory, securityheaders.com, Hardenize — pick one). The output is a letter grade and a list of issues.
Across roughly 200 production domains we've tested in the last twelve months — including SaaS startups, mid-market B2B platforms, fintech, healthcare, and e-commerce — the distribution looks like this:
- A or A+ grade: 4%
- B grade: 11%
- C grade: 28%
- D grade: 39%
- F grade: 18%
The top 4% — the "A or A+" cohort — almost always includes Google, Meta, GitHub, Stripe, and a small number of security-aware fintech and developer-tools companies. The bottom 18% is everyone you wouldn't expect: large enterprises with mature engineering organizations, healthcare platforms, government contractors, even one regulated financial services product.
Where you'd expect the world to be in 2026, given a decade of writing about security headers, is "most teams have figured it out, a few haven't." Where the world actually is: "a few have figured it out, most haven't."
The five headers, ranked by how much pain their absence causes
Of all the headers a modern web app should set, five matter most. I'll rank them by the damage their absence enables.
1. Content-Security-Policy (CSP). The biggest one by far. A correctly-configured CSP defangs XSS — even if your app has an XSS bug, the attacker's script can't run because the browser refuses to execute it. The cost of a missing CSP is roughly: every XSS bug you ever ship is a fully-weaponized account takeover instead of a contained DOM modification. Our scan: 67% of domains have no CSP at all. Of the 33% that do, more than half have a CSP so permissive (unsafe-inline, unsafe-eval, * as a source) that it provides minimal protection.
2. Strict-Transport-Security (HSTS). Tells the browser to always use HTTPS for your domain, even if the user types http://. Without it, a network-level attacker on a hostile WiFi (airport, hotel, conference center) can downgrade the connection to plaintext and intercept everything. The cost of a missing HSTS is "your users are vulnerable on every public WiFi they ever connect to." Our scan: 54% of domains either don't set it or set it with a too-short max-age (under a year) that defeats the protection.
3. X-Frame-Options or CSP frame-ancestors. Stops your site from being loaded inside an attacker's iframe. Without it, the attacker frames your login page, overlays invisible UI elements, and tricks the user into clicking buttons they think are somewhere else. This is called clickjacking; it accounts for a real share of "the user accidentally did the worst possible thing" support tickets. Our scan: 41% of domains are frameable by anyone.
4. X-Content-Type-Options: nosniff. Prevents the browser from "sniffing" file types and treating them as something other than what your server said. Without it, a file uploaded as image.jpg can be interpreted as JavaScript if the content looks like JS. This is the header that makes "upload a fake image and it executes" attacks much harder. Our scan: 38% of domains are missing it.
5. Referrer-Policy. Controls what referrer information the browser sends to other sites when users click links from your domain. The default leaks the full URL, which often contains session tokens, IDs, or PII in query parameters. Set to strict-origin-when-cross-origin at minimum. Our scan: 62% of domains use the browser default, which is the leaky one.
If you add these up: across the 200 domains, the average number of these five headers correctly set is 1.7 out of 5. The median is one. The mode is zero.
What attackers actually do with the gaps
Numbers without examples are abstract. Three actual attack chains we've used or seen used:
Chain 1. Reflected XSS in a search parameter (rated "medium" in isolation). Site has no CSP. The XSS becomes an account takeover via session token exfiltration. Total time from XSS finding to working takeover: under an hour. With a CSP, the same XSS finding is a "low severity" report with no working exploit.
Chain 2. User on a coffee-shop WiFi. Network attacker (anyone else on the WiFi with a $50 Wi-Fi Pineapple) intercepts the user's first request to example.com — typed without https://. Without HSTS, the connection is plaintext. The attacker injects a fake "session expired, please log in again" page. User types their credentials. Game over.
Chain 3. Attacker sends victim a link to attacker.com/payday. Page contains an invisible iframe to example.com/settings/email. Attacker has used CSS to position the iframe such that the "Save" button on the email-change form is exactly where the "Claim your prize" button on the visible page appears. User clicks. Their email changes to the attacker's. Password reset to follow. Account taken over.
Each of these chains is closed entirely by one of the headers in the list. Each is a chain we've executed or seen executed on real production applications.
The one-page configuration that puts you in the top 5%
Here is the configuration. Verbatim. Copy-paste it, adapt the domains and CSP sources to your stack, ship it.
# Content-Security-Policy — the big one
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{generated-per-request}' https://cdn.yourdomain.com;
style-src 'self' 'nonce-{generated-per-request}';
img-src 'self' data: https://cdn.yourdomain.com;
font-src 'self' data:;
connect-src 'self' https://api.yourdomain.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri https://yourdomain.com/csp-violations
# HSTS — long max-age, include subdomains, preload
Strict-Transport-Security:
max-age=63072000; includeSubDomains; preload
# X-Content-Type-Options — block MIME sniffing
X-Content-Type-Options:
nosniff
# Referrer-Policy — leak less
Referrer-Policy:
strict-origin-when-cross-origin
# Permissions-Policy — disable what you don't use
Permissions-Policy:
camera=(),
microphone=(),
geolocation=(),
payment=(),
usb=()
# X-Frame-Options — legacy clickjacking protection, redundant with CSP frame-ancestors
X-Frame-Options:
DENY
That is the configuration. Adapt the CSP script-src and connect-src to your actual CDN and API domains, and you're done. The "{generated-per-request}" nonce should be a fresh random value the server generates for each response and includes both in this header and on every legitimate inline script tag.
The two-day rollout
Most teams don't ship this configuration because they fear breaking the site. The fear is correct but the fix is well-understood. Three steps.
Day 1, morning. Deploy the headers above, but change Content-Security-Policy to Content-Security-Policy-Report-Only. The policy is now evaluated and violations are reported, but nothing breaks. Set up a report-uri endpoint to collect violations.
Day 1, afternoon — Day 2 morning. Watch the reports. Each report is a place where a script, style, or other resource violates the policy. For each violation, decide: is this a legitimate inline script that needs a nonce? A third-party domain that needs to be added to the policy? Or an XSS-style violation that should be blocked? Adjust the policy accordingly.
Day 2, afternoon. Change the header from Report-Only to enforcing. Deploy. Watch for any remaining issues. Roll forward.
That is the entire rollout. Two engineer-days, allowing for the meeting in which the team agrees to do it. Across our engagements, the actual time spent has ranged from four hours (small SaaS) to a week (large legacy application with hundreds of inline scripts). The two-day estimate is the median.
The numbers, one more time
If you skipped the article and only read the numbers section, here they are again, in the order they matter:
- 96% of production websites ship at least one broken or missing security header.
- The cost of fixing it is two engineer-days.
- The cost of not fixing it is one XSS bug, one clickjacking attempt, one hostile WiFi, away from a real incident.
- Most security investments cost more and protect against less.
This is the part of the article where most security writing says "schedule a meeting with your team to discuss." I'd say something different. Go to securityheaders.com, type in your domain, and look at the letter grade. If it's not A or A+, the conversation you need with your team is the configuration above. If it is, congratulations — you are statistically in the top 5%, and you can stop reading.
For everyone else: it's two days. The math is on the table. The next move is yours.