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.

BugViso

14 min read

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

ItemDetail
Sampling frameTranco list 94GG2, ranks 1,001–50,000
Sample420 domains drawn at random with a fixed seed (the same sample as our website accessibility statistics)
Valid homepages219 after removing unreachable hosts, bot challenges and pages with under 50 words
Header sample188 homepages whose final response was HTTP 200 (403 challenge responses and errors excluded)
TLS sample215 hosts with a verified TLS handshake
Date9 October 2026, one request per homepage after following redirects
GraderBugViso's analyze_security_headers, the same code that grades headers in a scan
Vantage pointA 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

HeaderHomepagesShare of 188
Strict-Transport-Security11159.0%
Clickjacking protection (X-Frame-Options or CSP frame-ancestors)10053.2%
X-Content-Type-Options: nosniff9248.9%
X-Frame-Options9047.9%
Content-Security-Policy (enforced)6534.6%
Referrer-Policy5629.8%
X-XSS-Protection (deprecated)5629.8%
CSP frame-ancestors3619.1%
Permissions-Policy3217.0%
X-Powered-By (information leak)3116.5%
Cross-Origin-Opener-Policy115.9%
Content-Security-Policy-Report-Only73.7%
Cross-Origin-Resource-Policy52.7%
Cross-Origin-Embedder-Policy31.6%
All of HSTS + CSP + clickjacking + nosniff3820.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.

GradeScoreHomepagesShare
A90–100158.0%
B80–892010.6%
C70–79179.0%
D60–692211.7%
Fbelow 6011460.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 bandHomepagesHSTSCSPnosniffMedian scoreGrade F
1,001–10,0004132%20%37%2576%
10,001–25,0005576%40%60%5551%
25,001–50,0009261%38%48%5060%

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 patternSitesShare of 65 CSPs
No script policy: only frame-ancestors and/or upgrade-insecure-requests3147.7%
Script policy with un-neutralised 'unsafe-inline'2843.1%
Script policy with 'unsafe-eval'2843.1%
Script policy with a wildcard source (*, https: or http:)1116.9%
Uses nonces34.6%
Uses 'strict-dynamic'23.1%
Script policy with none of the weaknesses above812.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.

http
# ❌ 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-ageSitesShare of 111
0 (HSTS switched off)32.7%
Under 1 day43.6%
1 day to under 180 days54.5%
180 days to under 1 year119.9%
Exactly 1 year (31536000)6054.1%
More than 1 year2825.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.

nginx
# ✅ 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=block could itself be abused. The MDN page recommends a CSP instead. Most senders used 1; mode=block (38 sites), and only 9 sent the safer 0.
  • X-Powered-By (16.5%): this advertises the stack (PHP 9, Next.js 9, ASP.NET 3). It's no vulnerability on its own, but it costs nothing to remove. In Next.js, set poweredByHeader: false. In PHP, set expose_php = Off.
  • Versioned Server headers (8.0%): nginx/1.x or Apache/2.4.x tells scanners exactly which CVEs to try. Use server_tokens off; in Nginx and ServerTokens Prod in 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.

python
#!/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)
text
$ 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-Policy

Point 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.

Found this useful? Share it.

See where your site stands

Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.