Security Headers Statistics 2026: CSP, HSTS Adoption Data
Security headers statistics from 188 live homepages: 59% send HSTS, 35% CSP, 61% grade F. Includes CSP quality, HSTS max-age, legacy TLS and a grading script.
In October 2026 we graded the HTTP security headers of 188 randomly sampled homepages. 59.0% sent Strict-Transport-Security, 34.6% sent a Content-Security-Policy, 53.2% were protected against clickjacking, and 48.9% sent X-Content-Type-Options: nosniff. Only 20.2% had all four. Graded with BugViso's header rules, 60.6% scored an F.
Adoption is only half the story. A header can be present and still protect nothing. Of the 65 sites that sent a CSP, 47.7% didn't restrict scripts at all. Their policy only set frame-ancestors or upgrade-insecure-requests. Only 8 sites (4.3% of the sample) had a script policy without 'unsafe-inline', 'unsafe-eval' or wildcard sources.
Below are the method, the full adoption table, CSP and HSTS quality, legacy headers that should be removed, the TLS picture, and a script that grades your own sites the same way.
Methodology
| Item | Detail |
|---|---|
| Sampling frame | Tranco list 94GG2, ranks 1,001–50,000 |
| Sample | 420 domains drawn at random with a fixed seed (the same sample as our website accessibility statistics) |
| Valid homepages | 219 after removing unreachable hosts, bot challenges and pages with under 50 words |
| Header sample | 188 homepages whose final response was HTTP 200 (403 challenge responses and errors excluded) |
| TLS sample | 215 hosts with a verified TLS handshake |
| Date | 9 October 2026, one request per homepage after following redirects |
| Grader | BugViso's analyze_security_headers, the same code that grades headers in a scan |
| Vantage point | A single client in Bangladesh, desktop Chrome user agent |
Two limits are worth stating. Headers were read from the homepage document only, not from API routes or static assets, so a site that sets CSP only on its app pages counts as "no CSP". And CDNs sometimes vary headers by region or bot score. One vantage point captures one view.
Adoption: Which Security Headers Sites Actually Send
| Header | Homepages | Share of 188 |
|---|---|---|
Strict-Transport-Security | 111 | 59.0% |
Clickjacking protection (X-Frame-Options or CSP frame-ancestors) | 100 | 53.2% |
X-Content-Type-Options: nosniff | 92 | 48.9% |
X-Frame-Options | 90 | 47.9% |
Content-Security-Policy (enforced) | 65 | 34.6% |
Referrer-Policy | 56 | 29.8% |
X-XSS-Protection (deprecated) | 56 | 29.8% |
CSP frame-ancestors | 36 | 19.1% |
Permissions-Policy | 32 | 17.0% |
X-Powered-By (information leak) | 31 | 16.5% |
Cross-Origin-Opener-Policy | 11 | 5.9% |
Content-Security-Policy-Report-Only | 7 | 3.7% |
Cross-Origin-Resource-Policy | 5 | 2.7% |
Cross-Origin-Embedder-Policy | 3 | 1.6% |
| All of HSTS + CSP + clickjacking + nosniff | 38 | 20.2% |
The order matches what the HTTP Archive's Web Almanac 2024 security chapter reports across millions of sites: nosniff and HSTS lead, CSP sits in the middle, and Permissions-Policy trails. Their figures cover all requests of a site rather than homepages, so the percentages aren't directly comparable.
Separately, the HTTP-to-HTTPS redirect is close to universal: 94.5% of the homepages we tested on 8 October redirected http:// to https://. Transport encryption is solved for most sites. The browser-side defenses layered on top of it are not.
Grades: How the Sample Scored
BugViso grades headers from 100 with fixed penalties. Missing CSP costs 30 (−10 for script 'unsafe-inline', −5 for 'unsafe-eval', −10 if report-only). Missing HSTS costs 25 (−10 if max-age is under a year), no clickjacking protection 15, no nosniff 10, and no Referrer-Policy or Permissions-Policy 5 each.
| Grade | Score | Homepages | Share |
|---|---|---|---|
| A | 90–100 | 15 | 8.0% |
| B | 80–89 | 20 | 10.6% |
| C | 70–79 | 17 | 9.0% |
| D | 60–69 | 22 | 11.7% |
| F | below 60 | 114 | 60.6% |
The median score was 50. 45 homepages (23.9%) sent none of the six graded headers and scored the minimum 10. Only 6 scored a full 100.
By popularity band, the pattern isn't "bigger sites do better":
| Tranco rank band | Homepages | HSTS | CSP | nosniff | Median score | Grade F |
|---|---|---|---|---|---|---|
| 1,001–10,000 | 41 | 32% | 20% | 37% | 25 | 76% |
| 10,001–25,000 | 55 | 76% | 40% | 60% | 55 | 51% |
| 25,001–50,000 | 92 | 61% | 38% | 48% | 50 | 60% |
The top band has many API, CDN and ad-tech domains whose homepage is an afterthought. The bands are small, so treat the differences as directional.
CSP Quality: Present Is Not the Same as Protective
A Content-Security-Policy exists to stop injected scripts from running. That only happens when the policy has a script-src (or a default-src fallback) that doesn't allow inline code. Here's what the 65 CSPs actually contained:
| CSP pattern | Sites | Share of 65 CSPs |
|---|---|---|
No script policy: only frame-ancestors and/or upgrade-insecure-requests | 31 | 47.7% |
Script policy with un-neutralised 'unsafe-inline' | 28 | 43.1% |
Script policy with 'unsafe-eval' | 28 | 43.1% |
Script policy with a wildcard source (*, https: or http:) | 11 | 16.9% |
| Uses nonces | 3 | 4.6% |
Uses 'strict-dynamic' | 2 | 3.1% |
| Script policy with none of the weaknesses above | 8 | 12.3% |
The first row is the biggest single group, and it's not a mistake. A CSP of frame-ancestors 'self' is a legitimate, modern replacement for X-Frame-Options. It just isn't XSS protection, and a checklist that ticks "CSP: yes" misses that.
The 'unsafe-inline' row is the classic trade-off. Tag managers, A/B testing tools and inline analytics snippets need inline script, so teams allow all inline script. CSP Level 2 lets a nonce or hash neutralise 'unsafe-inline' in browsers that support it, and only 3 sites used nonces.
# ❌ Present, but any injected inline <script> still runs
Content-Security-Policy: default-src * data: blob: 'unsafe-inline' 'unsafe-eval'
# ✅ Nonce-based script policy (the nonce is random per response)
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'The CSP guide on MDN covers nonce and hash deployment in detail. Roll out with Content-Security-Policy-Report-Only first. 7 sites in the sample were at that stage.
HSTS Quality: Max-Age and Preload
Of the 111 sites sending HSTS:
max-age | Sites | Share of 111 |
|---|---|---|
| 0 (HSTS switched off) | 3 | 2.7% |
| Under 1 day | 4 | 3.6% |
| 1 day to under 180 days | 5 | 4.5% |
| 180 days to under 1 year | 11 | 9.9% |
| Exactly 1 year (31536000) | 60 | 54.1% |
| More than 1 year | 28 | 25.2% |
The median was exactly one year. 54.1% added includeSubDomains, 33.3% added preload, and 27.9% met all the requirements of the HSTS preload list: a one-year or longer max-age, includeSubDomains and preload.
An HSTS header with max-age=0 instructs browsers to forget the policy. Three sites sent it, probably a leftover from a rollback. A max-age under a day protects almost nothing, because the policy has usually expired by the next visit. The MDN reference for Strict-Transport-Security explains the semantics.
# ✅ One year, all subdomains. Add "; preload" only once every subdomain serves HTTPS.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;Legacy Headers That Should Go
Three headers appeared often that you can remove or fix:
X-XSS-Protection(29.8%): Chrome removed its XSS Auditor in 2019, and the header does nothing in current browsers. In old browsers,1; mode=blockcould itself be abused. The MDN page recommends a CSP instead. Most senders used1; mode=block(38 sites), and only 9 sent the safer0.X-Powered-By(16.5%): this advertises the stack (PHP9,Next.js9,ASP.NET3). It's no vulnerability on its own, but it costs nothing to remove. In Next.js, setpoweredByHeader: false. In PHP, setexpose_php = Off.- Versioned
Serverheaders (8.0%):nginx/1.xorApache/2.4.xtells scanners exactly which CVEs to try. Useserver_tokens off;in Nginx andServerTokens Prodin Apache.
The TLS Layer Underneath
Headers sit on top of TLS, so we inspected the 215 certificates as well, using the same sample and date. 90.2% of handshakes negotiated TLS 1.3 and the rest TLS 1.2. However, 32.6% of servers still accepted a TLS 1.0 or 1.1 handshake when a client offered nothing newer. That's well above what the negotiated numbers suggest, and most were CDN-fronted. The breakdown, the certificate-lifetime data and a checker script are in our guide on how to check SSL certificate expiry and TLS version.
Grade Your Own Sites the Same Way
This script reproduces BugViso's header grading using only the Python standard library. Re-graded against our stored responses, it matched the engine's score on all 188 homepages.
#!/usr/bin/env python3
"""header_grade.py: grade the security headers of one or many sites (A-F).
Usage:
python3 header_grade.py https://example.com https://example.org
python3 header_grade.py --file urls.txt --csv out.csv
Standard library only. Uses the same penalties as BugViso's header grader:
missing CSP -30 (unsafe-inline -10, unsafe-eval -5, report-only -10), missing HSTS -25
(max-age under 1 year -10), no clickjacking protection -15, no nosniff -10,
no Referrer-Policy -5, no Permissions-Policy -5. A>=90 B>=80 C>=70 D>=60 F<60.
"""
import argparse, csv, re, ssl, sys, urllib.request
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36"
def fetch_headers(url):
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=20, context=ssl.create_default_context()) as r:
return r.status, {k.lower(): v for k, v in r.headers.items()}
def csp_script_flags(csp):
dirs = {}
for part in csp.split(";"):
t = part.strip().split()
if t and t[0].lower() not in dirs:
dirs[t[0].lower()] = [x.lower() for x in t[1:]]
src = dirs.get("script-src", dirs.get("default-src")) or []
nonce_or_hash = any(x.startswith(("'nonce-", "'sha256-", "'sha384-", "'sha512-")) for x in src)
fa = dirs.get("frame-ancestors")
return ("'unsafe-inline'" in src and not nonce_or_hash), ("'unsafe-eval'" in src), (fa is not None and "*" not in fa)
def grade(h):
penalty, notes = 0, []
hsts = h.get("strict-transport-security", "")
m = re.search(r"max-age\s*=\s*(\d+)", hsts, re.I)
if not hsts:
penalty += 25; notes.append("no HSTS")
elif not m or int(m.group(1)) < 31536000:
penalty += 10; notes.append("HSTS max-age < 1 year")
csp = h.get("content-security-policy") or h.get("content-security-policy-report-only")
frame_ancestors = False
if not csp:
penalty += 30; notes.append("no CSP")
else:
inline, evl, frame_ancestors = csp_script_flags(csp)
if "content-security-policy" not in h:
frame_ancestors = False # report-only never blocks framing
penalty += 10; notes.append("CSP is report-only")
if inline: penalty += 10; notes.append("script 'unsafe-inline'")
if evl: penalty += 5; notes.append("script 'unsafe-eval'")
if not (h.get("x-frame-options", "").strip().upper() in ("DENY", "SAMEORIGIN") or frame_ancestors):
penalty += 15; notes.append("no clickjacking protection")
if h.get("x-content-type-options", "").strip().lower() != "nosniff":
penalty += 10; notes.append("no nosniff")
if "referrer-policy" not in h:
penalty += 5; notes.append("no Referrer-Policy")
if "permissions-policy" not in h and "feature-policy" not in h:
penalty += 5; notes.append("no Permissions-Policy")
score = max(0, 100 - penalty)
letter = "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 70 else "D" if score >= 60 else "F"
return score, letter, notes
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("urls", nargs="*")
ap.add_argument("--file", help="text file with one URL per line")
ap.add_argument("--csv", help="write results to this CSV file")
a = ap.parse_args()
urls = a.urls + ([l.strip() for l in open(a.file) if l.strip()] if a.file else [])
rows = []
for u in urls:
u = u if u.startswith("http") else "https://" + u
try:
status, h = fetch_headers(u)
score, letter, notes = grade(h)
print(f"{letter} {score:3d} {u} {'; '.join(notes) or 'all headers present'}")
rows.append([u, status, letter, score, "; ".join(notes)])
except Exception as e:
print(f"? - {u} error: {e}")
rows.append([u, "", "", "", f"error: {e}"])
if a.csv:
with open(a.csv, "w", newline="") as f:
csv.writer(f).writerows([["url", "status", "grade", "score", "issues"]] + rows)
sys.exit(0)$ python3 header_grade.py bugviso.com https://github.com example.com
A 100 https://bugviso.com all headers present
A 95 https://github.com no Permissions-Policy
F 10 https://example.com no HSTS; no CSP; no clickjacking protection; no nosniff; no Referrer-Policy; no Permissions-PolicyPoint it at a list of client sites with --file and --csv to get a portfolio baseline in a minute.
How BugViso Grades Security Headers in Every Scan
Every BugViso audit reads the response headers of the scanned page and runs the grader described above. The Security section shows the A–F header grade and score, the parsed HSTS policy (max-age, includeSubDomains, preload and preload eligibility), the CSP with any script 'unsafe-inline' or 'unsafe-eval' flagged and nonce or hash neutralisation taken into account, and whether clickjacking protection comes from X-Frame-Options or frame-ancestors. It also flags a missing object-src or base-uri in CSPs.
The same scan inspects the TLS certificate (issuer, days left, negotiated protocol, SAN match), checks that HTTP redirects to HTTPS, and lists mixed-content resources (see how to fix mixed content errors for why those no longer show up as http:// requests). Each missing header comes with a copy-paste fix in the PDF report's remediation playbook, with a server config snippet.
You can grade your site's headers with a free BugViso scan. For deployment detail per header, read security headers explained: CSP, HSTS and X-Frame-Options. The full list of checks is on the security and privacy audit page.
What the Data Doesn't Say
- A low grade isn't a breach. Headers are defence in depth. A site with an F isn't necessarily vulnerable, but if an XSS or clickjacking bug exists, nothing in the browser limits the damage.
- Homepages aren't whole sites. Login and checkout pages often carry stricter headers than marketing homepages. Grade the pages that matter.
- Some 403s hid real headers. 24 homepages answered our plain HTTP client with a 403 (usually a bot challenge) and 7 with an error or other status. We excluded all 31 rather than grade a challenge page.
FAQ
What percentage of websites use a Content Security Policy?
In our sample of 188 homepages, 34.6% sent an enforced CSP and 3.7% a report-only one. Only 12.3% of those CSPs had a script policy without 'unsafe-inline', 'unsafe-eval' or wildcard sources, which is 4.3% of all homepages.
How many websites use HSTS?
59.0% of homepages sent Strict-Transport-Security. Most of them (79.3%) used a max-age of at least one year, and 27.9% qualified for the browser preload list.
Which security header is most commonly missing?
Permissions-Policy (missing on 83.0%) among the graded headers, then Referrer-Policy (70.2%) and CSP (65.4%). Among the high-impact ones, a missing CSP costs the most points.
Is X-Frame-Options still needed if I have CSP?
Not if your enforced CSP sets frame-ancestors. That supersedes X-Frame-Options in modern browsers. Keep X-Frame-Options: SAMEORIGIN only if you need to support very old browsers.
Can I cite this data?
Yes. Please cite "BugViso Security Headers Statistics 2026 (188 homepages, October 2026)" and link to this page. The sampling method and grader are described above, and the script reproduces the grades.
Conclusion
Most sites have solved encryption but not browser-side defence: 60.6% of homepages graded F, and half of the CSPs that exist don't restrict scripts. A BugViso scan shows which of the six headers your site is missing and gives the exact line to add.
See where your site stands
Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.