X-XSS-Protection deprecated
Yes. Run curl -sI https://www.cloudflare.com/ | grep -i '^x-xss-protection:' and it still returns 1; mode=block, but that value blocks nothing now. Chrome removed the filter in version 78, Safari in 15.4, and Firefox never shipped one. Expect 0 or no header, and test script blocking through Content-Security-Policy instead.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
Run this in the security-header review before a release, and whenever a scanner reports X-XSS-Protection as present, missing or wrong. The failure it prevents is a reflected XSS finding closed as mitigated because the response carries 1; mode=block. Steps 5 to 7 load exactly that response in three current browsers, and the injected script runs in all of them. The reverse failure is cheaper but real: a scan rule that fails the build on a missing header, so a sprint goes to adding a header that no current engine acts on.
Prerequisites
- curl 8 or later. See the curl manual.
- The MDN reference on X-XSS-Protection and the OWASP HTTP Security Response Headers Cheat Sheet, which steps 2 and 3 are read against.
- Node 22 and puppeteer-core, installed with
npm i puppeteer-corein the working directory. - A local target that reflects
qinto the page unescaped. Save it asreflect.jsand runnode reflect.jsbefore step 4. It binds to 127.0.0.1 only.
const http = require('node:http');
const routes = {
'/none': {},
'/xxp-0': { 'x-xss-protection': '0' },
'/xxp-1': { 'x-xss-protection': '1' },
'/xxp-block': { 'x-xss-protection': '1; mode=block' },
'/csp': { 'content-security-policy': "script-src 'self'" },
'/csp-hash': { 'content-security-policy': "script-src 'self' 'sha256-KqG8JC2gsc/8am502f2BI51NnjEjvgQJvnG4ApP0yUI='" },
};
http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1:8939');
const extra = routes[url.pathname];
if (!extra) {
res.writeHead(404);
return res.end();
}
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', ...extra });
// Deliberately vulnerable: q goes into the HTML without escaping.
res.end(`<!doctype html><title>search</title><link rel="icon" href="data:,">
<p>Results for ${url.searchParams.get('q')}</p>`);
}).listen(8939, '127.0.0.1', () => console.log('reflect target on http://127.0.0.1:8939/'));
- A probe that loads every route with the same payload and reads back whether it ran. Save it as
probe.js. It takes the browser binary as its first argument.
const puppeteer = require('puppeteer-core');
const [executablePath, browser = 'chrome'] = process.argv.slice(2);
const q = encodeURIComponent('<script>window.injected = true</script>');
const routes = ['/none', '/xxp-0', '/xxp-1', '/xxp-block', '/csp', '/csp-hash'];
(async () => {
const b = await puppeteer.launch({ executablePath, browser, headless: true });
console.log(await b.version(), '|', executablePath);
const page = await b.newPage();
const messages = [];
page.on('console', (m) => messages.push(m.text()));
for (const route of routes) {
messages.length = 0;
const res = await page.goto(`http://127.0.0.1:8939${route}?q=${q}`);
const ran = await page.evaluate(() => window.injected === true);
const xxp = res.headers()['x-xss-protection'] ?? '(absent)';
console.log(`${route.padEnd(10)} x-xss-protection: ${xxp.padEnd(13)} injected script ran: ${ran}`);
for (const m of messages) console.log(` console: ${m}`);
}
await b.close();
})();
- Browser results are one capture on one Windows 11 machine. Firefox 154.0 came from the puppeteer browser cache.
Steps
- Step 1.
Read the header on a live response, with the status lines, so a challenge page is not mistaken for the real one.
curl -sI https://www.cloudflare.com/ | tr -d '\r' | grep -i -E '^HTTP/|^x-xss-protection:'HTTP/2 103 HTTP/2 200 x-xss-protection: 1; mode=block103is an Early Hints response and carries no security headers. The header belongs to the200that follows. If the second status is anything else, the header you read is not the page's. - Step 2.
Read the same header on a host that sets it the other way.
curl -sI https://api.github.com/ | tr -d '\r' | grep -i -E '^HTTP/|^x-xss-protection:'HTTP/2 200 x-xss-protection: 00switches the filter off. OWASP's recommendation reads: "Do not set this header or explicitly turn it off." - Step 3.
Count the header on a host that does not send it.
curl -sI https://example.com/ | tr -d '\r' | grep -ic '^x-xss-protection:'0Absent is the other state OWASP accepts. The Security headers checker adds no row for it, and reports a present header as
info. - Step 4.
Confirm the payload comes back as markup.
curl -si "http://127.0.0.1:8939/xxp-block?q=%3Cscript%3Ewindow.injected%20%3D%20true%3C%2Fscript%3E" | tr -d '\r' | grep -i -E '^x-xss-protection:|Results for'x-xss-protection: 1; mode=block <p>Results for <script>window.injected = true</script></p>The query string is now a live
<script>element under the blocking header. That is the reflected XSS the old filter was built to catch. - Step 5.
Run the probe in Chrome.
node probe.js "C:/Program Files/Google/Chrome/Application/chrome.exe"Chrome/152.0.7977.76 | C:/Program Files/Google/Chrome/Application/chrome.exe /none x-xss-protection: (absent) injected script ran: true /xxp-0 x-xss-protection: 0 injected script ran: true /xxp-1 x-xss-protection: 1 injected script ran: true /xxp-block x-xss-protection: 1; mode=block injected script ran: true /csp x-xss-protection: (absent) injected script ran: false console: Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-KqG8JC2gsc/8am502f2BI51NnjEjvgQJvnG4ApP0yUI='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked. /csp-hash x-xss-protection: (absent) injected script ran: trueLine 1 is
browser.version()and the binary that launched. Lines 2 to 5 differ only in the header, and the payload setwindow.injectedunder all four. The/csprow is the only block, and its console line is the violation Chrome reported. The/csp-hashrow lists the hash from that message and runs the payload again. How to check CSP header covers reading a real policy. - Step 6.
Run the same probe in Brave, a second Chromium engine.
node probe.js "C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe"Chrome/153.0.8010.37 | C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe /none x-xss-protection: (absent) injected script ran: true /xxp-0 x-xss-protection: 0 injected script ran: true /xxp-1 x-xss-protection: 1 injected script ran: true /xxp-block x-xss-protection: 1; mode=block injected script ran: true /csp x-xss-protection: (absent) injected script ran: false console: Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-KqG8JC2gsc/8am502f2BI51NnjEjvgQJvnG4ApP0yUI='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked. /csp-hash x-xss-protection: (absent) injected script ran: trueBrave reports its Chromium version through
browser.version(). The six rows match Chrome's. - Step 7.
Run the probe in Firefox, which needs
firefoxas the second argument.node probe.js "$HOME/.cache/puppeteer/firefox/win64-stable_154.0/core/firefox.exe" firefoxfirefox/154.0 | C:/Users/khark/.cache/puppeteer/firefox/win64-stable_154.0/core/firefox.exe /none x-xss-protection: (absent) injected script ran: true /xxp-0 x-xss-protection: 0 injected script ran: true /xxp-1 x-xss-protection: 1 injected script ran: true /xxp-block x-xss-protection: 1; mode=block injected script ran: true /csp x-xss-protection: (absent) injected script ran: false /csp-hash x-xss-protection: (absent) injected script ran: trueSame verdicts. The
/csprow has no console line, although the policy blocked the script.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| x-xss-protection: 1; mode=block | Asks for a filter that current Chrome, Edge, Safari and Firefox do not have | Record it as informational. Do not count it as XSS protection. |
| x-xss-protection: 1 | The same request in sanitising mode | Same as the row above. |
| x-xss-protection: 0 | The filter is switched off, as OWASP recommends when the header is sent | Accept it. |
| No x-xss-protection header | The other state OWASP accepts | Accept it. Do not raise a defect. |
| injected script ran: true under any of those values | Output is not escaped, and the header changed nothing | File the reflected XSS. The fix is output encoding, with CSP as the second layer. |
| injected script ran: false with a violation | The policy blocked the inline script | Confirm the production script-src has no 'unsafe-inline'. |
| injected script ran: true with a hash in script-src | The allowed hash matches the payload | Remove the hash and escape the output instead. |
Common mistakes
Thresholds
The version in which each engine removed the filter, from the data behind MDN's compatibility table. Firefox is recorded with version_added: false, so it never had one.
Chrome's removal entry gives the reason: "The XSS Auditor can introduce cross-site information leaks and mechanisms to bypass the Auditor are widely known."
Source: https://chromestatus.com/feature/5021976655560704What to check next
- How to check CSP header: the control that blocked the script in step 5, and how to find
'unsafe-inline'in a real policy. - How to check security headers: the full header sweep, where this row is the one that needs no fix.
- How to check X-Frame-Options: MDN ties the side-channel risk of
1; mode=blockto a page that can be framed. - How to check X-Content-Type-Options: the neighbouring
X-header in the same sweep, read the same way.
FAQ
Is the X-XSS-Protection header deprecated?
Yes. MDN marks it deprecated and non-standard, and its compatibility data lists removal in Chrome 78, Edge 17 and Safari 15.4, with no Firefox version ever. Steps 5 to 7 show the same thing from the other side: the header changed nothing in three current browsers.
What is X-XSS-Protection?
A response header that controlled a browser's built-in reflected-XSS filter. MDN describes it as "a feature of Internet Explorer, Chrome and Safari that stopped pages from loading when they detected reflected cross-site scripting (XSS) attacks." The filter is gone, so the header survives only in configuration.
What does X-XSS-Protection: 1; mode=block do?
In a browser that still had the filter, MDN says "the browser will prevent rendering of the page if an attack is detected." In Chrome 152, Brave 1.95.101 and Firefox 154 it did nothing: the page rendered and the payload ran. MDN also warns that this mode "might be vulnerable to side-channel attacks" on a framable page.
Verified
Verified by Maks Vernycurl 8.21.0Node 22.23.2puppeteer-core 25.10.0Chrome 152.0.7977.76Brave 1.95.101Firefox 154.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
- Checker: security-headers CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
- All security headers and tls checks
intermediate10 minpublished updated Maks Verny