X-XSS-Protection: Why to Set It to 0
This guide is part of the Deprecated Security Headers & Legacy Browser Support reference. X-XSS-Protection controlled a browser feature called the XSS Auditor — a reflected-XSS heuristic that lived in WebKit and Blink. That feature is gone: Chrome removed the Auditor in version 78 (2019), Edge dropped it when it moved to Chromium, and neither Firefox nor Safari ever shipped it. The header now has exactly one correct value, 0, which disables the residual behaviour on any engine that still reads it. The only real replacement for reflected-XSS defence is a strict Content-Security-Policy, and this page shows how to set the header off cleanly while pointing your defence at CSP.
Configuration Syntax & Exact Values
The header takes one of a small set of tokens. Only 0 is correct today. Every other value either enables removed behaviour or actively introduces risk on the engines that still honour it.
X-XSS-Protection: 0
Directive breakdown of every historical value and why to avoid all but 0:
| Value | Historical meaning | Verdict |
|---|---|---|
0 |
Disable the browser XSS filter entirely | Correct. Neutralises residual Auditor behaviour; no downside. |
1 |
Enable the filter; sanitise the suspected reflection | Enables removed/risky heuristic; can corrupt legitimate markup. |
1; mode=block |
Enable the filter; block the whole page on detection | Harmful. The block action became an exploitable XS-leak oracle. |
1; report=<uri> |
Enable and POST a violation report (Chromium-only) | Depends on the removed engine; reports nothing on modern browsers. |
| (header absent) | Engine default | Fine on modern browsers, but explicit 0 documents intent and covers legacy engines defaulting to 1. |
The critical point about 1; mode=block: when the Auditor believed it detected an injected script, it rendered about:blank instead of the page. An attacker who could inject a benign, known string could make the Auditor falsely trigger on chosen substrings of the response, and by observing whether the page blocked or loaded, read out secret data one character at a time — a classic cross-site leak. mode=block turned a defensive feature into an information-disclosure primitive.
Server-Side Configuration
Emit X-XSS-Protection: 0 on every response. In each platform below the header is set unconditionally so it also lands on error pages, where injected reflections frequently occur.
Nginx
server {
listen 443 ssl;
server_name example.com;
# "always" attaches the header to 4xx/5xx responses too, not just 2xx/3xx.
# Error pages reflect input as often as normal pages, so they must be covered.
add_header X-XSS-Protection "0" always;
# The real reflected-XSS control belongs here alongside it:
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'" always;
}
See the Nginx security headers guide for the module-load and map patterns used to apply this across many server blocks.
Apache
# Requires mod_headers. "always" sets the header in the fixup phase so it is
# emitted on error responses too; without it, ErrorDocument pages omit it.
Header always set X-XSS-Protection "0"
Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'"
The Apache hardening guide covers placing this in a VirtualHost versus .htaccess and merge order with inherited configs.
Cloudflare
{
"action": "rewrite",
"expression": "http.host eq \"example.com\"",
"action_parameters": {
"headers": {
"X-XSS-Protection": { "operation": "set", "value": "0" }
}
}
}
Use a Transform Rule (Modify Response Header) with a set operation so the value is applied at the edge regardless of what origin sends. Details in the Cloudflare headers guide.
Node / Express (Helmet)
const express = require('express');
const helmet = require('helmet');
const app = express();
// Helmet sets X-XSS-Protection: 0 by default since v5 — this is intentional.
// The xXssProtection middleware ONLY ever writes 0; there is no "enable" option.
app.use(helmet.xXssProtection());
// Do the actual work in CSP:
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
objectSrc: ["'none'"],
baseUri: ["'none'"],
},
}));
Modern Helmet already sends 0 out of the box; older tutorials that show 1; mode=block are outdated — remove any such override.
Django / FastAPI
# Django: SECURE_BROWSER_XSS_FILTER was removed in Django 3.0 because it emitted
# the harmful "1; mode=block". Do NOT reintroduce it. Set 0 via middleware:
class XssProtectionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response["X-XSS-Protection"] = "0"
return response
Django deliberately dropped its SECURE_BROWSER_XSS_FILTER setting; the Django/FastAPI middleware guide shows wiring a plain 0 header and a real CSP alongside it.
Diagnostic & Verification Steps
Confirm the header value at the edge the browser actually reaches.
curl -sI https://example.com/ | grep -i x-xss-protection
Expected output:
x-xss-protection: 0
If you see 1; mode=block or 1, an outdated config or a proxy default is still injecting it — find and remove that source. Verify error pages too, since they reflect input:
curl -sI 'https://example.com/does-not-exist?q=<script>' | grep -i x-xss-protection
Expected output on a 404 response (the header must still be present because of always):
x-xss-protection: 0
In DevTools → Network, select the document request and read the Response Headers pane; X-XSS-Protection: 0 should appear. Chrome will not act on any value here — the Auditor is gone — but confirming 0 documents that no legacy engine in your user base will run the filter. There is no browser console warning for this header, so the curl checks are authoritative.
Edge Cases, Security Implications & Safe Rollback
- A framework or scanner re-adds
1; mode=block. Old Rails (X-XSS-Protection: 1; mode=blockwas a former default), pre-3.0 Django, and stale Helmet configs emit the harmful value; automated security scanners also still flag a missing header as a finding and tempt you to add1; mode=block. Fix: set0explicitly and, if a scanner complains, document that0is the OWASP-aligned modern value — the scanner rule is outdated. Never satisfy a scanner by enablingmode=block. - The header is not a substitute for CSP. Setting
0removes a broken defence; it adds none. A site that relied on the Auditor for reflected-XSS has no reflected-XSS mitigation until a CSP with a restrictivescript-src(nonce- or hash-based, no'unsafe-inline') is deployed. Fix: ship CSP inContent-Security-Policy-Report-Onlyfirst, triage reports, then enforce — treat that as the actual project, with the0header as a one-line prerequisite. - CDN or WAF injects its own value. An edge product may add
X-XSS-Protectionindependently of origin, overriding your0with1; mode=block. Fix: set the value at the hop closest to the client (the CDN Transform Rule above) so the edge value wins, and re-verify with curl against the public hostname. - Rollback. This change is safe and non-destructive:
0cannot break rendering because it only disables a feature most engines no longer have. If you must revert to prior behaviour for a compliance snapshot, restore the old directive and reload — but there is no functional scenario in which1; mode=blockis safer than0.
Conclusion
Apply X-XSS-Protection: 0 in staging first and confirm it with curl -sI on both normal and error responses. Because the value only disables a removed feature there is no enforcement risk, so the substantive rollout is the CSP that replaces it: deploy Content-Security-Policy-Report-Only with a short reporting window, triage the violation stream, then promote to an enforcing Content-Security-Policy in production. The header goes to 0 on day one; the real defence lands when CSP enforces.
Frequently Asked Questions
Why is 0 recommended instead of just omitting the header?
On modern browsers omitting it and sending 0 are equivalent, because the Auditor is gone. Setting 0 explicitly is still preferred: it documents intent, overrides any legacy engine or proxy that defaults to 1, and prevents a well-meaning teammate or scanner from reintroducing 1; mode=block.
What was actually wrong with 1; mode=block?
The block action rendered a blank page whenever the Auditor thought it saw a reflected script. Attackers could force false positives on chosen substrings and observe whether the page blocked, turning it into a cross-site leak oracle that exfiltrated secrets byte by byte. It was a defence that created a new vulnerability.
Does any current browser still run the XSS Auditor?
No. Chrome removed it in version 78 (2019), Edge dropped it in its Chromium transition, and Firefox and Safari never implemented it. The header is inert on every current engine; 0 matters only for the shrinking set of legacy browsers that still read it.
If the header does nothing now, what stops reflected XSS?
A strict Content-Security-Policy with a script-src that uses nonces or hashes and excludes 'unsafe-inline', combined with correct output encoding on the server. CSP is enforced by every modern browser and, unlike the Auditor, does not depend on fragile string-matching heuristics.
Will a security scanner mark a missing X-XSS-Protection as a failure?
Some outdated scanner rules still do, and a few even suggest 1; mode=block. Send 0 to satisfy the presence check without enabling harmful behaviour, and treat any recommendation to enable the filter as a stale rule that predates the feature’s removal.