Data study with measurement22 minutes

Are Security Headers and Content Security Policy Security Theatre?

What deployed headers are specified to do, what 1,266 reachable sites actually send, and what the evidence says about attack outcomes

Websites send headers asking the browser to apply protections. Content Security Policy limits which scripts are allowed to run, HSTS forces encrypted connections. Scanners grade sites on these headers and people read the grade as a sign of security, so I wanted to know whether the grades mean anything.

I measured the headers on a pre-declared sample of 2,000 popular domains, 1,266 of which responded normally, and looked at how three scanners grade them. Only about one site in five sends a CSP that restricts scripts at all, and roughly 70% of those can be defeated by their own settings, usually by allowing any inline script or by trusting a host an attacker can reuse. Just 6% of responding sites send a script policy that passes every check. A third still send X-XSS-Protection, which controls a browser filter no current browser runs. The scanners mark down weak CSP only lightly, and none of them documents a check for reusable trusted hosts.

Two things complicate that. Whether you call most top-1,000 policies weak turns on whether you count Google and CDN hosts as reusable, which is a judgement rather than a measurement. And Google reports that a strict CSP stopped attackers exploiting most of the reported script-injection bugs on the pages it covered, which is the only evidence here about real attacks rather than configuration.

So a strict CSP is a working defence, not theatre, and X-XSS-Protection is decoration. The typical CSP is defeated by its own settings, though that measures how policies are configured rather than whether attacks succeeded. For the other headers I couldn't find a record of them stopping or failing to stop a real attack, so I wouldn't claim either way.

The full paper

The full paper is about a 22-minute read. It has the claims I checked, what I used, what I rejected and why, and dated copies of every source with hashes, so there is a record of exactly what I was working from.

Ask if you want to read it.