DEV Community

Cover image for 81% of Sites Ship Without a CSP Header. Here Is How to Catch Yours in CI
Louis Wright
Louis Wright

Posted on

81% of Sites Ship Without a CSP Header. Here Is How to Catch Yours in CI

This article was written with AI assistance and reviewed by the author.

Here is a number that should bother anyone who ships websites: in the HTTP Archive's 2024 crawl, only 19% of hosts sent a Content-Security-Policy header [1]. Flip that around and roughly four out of five sites go live with no CSP at all. HSTS fares little better: just 30% of mobile responses carried a Strict-Transport-Security header [1].

These are not exotic, cutting-edge defenses. They are one-line response headers. So why are they missing so often?

The header that quietly disappears

My theory, after watching it happen on my own projects: headers are rarely forgotten. They are lost. Someone sets them up in Nginx, then the site moves to a CDN. A framework upgrade changes the default middleware. A new preview environment gets its own config. Nobody notices, because a missing header breaks nothing. The page still loads, the tests still pass, and the only person who finds out is the one probing for weaknesses.

The data backs up how uneven this is. Among the top 1,000 sites on mobile, 64.3% enable HSTS, but adoption drops steadily as you move down the popularity ranks [1]. And even where a CSP exists, it often does less than it seems: 91% of desktop sites with a CSP allow 'unsafe-inline' in script-src [1], which undermines the XSS protection CSP is meant to provide [1][3].

Share of desktop site requests carrying each security header in 2024

What each header actually buys you

A quick tour, so the scanner output below makes sense:

  • Strict-Transport-Security tells the browser to only reach your site over HTTPS. It was standardized in RFC 6797 to defend against man-in-the-middle attacks that downgrade connections to plain HTTP [2][4].
  • Content-Security-Policy lets you restrict which sources may load scripts and other resources, reducing the impact of cross-site scripting [3][5].
  • X-Frame-Options or CSP frame-ancestors stops other sites from framing your pages, the core defense against clickjacking [6].
  • X-Content-Type-Options: nosniff tells the browser not to MIME-sniff responses, so the declared content type is respected [7].

OWASP's Secure Headers Project maintains a broader catalogue of recommended headers and values if you want to go deeper [8].

Turning it into a test

The fix for "lost" headers is the same as for any regression: test for it continuously. That is why I built header-sentinel, a small CLI that fetches your deployed URLs and fails when security headers are missing or misconfigured. Grades follow the Mozilla HTTP Observatory scoring model, where every site starts at a baseline of 100 and modifiers are applied per test [9].

Install-free:

npx header-sentinel https://example.com https://github.com --fail-on none
Enter fullscreen mode Exit fullscreen mode

Here is real output from version 1.1.1 against two sites today:

header-sentinel terminal output for example.com and github.com

https://example.com/ (HTTP 200)  Grade D score 30/100 - MEDIUM risk
  x error Strict-Transport-Security missing [hsts]
  x error Content-Security-Policy missing [csp]
  x error Clickjacking protection missing [frame-options]
  x error X-Content-Type-Options missing [content-type-options]
  ! warn  Referrer-Policy missing [referrer-policy]
  ! warn  Permissions-Policy missing [permissions-policy]
  ! warn  Cross-Origin-Opener-Policy missing [coop]

https://github.com/ (HTTP 200)  Grade A+ score 115/100 - LOW risk
  ! warn  CSP style-src allows 'unsafe-inline' [csp-unsafe-inline]
  ! warn  Permissions-Policy missing [permissions-policy]
  ! warn  Cross-Origin-Opener-Policy missing [coop]
  ! warn  Set-Cookie present (3 headers) [cookies]

2 URL(s) scanned: 4 error(s), 7 warning(s)
Enter fullscreen mode Exit fullscreen mode

Two things I like about this output. First, every finding comes with a copy-paste fix for Nginx, Express (helmet) and Vercel/Netlify, so the error message is also the patch. Second, even an A+ site gets useful nudges: GitHub's CSP allows 'unsafe-inline' for styles, and the tool says so.

Wiring it into CI

The default --fail-on error exits non-zero on any error, which is exactly what you want after a deploy step:

- name: Check security headers
  run: npx header-sentinel@1 https://staging.example.com --sarif headers.sarif
Enter fullscreen mode Exit fullscreen mode

Other options worth knowing:

  • --sitemap <url> scans every URL in your sitemap.
  • --baseline <file> reports only new findings, so you can adopt it on a legacy site without a wall of red.
  • --sarif writes SARIF 2.1.0 for GitHub code scanning.
  • --json for your own dashboards.

The takeaway

Security headers are cheap to add and easy to lose. The Web Almanac numbers say most of the web is still missing them [1]. Treat them like any other contract your deployment must honor, and let a test tell you the day one goes missing.

References

  1. Franken, G. and Vanderlinden, V. (HTTP Archive). "Security", Web Almanac 2024. 2024. https://almanac.httparchive.org/en/2024/security
  2. Hodges, J., Jackson, C. and Barth, A. (IETF). "RFC 6797: HTTP Strict Transport Security (HSTS)". 2012. https://www.rfc-editor.org/rfc/rfc6797
  3. MDN Web Docs (Mozilla). "Content Security Policy (CSP)". Accessed Oct 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
  4. MDN Web Docs (Mozilla). "Strict-Transport-Security". Accessed Oct 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security
  5. W3C. "Content Security Policy Level 3". Accessed Oct 2026. https://www.w3.org/TR/CSP3/
  6. OWASP. "Clickjacking Defense Cheat Sheet". Accessed Oct 2026. https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html
  7. MDN Web Docs (Mozilla). "X-Content-Type-Options". Accessed Oct 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options
  8. OWASP. "OWASP Secure Headers Project". Accessed Oct 2026. https://owasp.org/www-project-secure-headers/
  9. MDN Web Docs (Mozilla). "HTTP Observatory: Tests and scoring". Accessed Oct 2026. https://developer.mozilla.org/en-US/observatory/docs/tests_and_scoring

Top comments (2)

Collapse
 
cekuu35 profile image
Cenk KURTOĞLU •

Headers being lost, not forgotten, is the accurate reframing — and it generalizes past headers to every security control enforced somewhere other than the code that depends on it. RLS has the identical failure: teams enable it in a migration, then a later create table ships without it, or a view runs with the owner’s rights and sidesteps it, and nothing breaks visibly. The fix is the one you advocate for CSP: a check in CI that fails the build. For headers it is a curl asserting the response headers; for RLS it is a two-account probe that fails if account B sees any of account A’s rows. The other parallel is your 'unsafe-inline' point — coverage that looks done but does nothing is exactly an RLS table that is enabled but carries a USING (true) policy. Measure the effect, not the presence. A CI-ready probe: github.com/cekuu35/supabase-rls-le...

Collapse
 
mrlouiswright profile image
Louis Wright •

Thanks, this is a great extension of the idea. RLS is a perfect example: the control lives in a migration, the code that relies on it lives somewhere else, and nothing tells you when the two drift apart. "Measure the effect, not the presence" sums it up better than I did. A policy of USING (true) and a CSP with 'unsafe-inline' both pass a presence check and protect nothing. A two-account probe in CI is a nice, concrete way to test the real outcome.