How HTTP Works

Response header · deprecated

< X-XSS-Protection: 0

X-XSS-Protection Header

Deprecated header that enabled browser XSS filters to detect and block reflected cross-site scripting attacks.

Reviewed 6 min readbeginner3 sourcesTry itMarkdown
Direction
Response
Category
Security
Status
Deprecated: Browsers removed the XSS filter; use CSP (0 disables it)
On this page

TL;DR: X-XSS-Protection switched the XSS filters in old browsers on or off. Those filters belong to older browsers, and the header is deprecated. If you send it at all, send X-XSS-Protection: 0. Real XSS protection comes from context-aware output encoding and safe DOM APIs, with a tested Content-Security-Policy as a second layer.

What is X-XSS-Protection?

It’s a nonstandard response header that controlled reflected-XSS filters in older browsers. It’s deprecated, and seeing it on a response tells you nothing about whether a current browser is protecting the page.

How X-XSS-Protection Worked

The filters compared what was in the request with what came back in the document. If something looked like reflected script injection, the browser either stripped the suspect content or refused to render the page. It was a heuristic: it caught some attacks and missed others, with no guarantee that all attacker-controlled script was blocked.

So a filter that watched reflected input told you nothing about the app’s other rendering paths, such as stored comments or DOM-based injection.

Syntax

These are the historical values, useful when you’re reading an old config:

X-XSS-Protection: 0

0 turns the legacy filter off. 1 turned filtering on with sanitization, and 1; mode=block told the browser to block the whole page instead of sanitizing it. You pick one value; they aren’t three fields to send together.

Historical Usage

You’ll still find 1; mode=block in old configurations and copy-pasted hardening guides. Leave it out of new deployments, even if a scanner complains. If a client or compliance rule really requires it, write down which one before you treat a missing-header warning as an application vulnerability.

When reviewing an old server config, here’s what each value asked for:

ValueRequested legacy behavior
0Disable the reflected-XSS filter
1Filter suspected reflected content
1; mode=blockBlock rendering when the filter detects an attack

These only controlled the old filter. They have nothing to do with output encoding, sanitizing stored comments or configuring CSP.

Support lived in older browser filters and differed between engines. Check the compatibility data if you care about a specific legacy client. The header showing up in DevTools doesn’t mean anything is enforcing it.

Why It’s Deprecated

The filter could remove a legitimate script just because similar text appeared in the request. If later code relied on that script to set up security-relevant state, an attacker could use the filter itself to change program behavior and open a hole. MDN documents this failure mode. Turning the filter off removes that risk. Any injection bugs your app already has are still there and still need fixing.

Migration to CSP

For an app whose JavaScript lives entirely in external, same-origin files, this policy blocks inline scripts and scripts from other origins (response headers on this page are trimmed examples):

Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'none'

It will break an app that relies on inline scripts. It also isn’t complete XSS prevention: an attacker-controlled script hosted on an allowed origin still runs. For stronger policies, use generated nonces or hashes as described in the CSP specification, and never reuse a literal example nonce as a deployment secret.

If you need inline code, generate a fresh nonce for every response. In an existing Express app with Helmet:

const { randomBytes } = require('node:crypto')
const helmet = require('helmet')

app.use((_req, res, next) => {
  res.locals.cspNonce = randomBytes(32).toString('base64')
  next()
})
app.use(helmet.contentSecurityPolicy({
  useDefaults: false,
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: [(_req, res) => `'nonce-${res.locals.cspNonce}'`],
    objectSrc: ["'none'"],
    baseUri: ["'none'"]
  }
}))
app.get('/nonce-demo', (_req, res) => {
  res.type('html').send(
    `<script nonce="${res.locals.cspNonce}">console.log('nonce demo')</script>`
  )
})

Helmet supports response-local nonce functions, and Node’s randomBytes supplies the random bytes. The only dynamic value in this demo markup is the generated nonce. Keep request input out of script bodies entirely. You’ll need to extend the policy for your app’s styles, frames and external scripts. If you cache these pages, cache the nonce-bearing HTML together with its matching header.

Server Configuration

In an existing nginx server block:

add_header X-XSS-Protection "0" always;

With Apache’s mod_headers loaded:

Header always set X-XSS-Protection "0"

If upstream code also sets the field, check nginx header inheritance and Apache’s response header tables so you end up with one value.

Removing X-XSS-Protection

For a legacy browser whose filter is on by default, removing the header and sending 0 are different things: only 0 actually turns the filter off. Current Helmet documentation says helmet() sets X-XSS-Protection: 0 by default. The standalone middleware does the same:

import express from 'express';
import helmet from 'helmet';

const app = express();
app.use(helmet.xXssProtection());
app.get('/', (_req, res) => res.send('Legacy XSS filter disabled'));
app.listen(3000);

Setting Helmet’s xXssProtection option to false skips the middleware entirely, so no header is sent at all, not even 0.

Modern XSS Protection

Put untrusted text into the page through a text sink, so the browser never parses it as HTML:

const message = new URLSearchParams(location.search).get('message') ?? '';
document.querySelector('#message').textContent = message;

The page needs an element with id="message". When you deliberately render HTML, run it through a sanitizer. Encoding rules differ for HTML text, attributes, URLs and JavaScript, and OWASP’s XSS guidance explains where each applies.

For server-rendered text, Handlebars escapes ordinary expressions:

<p>{{message}}</p>

Triple braces disable HTML escaping, so keep untrusted text out of them. HTML-text escaping also won’t protect a JavaScript string or a URL attribute; those contexts need their own encoding or validation.

When a feature intentionally accepts a little HTML, a browser app with DOMPurify installed can restrict it to an allowlist:

import DOMPurify from 'dompurify'

const dirty = new URLSearchParams(location.search).get('message') ?? ''
const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['strong', 'em'],
  ALLOWED_ATTR: []
})
document.querySelector('#message').innerHTML = clean

DOMPurify documents these allowlist options. Bundle the import for the browser and keep the library updated. Use the sanitized result exactly where you sanitized it: appending more untrusted markup afterwards, or moving the result into a different template context, undoes the protection.

Complete Security Headers Setup

The legacy filter setting, CSP, framing policy and MIME-type enforcement each handle a different failure mode. Configure them together, then test your real document and resource responses. None of them fixes unsafe rendering code.

For an nginx server whose app uses only external same-origin JavaScript, these directives set each control separately:

add_header X-XSS-Protection "0" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;

The CSP blocks inline scripts and framing, nosniff makes the browser trust declared resource types, and the legacy field turns off the old filter. Merge these with any policy your app already sends, then confirm real pages still load. An unsafe innerHTML assignment stays unsafe no matter what headers you send.

Testing and Validation

curl -sS -D - -o /dev/null https://app.example.com/

You should see exactly one X-XSS-Protection: 0. That confirms the header is delivered; it says nothing about XSS resistance. For that, watch for CSP violations in the browser and test your untrusted-input rendering paths directly.

On a served test page, log CSP violations while you exercise its scripts:

document.addEventListener('securitypolicyviolation', (event) => {
  console.log(event.effectiveDirective, event.blockedURI)
})

The violation event reports policy failures, which is only a subset of XSS bugs. Feed untrusted text containing <, > and quotes into each place the app renders it. A header scan can’t tell you whether that rendering escapes properly or goes through an unsafe sink.

Getting X-XSS-Protection right

Remove any upstream 1; mode=block before adding 0, so the response carries one deliberate value instead of two conflicting ones. If your CSP is breaking something, fix the resource it blocks or the injection bug behind it, rather than re-enabling a legacy filter as a workaround.

Common Misconceptions

CSP backs up output encoding; it doesn’t replace it. HttpOnly keeps JavaScript from reading a cookie, but injected script can still make authenticated same-origin requests that send it. Neither that cookie attribute nor this deprecated header makes an unsafe HTML sink safe.

Frequently asked questions

What is X-XSS-Protection?

X-XSS-Protection controlled legacy reflected-XSS filters. It is deprecated and nonstandard; do not depend on it for current browser security.

Should I still use X-XSS-Protection?

Use X-XSS-Protection: 0 to disable legacy filters when needed. Prevent injection with context-aware encoding and safe DOM operations, with CSP as an additional defense.

Why was X-XSS-Protection deprecated?

Filter heuristics could remove legitimate code and introduce vulnerabilities. The header is deprecated; browser support varied and is unsuitable as a modern security boundary.

What should I use instead?

Use context-aware output encoding, safe sinks such as textContent, and sanitization where HTML is required. Add a tested CSP; CSP alone does not provide complete XSS protection.

Sources

  1. MDN Web Docs: X-XSS-Protectiondeveloper.mozilla.org
  2. OWASP XSS Preventioncheatsheetseries.owasp.org
  3. Helmet: X-XSS-Protection defaultshelmet.js.org

Keep going

Browse /search