How this was measured
On 2026-10-21 we requested the homepage of each of the same 78 hand-picked domains our other surveys use — news, commerce, SaaS, developer tooling, social, streaming, reference, SEO, education, finance, cloud, local and government — and kept every response header the server sent back. Each request identified itself as a survey crawler and was attempted directly first, then through a local proxy. 60 of the 78 answered with a 200 (76.9%), 40 on the direct path and 20 through the proxy.
18 did not, and we did not guess at them. Thirteen returned 403 — nytimes.com, bloomberg.com, ebay.com, etsy.com, quora.com, stackoverflow.com, npmjs.com, vimeo.com, searchengineland.com, coinbase.com, doordash.com, yelp.com and nih.gov — while two returned 202 (amazon.com, booking.com), one 401 (reuters.com), one 406 (uber.com) and washingtonpost.com ended on a 301 we could not follow to a 200. We then retried all 18 with a current Chrome user-agent string: not one of them changed its answer. That is worth knowing on its own — the blocking here is not keyed to the user-agent, so a "bot-friendly" UA will not buy you a bigger sample from this vantage point.
Three parsing rules shaped the numbers. We read the final response of the chain, not every hop. 46 of the 60 homepages redirect at least once (median one hop, 14 arrive directly), and a header set on a 301 is not the header set a visitor ends up with. Header names are case-insensitive and duplicates are legal, so two Content-Security-Policy headers are kept as two and both apply. When we went through the proxy we discarded its 200 Connection established block, which otherwise masquerades as the site's own response.
Finally, a caution about stability that we only found because we ran the whole thing twice, five minutes apart: the cookie counts moved (123 cookies in the first run, 137 in the second) while every other number held. Everything below comes from the second run; anything derived from cookies is a snapshot, not a constant.
Finding 1: HSTS is nearly universal, and seven sites ask for less than a year
55 of the 60 homepages send Strict-Transport-Security (91.7%). The median max-age is exactly 31,536,000 — one year — because that is the number in every deployment guide, and 35 sites use it verbatim. Thirteen ask for more than a year and one much more: squareup.com at 631,152,000 seconds is twenty years. Five send no HSTS at all: cnn.com, rust-lang.org, harvard.edu, khanacademy.org and digitalocean.com.
The seven that ask for less than a year are the finding, because a short HSTS expires and the first request after it expires is a plain HTTP request again. screamingfrog.co.uk sends max-age=0; includeSubDomains — zero is the documented way to delete an HSTS policy, so this homepage is instructing browsers to forget that it ever asked for HTTPS. mit.edu asks for 600 seconds, ten minutes. Then moz.com at 30 days, shopify.com and zoom.us and w3.org at 180 days, and archive.org at 182. All seven were read back from the raw header line before being written down.
includeSubDomains appears on 34 of the 55 (61.8%), which means nearly four in ten are protecting the apex host only — a subdomain can still be reached over plain HTTP on first visit.
Finding 2: 33 sites ask to be preloaded and 8 of them do not qualify
33 of the 55 HSTS headers carry the preload token (60.0%). Preload is not a decoration: it is a request to be baked into a list shipped inside the browser, and the list has published entry conditions — a max-age of at least 31,536,000 seconds, includeSubDomains, and the preload token itself.
Two fail on the first condition. shopify.com sends max-age=15552000; includeSubDomains; preload and w3.org sends max-age=15552000; includeSubdomains; preload — 180 days with the preload flag on it, which no browser's list will accept. Six fail on the second: bbc.com, atlassian.com, trello.com, tumblr.com, searchenginejournal.com and who.int all write preload with a valid year or two of max-age and no includeSubDomains. Eight of 33 is a 24% rejection rate on a checklist with three items on it.
Nobody ships a malformed header otherwise: no site sends HSTS twice, and every max-age parsed as a number. This is a configuration gap, not a syntax problem.
Finding 3: two thirds send a CSP, and three quarters of those allow inline script
37 of 60 send an enforcing Content-Security-Policy (61.7%). Six send Content-Security-Policy-Report-Only — atlassian.com, trello.com, python.org, netflix.com, mozilla.org and squareup.com — and three of those send nothing enforcing at all, so their policy observes and never blocks. Counting either kind, 40 of 60 (66.7%) have some CSP in flight.
Then the contents. 'unsafe-inline' appears in 27 of the 37 enforcing policies (73.0%) and 'unsafe-eval' in 18 (48.6%); 17 carry both. A policy that permits inline script and eval does not stop the injection class it is usually deployed for — it is a wall with the front door left open, and it is the majority configuration here. The median policy is 1,791 characters long, and the longest, who.int at 8,765, is a CMS allowlisting roughly two hundred hostnames.
Three policies contain nothing enforceable at all. trello.com sends a Content-Security-Policy header whose value is empty — zero characters, no directives, so it restricts nothing — alongside a real report-only policy; the enforcing header is present and inert. forbes.com and searchenginejournal.com send exactly one valueless directive, upgrade-insecure-requests, which rewrites http subresources and does nothing else. Four more — target.com, w3.org, ahrefs.com, screamingfrog.co.uk, mit.edu — ship a frame-ancestors directive and nothing else, which is a clickjacking rule wearing a CSP's name.
14 of the 37 have no default-src fallback, and only 13 (35.1%) configure report-uri or report-to — two thirds of these deployments are flying blind, with no violation reports being collected anywhere. youtube.com is the only site sending the header twice: a require-trusted-types-for 'script' rule and a full nonce-based policy, and a request has to satisfy both.
Finding 4: a quarter of the sample blocks nothing against clickjacking
38 of 60 send X-Frame-Options (63.3%) — 26 SAMEORIGIN, 12 DENY. 25 send CSP frame-ancestors (41.7%). 19 send both and 16 send neither (26.7%): shopify.com, gitlab.com, zoom.us, trello.com, kubernetes.io, nodejs.org, rust-lang.org, wikipedia.org, archive.org, wikimedia.org, yoast.com, harvard.edu, khanacademy.org, paypal.com, netlify.com and cdc.gov. Any of them can be placed in a frame on someone else's page.
The 19 that send both are not wrong, but they are redundant: frame-ancestors supersedes X-Frame-Options, and where both are present a modern browser applies the CSP rule and ignores the header. X-Frame-Options only ever had two useful values and never handled the allowlist case, which is what frame-ancestors exists for — wise.com and mit.edu both use it to name the specific origins allowed to embed them.
Finding 5: HSTS is the floor, and three sites are below it
Across the twelve security headers we looked for, the median homepage sends 4.5 of them, the best sends 9, and the worst sends 0. Three sites send none of the twelve: rust-lang.org, harvard.edu and khanacademy.org. Five send exactly one, and in every case it is HSTS: gitlab.com, kubernetes.io, wikipedia.org, wikimedia.org and cdc.gov.
That ordering is consistent: HSTS is the one security header that has crossed into default practice, and everything else is optional. Worth noting for anyone auditing from HTML alone — khanacademy.org declares its content security policy in a <meta http-equiv="Content-Security-Policy"> tag, which this header-only method cannot see and which cannot express frame-ancestors in any case.
Finding 6: nosniff on two thirds, and one site sends it twice
41 of 60 send X-Content-Type-Options (68.3%), and every single one of them spells it nosniff — there are no variants, no typos, no other values in the sample. It is the most boring header here and it shows: when a header has one legal value, nobody gets it wrong.
figma.com sends it twice in the same response. That is legal, harmless and pointless, and it is the only duplicated security header in the sample apart from YouTube's two CSPs. The 19 that omit it entirely include forbes.com, walmart.com, slack.com, notion.so, python.org, go.dev, netlify.com and digitalocean.com.
Finding 7: 11 sites deliberately send more referrer than the browser default
23 of 60 send Referrer-Policy (38.3%). That is a lower number than it looks, because every current browser has defaulted to strict-origin-when-cross-origin for years — silence gets you the same behaviour as 10 of these sites ask for explicitly.
Which makes the other direction the interesting one. 11 sites (18.3%) declare something weaker than the default: eight choose no-referrer-when-downgrade (theguardian.com, bbc.com, target.com, archive.org, moz.com, stripe.com, heroku.com, who.int), which hands the full URL including path and query to any third party as long as the connection is HTTPS. Three write origin-when-cross-origin (stanford.edu, vercel.com, and github.com, which sends origin-when-cross-origin, strict-origin-when-cross-origin as a comma-separated fallback pair — a legitimate list, where a browser that does not understand the first token uses the second). Only slack.com goes the other way with no-referrer.
We had to fix our own parser to get this. Our first comparison matched the substring origin-when-cross-origin, which also matches the tail of strict-origin-when-cross-origin, and reported 21 offenders instead of 11 — it was accusing ten sites of leaking referrers when those sites had asked for exactly the modern default.
Finding 8: half the web still ships a retired header, and one retired header is gone
28 of 60 homepages (46.7%) still send X-XSS-Protection, a non-standard header from Internet Explorer 8 that enabled a heuristic reflected-XSS filter. Chrome and Edge removed their filters years ago, Firefox never implemented the header at all, and MDN now warns that the filter could itself introduce cross-site scripting bugs in otherwise safe pages. The values split into 1; mode=block on 19 sites, 0 on 8 (github.com, slack.com, x.com, youtube.com, ahrefs.com, wise.com, squareup.com, vercel.com — the value that switches the filter off, which is the one MDN recommends if you must send it at all) and one Chromium-only variant with a reporting URL on netflix.com. Roughly half the sample is shipping bytes for a browser feature that no longer exists.
Expect-CT appears zero times. Chromium deprecated it in version 107 because certificate transparency is now enforced by default, and this sample has already finished deleting it. That is the natural end state for the X-XSS-Protection group too, and the contrast between 46.7% and 0% is a fair measure of how long a deletion takes.
Finding 9: nobody is cross-origin isolated
Cross-Origin-Embedder-Policy appears on 0 of 60 homepages. Without it a page cannot be cross-origin isolated, which is the precondition for SharedArrayBuffer, high-resolution timers and the rest of that family of APIs. 8 send Cross-Origin-Opener-Policy (13.3%) — mostly same-origin and same-origin-allow-popups, with youtube.com attaching a report-to endpoint — and 4 send Cross-Origin-Resource-Policy (zoom.us and cloudflare.com at cross-origin, x.com and squareup.com at same-origin). cloudflare.com is the neat one: it sends COOP: unsafe-none, which is the default expressed explicitly, next to a deliberate CORP: cross-origin. The pattern is consistent with COOP being adopted for popup isolation while COEP, which costs real integration work, stays at zero.
Finding 10: cookies are mostly Secure, mostly not HttpOnly, and often third-party by design
37 of the 60 homepages set cookies on their final response — 137 cookies, a median of 3 each. 122 carry Secure (89.1%), which is close to solved. Only 57 carry HttpOnly (41.6%), meaning the majority are readable by any script on the page. SameSite is present on 87 of the 137: None on 60 (43.8%), Lax on 24, Strict on 3 — so the largest single group is the one that explicitly opts a cookie into cross-site sending. 43 cookies (31.4%) carry all three attributes and 12 carry none of them.
Two of those 12 are not defects: theguardian.com sets gu_client_ab_tests and gu_v2_mvt_id with max-age=0, which is a deletion instruction, and flags on a cookie being deleted do nothing. The rest are ordinary first-party identifiers — notion.so's browser id, netflix.com's flwssn, target.com's TealeafAkaSid, coursera.org's __204u — readable from JavaScript with no SameSite declared.
Finding 11: the server header gives away less than it used to
50 of 60 send Server (83.3%), but only 3 include a version number. The values are generic: cloudflare on 15, nginx on 9, Netlify on 4, envoy on 3, then AtlassianEdge, Apache, Google Frontend, ATS/9.2.15, BBC-GTM, istio-envoy and, cheerfully, github.com and snooserv. 7 send X-Powered-By (11.7%), which is where the framework leaks: Next.js, Express and Payload. Both headers are advice rather than policy, and the industry has quietly stopped putting build numbers in them.
What to ship
Send HSTS with a year or more, includeSubDomains, and only then preload. 8 of the 33 sites asking for preload here fail one of the three conditions; max-age=0 deletes your own policy.
Do not send a CSP you do not mean. One site here sends an empty enforcing policy next to a real report-only one. If you cannot deploy enforcement yet, deploy Content-Security-Policy-Report-Only with a report-uri and watch it; 65% of the enforcing policies in this sample collect no reports at all, so their owners cannot know what their policy is doing.
Plan to remove 'unsafe-inline', and say so out loud if you cannot. 73% of enforcing policies here allow it. Nonces or hashes are the way out, and until then the policy is protecting against less than the audit checkbox suggests.
Use frame-ancestors, and keep X-Frame-Options only for old clients. 16 of 60 sites here block no framing at all, and where both are sent the CSP rule wins.
Send X-Content-Type-Options: nosniff once. It has one legal value and 19 sites still omit it.
If you declare Referrer-Policy, declare something at least as strict as the browser default. 11 sites here choose weaker, and strict-origin-when-cross-origin is what silence already gives you.
Delete X-XSS-Protection. It is non-standard, no current browser honours it, and MDN documents cases where it made safe pages unsafe. Expect-CT has already reached 0% here — this one should follow.
Security headers share a delivery path with the SEO headers that matter more to a crawler: the same edge config that adds HSTS is where a X-Robots-Tag goes, and the same redirect hop that drops them drops your canonicalisation. Our indexing directive survey reads that header layer from the crawler's side, and our redirect chain survey shows how often a header is set on a hop the visitor never lands on. To assemble the document-level tags that share your <head>, the meta tag generator emits title, description, canonical, robots and icon tags from one input.
Limitations
Homepages only, 60 of them, one snapshot, from one vantage point. Everything was fetched on 2026-10-21 with a crawler identity from an exit IP in Asia. 18 of 78 domains never answered, and 14 of those are 403s from WAFs that treat our network as hostile. Because those are the same kind of sites that tend to have mature header configurations, the true coverage rates for the population we aimed at are probably higher than what is reported here — this sample under-represents the best-configured end of the web.
Final hop only. 46 of the 60 homepages redirect. A header set on the 301 that takes http:// to https://www. is never seen by a returning visitor and is not counted here; our redirect survey found HSTS on hops that this method by construction ignores. Cookies are the worst affected — most sites set them on the first hop, so 137 is a floor, not a count.
Single run, and responses move. Two runs five minutes apart gave 59 and 60 usable sites, 123 and 137 cookies. Rates held; absolute cookie counts did not. Treat every cookie number as one sample.
Headers, not behaviour. We did not evaluate a single policy against a browser. A CSP with 'unsafe-inline' may still be blocking plenty; max-age=0 may be an accident in a staging config that leaked; an empty CSP may be a CDN default nobody noticed. "Sends" is all we measured.
Twelve named headers, not all of them. A site using only Content-Security-Policy in a <meta> tag, or a WAF-specific header, counts as sending nothing. khanacademy.org is the one case we caught by hand.
Localisation reaches the cookies. walmart.com set isoLoc=CN_SN_t3 for us, which is what a Chinese exit IP gets. Regional variants can carry different cookie sets and different edges, and therefore different headers.
Reproduce it
The script and the shared domain list ship with this site. node scripts/survey-security-headers.mjs fetches all 78 domains and writes the raw header blocks to .workbuddy/tmp/security-headers-survey/raw.json; --report re-parses that file without touching the network, so a parsing fix can be applied and re-run months later, and --selftest checks the parser against a synthetic response first. --evidence=hsts, =csp, =nosniff, =frame, =referrer, =xss, =cookie, =dup and =missing print the original header line behind every verdict, always with the domain attached — that is what confirmed screamingfrog.co.uk's max-age=0, trello.com's zero-character policy, figma.com's duplicated nosniff and YouTube's two CSPs, and it is what caught our own origin-when-cross-origin substring bug before it turned 10 innocent sites into offenders. Our document metadata, resource hint, image and link rel surveys run the same domain list from the HTML side.