The easiest security headers are winning because the easiest security headers are easy.

That sounds stupidly obvious, but it is the whole story.

If you look at real-world adoption data instead of blog posts, the gap between “one-line config” headers and “this will expose architectural debt” headers is enormous. Teams happily turn on X-Content-Type-Options. They often set X-Frame-Options. A fair number manage HSTS. Then the curve falls off a cliff when you ask for a real Content Security Policy or modern cross-origin isolation headers — anything that requires application changes instead of a server-snippet copy-paste.

The data is blunt about this.

What the Numbers Show

Large-scale measurement projects like the HTTP Archive Web Almanac keep painting the same picture. The figures below are from the Almanac’s 2024 security chapter (desktop crawl, rounded); they shift a little every year, but the shape stays:

Roughly half to a third of sites:

  • X-Content-Type-Options: nosniff (about 48%) — one line, no configuration, breaks nothing. The most widely deployed security header.
  • X-Frame-Options (about 37%) — also simple: DENY or SAMEORIGIN. Several popular frameworks, Django and Rails among them, send it by default.
  • Strict-Transport-Security (about 34%) — hosting platforms and CDNs have turned it into a checkbox. But only about a third of those policies include includeSubDomains, and fewer carry preload.

About one in five or fewer:

  • Content-Security-Policy (about 19%, up from 15% in 2022) — the most important and least deployed. Many of these policies don’t restrict scripts at all; the most common ones only set upgrade-insecure-requests or frame-ancestors. And the Almanac found unsafe-inline in the script policy of around nine in ten sites with a CSP. Do the arithmetic and a CSP that actually stops injected inline script is a small single-digit share of the web.
  • Referrer-Policy (about 17%) — growing but far from universal.

Barely there:

  • Permissions-Policy (under 3%).
  • Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy, Cross-Origin-Embedder-Policy — the cross-origin isolation trio, each between a fraction of a percent and about one and a half percent. Almost entirely limited to large platforms.

What a CSP Header Actually Looks Like

If you came here wondering what “CSP headers” are: Content-Security-Policy is one HTTP response header that tells the browser which sources of script, style, frames and other resources the page is allowed to use. Anything else gets blocked, and that’s what turns a successful script injection into a dead string. A policy that really does that job today looks like this:

Content-Security-Policy: script-src 'nonce-R4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'

The server generates a fresh random nonce for every response and puts it on each legitimate <script nonce="R4nd0m"> tag. Injected script has no nonce, so it doesn’t run. strict-dynamic lets the trusted scripts load their own dependencies. Roll it out with Content-Security-Policy-Report-Only first, which reports violations without blocking anything, and switch to the enforcing header once the reports go quiet.

The CSP Gap Is the Story

Every other header is a configuration line. CSP is an engineering project.

Setting X-Content-Type-Options: nosniff takes two seconds and breaks almost nothing. Setting a functional CSP means auditing every script on your page, replacing inline scripts with nonces or hashes, controlling every resource origin, and testing extensively. For a site with years of accumulated frontend code, third-party analytics tags, embedded widgets, and marketing scripts, that’s weeks of work.

So teams do the rational thing: they set the easy headers, pass the scanner check, and defer CSP indefinitely.

The result is predictable. Sites proudly score 8/14 on header checks. The 8 they have are the ones that take five minutes to configure. The one that would actually prevent their most likely attack — XSS via script injection — is the one they skipped.

The unsafe-inline Problem

Even counting CSP adoption generously, the functional number is worse than it appears. A CSP that includes 'unsafe-inline' for scripts permits arbitrary inline script execution — which is exactly what XSS exploits. The policy exists. It just doesn’t protect against the primary attack it was designed to stop.

(One subtlety: if a policy also lists a nonce or a hash, modern browsers ignore unsafe-inline. Many sites leave it in as a fallback for very old browsers. A policy with unsafe-inline and nothing else is the one that protects nothing.)

Why do sites use unsafe-inline? Because their application breaks without it. Inline event handlers, inline script tags in templates, third-party widgets that inject scripts — all of these fail under a strict CSP. Fixing them requires refactoring. Adding unsafe-inline requires one line.

The industry created a header so powerful that most sites can only deploy it in a mode that neutralizes its power.

HSTS: Closer But Not There

HSTS adoption looks better than CSP, partly because hosting platforms and CDNs offer it as a single switch. But HSTS quality varies wildly.

A max-age=86400 (one day) provides minimal protection. A one-day max-age means an attacker only needs the victim to not visit the site for 24 hours before the HSTS protection expires. The recommended minimum is one year (31536000 seconds), which is also what the preload list requires. The good news: the Almanac puts the median HSTS max-age at a year. The bad news is the long tail of sites below it.

HSTS preload — submitting your domain to browser preload lists so HTTPS is enforced even on the very first visit — is a fraction of HSTS deployment. It requires includeSubDomains and a long max-age, plus an explicit submission. The commitment scares many domain owners.

The Zombie Headers

X-XSS-Protection is still sent by about 23% of sites in the 2024 Almanac, more than send a CSP. Chrome removed its XSS Auditor in 2019. Edge dropped its filter the year before. Firefox never had one. The header does nothing in current mainstream browsers. Yet it persists in server configs, copied from blog posts written a decade ago and never cleaned up.

Its presence is almost a marker for “security by cargo cult” — someone pasted a header block from a 2015 tutorial and never revisited it.

What This Tells Us

Security header adoption follows a clear pattern: if it’s a one-line config change with no risk of breakage, adoption is decent. If it requires understanding your application, adoption collapses.

The headers that matter most — CSP and HSTS with preload — are exactly the ones that require the most effort. The headers that are easiest to deploy — nosniff, X-Frame-Options — provide the least marginal protection on a modern browser.

The average site is optimizing for scanner scores, not attack surface. A neat report with green checkmarks for the easy stuff and a quietly missing CSP.

If you’re looking at your own headers and everything is green except CSP, you haven’t done the hard part yet. The hard part is where the protection actually lives.