X-XSS-Protection header: deprecated, what to set (tested)
X-XSS-Protection turned on a filter in old browsers that tried to spot reflected XSS and block it. Current browsers no longer have that filter, so the header does nothing in them. We checked this by serving a page that echoes a query parameter into the HTML and sending every common value of the header. Short answer: send X-XSS-Protection: 0 or leave the header out, and use Content-Security-Policy to stop injected scripts.
1. The test: does the header stop a reflected script?
The test page put the q query parameter straight into the HTML with no escaping. That is a textbook reflected XSS bug. We loaded it with this payload in the URL:
?q=<script>window.__xss=1;document.title="PWNED"</script>
Then we checked whether window.__xss was set and whether the title changed. Each row is a different response header:
| Response header | Chromium 153 | Firefox 156 |
|---|---|---|
| no X-XSS-Protection | script ran | script ran |
X-XSS-Protection: 0 | script ran | script ran |
X-XSS-Protection: 1 | script ran | script ran |
X-XSS-Protection: 1; mode=block | script ran | script ran |
X-XSS-Protection: 1; report=/r | script ran, no report sent | not tested |
Content-Security-Policy: script-src 'self' | blocked | blocked |
In Chromium the page rendered normally in every X-XSS-Protection case. mode=block did not blank the page. Chromium logged nothing about the header: no console message and no DevTools issue. The report= URL never received a request. The browser ignores the header completely.
With the CSP header, Chromium blocked the script and logged:
Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.
2. Why the header stopped working (from release notes, not measured)
Chrome removed its XSS Auditor in Chrome 78 (2019). Edge dropped its XSS filter in the move to Chromium, and Firefox never had one. Only old browsers such as Internet Explorer and pre-78 Chrome or Safari ever acted on the header. The filters were dropped partly because they missed many attacks and partly because attackers could abuse the blocking itself to break or probe pages. That second problem is why the current advice is 0 rather than 1; mode=block.
3. What popular libraries send today
| Library | X-XSS-Protection by default |
|---|---|
helmet 8.3.0 (Express 5.2.1), app.use(helmet()) | 0 |
helmet 8.3.0 with helmet({ xXssProtection: false }) | not sent |
Django 6.1.1 SecurityMiddleware | not sent |
Django 6.1.1 with SECURE_BROWSER_XSS_FILTER = True | still not sent, and no error |
helmet sends 0 on purpose, to switch the filter off in any old browser that still has it. In the same response it also sent a Content-Security-Policy with script-src 'self', which is the part that actually blocks injected scripts.
Django 6.1.1 has no X-XSS-Protection code at all. SECURE_BROWSER_XSS_FILTER is gone from its default settings, and setting it to True is silently ignored. If an old Django settings file still has it, delete the line. It does nothing.
4. What to set
Either leave the header out or send:
X-XSS-Protection: 0
Both behave the same in current browsers. 0 is the safer choice if you still have users on old browsers. Do not send 1; mode=block. It protected nothing in either current browser we tested.
Server config (not run here):
# nginx
add_header X-XSS-Protection "0" always;
# Apache (mod_headers)
Header always set X-XSS-Protection "0"
If a proxy or older config already adds 1; mode=block, remove that line rather than adding a second header.
5. What actually stops XSS
- Escape output. The real bug in our test page was echoing
qwithout HTML escaping. Escape every value you put into HTML, or use a template engine that does it by default. - Content-Security-Policy. A policy without
'unsafe-inline'inscript-srcblocked the injected script in both browsers. See Content-Security-Policy header examples, and roll it out safely with CSP Report-Only and Reporting-Endpoints. - The other headers that still matter: X-Content-Type-Options: nosniff and X-Frame-Options DENY vs SAMEORIGIN (or CSP
frame-ancestors).
Frequently asked questions
Is X-XSS-Protection deprecated?
Yes. No current major browser acts on it. In our test, Chromium 153 and Firefox 156 ran an injected script with X-XSS-Protection set to 0, 1, 1; mode=block, or not sent at all.
Should I set X-XSS-Protection to 0 or 1; mode=block?
Set 0 or leave it out. 1; mode=block did nothing in Chromium 153 or Firefox 156, and according to browser vendors the old filters it turned on could be abused. helmet 8.3.0 sends 0 by default.
Does Chrome still have the XSS Auditor?
No. According to Chrome's release notes it was removed in Chrome 78. In Chromium 153 the header made no difference and logged no console message.
What replaced X-XSS-Protection?
Content-Security-Policy. A script-src 'self' policy blocked the same injected inline script in both Chromium 153 and Firefox 156. Escaping output in your templates fixes the underlying bug.
Does Django still set X-XSS-Protection?
No. Django 6.1.1's SecurityMiddleware sent X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Cross-Origin-Opener-Policy, but no X-XSS-Protection, even with SECURE_BROWSER_XSS_FILTER = True in settings.
Written with AI assistance. Measured on 2026-10-02 with headless Chromium 153.0.8010.12 (Playwright 1.63.0) and headless Firefox 156.0.1 (geckodriver), loading a deliberately vulnerable page from a Node.js 24.21.0 server (Chromium test) and a Python 3.13 http.server (Firefox test) on localhost:8985. Defaults measured with helmet 8.3.0 on Express 5.2.1 and with Django 6.1.1 (SecurityMiddleware, test client). Browser history is quoted from release notes, not measured. The nginx and Apache lines were not run here. PlainHeaders is our extension; Chromium, Firefox, helmet and Django are free.