Deprecated Security Headers & Legacy Browser Support

This guide is part of the Web Security Headers Fundamentals reference. It catalogues the HTTP security headers that browsers have removed, obsoleted, or actively warn against, and maps each one to the standards-track control that replaced it. Several of these headers are not merely useless on modern engines — X-XSS-Protection introduced an exploitable side-channel, and Public-Key-Pins could permanently brick a domain. Removing them is part of the work, not just adding new headers on top.

The pages below carry the operational detail: this reference is the decision matrix for what to delete, what to keep, and what to set instead. For the specific case of suppressing version-disclosing headers like X-Powered-By and Server, see removing X-Powered-By and Server headers safely.

Deprecated header timeline and modern replacements A timeline showing four deprecated HTTP security headers retiring between 2018 and 2020, each mapped by an arrow to its modern standards-track replacement on the right. Deprecated header Modern replacement Public-Key-Pins removed 2018 X-XSS-Protection removed 2019 Expect-CT obsolete 2021 Feature-Policy renamed 2020 CT enforced by browsers Content-Security-Policy CT enforced by browsers Permissions-Policy
Each deprecated header retires into a standards-track replacement; pinning and Expect-CT collapsed into browser-enforced Certificate Transparency.

Threat Model & Protocol Mechanics

Each of these headers existed to mitigate a real attack. They were deprecated either because the mitigation proved worse than the disease, because the underlying threat was solved at a lower layer, or because the spec was folded into a successor with stricter parsing. Understanding why each was retired determines whether you should delete it, neutralize it, or leave it in place.

X-XSS-Protection — the filter that opened a side-channel

X-XSS-Protection toggled the heuristic reflected-XSS auditor built into Internet Explorer, EdgeHTML, and Blink. The auditor scanned the request for strings that also appeared in the response and selectively neutralized matching <script> blocks. The intent was defensive; the result was a cross-site information leak. Because an attacker could control which scripts the auditor disabled, the filter became a tool for selectively turning off legitimate, security-relevant inline scripts — a documented XSS-auditor bypass and a side-channel for inferring page content across origins. Chrome removed the auditor in version 78 (2019), EdgeHTML inherited the removal, and WebKit never shipped it.

The correct value today is 0, which explicitly disables any residual filtering on engines that still read the header. Do not send 1; mode=block: on the rare engine that still honors it, mode=block is itself a navigation side-channel. The real replacement for reflected-XSS defense is a strict Content Security Policy — nonce- or hash-based script-src neutralizes injection deterministically instead of heuristically.

X-XSS-Protection: 0

Public-Key-Pins (HPKP) — the header that bricked domains

HTTP Public Key Pinning let a site pin the SHA-256 hash of a public key in its certificate chain; browsers would then refuse any future connection presenting a chain that did not match a pin, for the duration of max-age. The threat it addressed — a fraudulently issued certificate from a compromised or coerced CA — was real. The failure mode was catastrophic: a pin to a key you later lost, or a botched rotation, locked every visitor out of the domain until their pin cache expired, with no recovery path. There was no way to un-pin remotely; the browser had committed. Operators bricked their own sites, and attackers found they could “RansomPin” a hijacked response to deny service.

Chrome deprecated HPKP in version 67 and removed enforcement in version 72 (2018); Firefox followed. The threat model moved down the stack to Certificate Transparency: every publicly trusted certificate must now appear in CT logs, and browsers enforce CT inclusion natively. There is no header to set. If you are still emitting Public-Key-Pins or Public-Key-Pins-Report-Only, remove it immediately — it is at best dead weight and at worst a live foot-gun if any client still caches pins.

Expect-CT — a transitional header that outlived its purpose

Expect-CT was the bridge between the pinning era and universal CT enforcement. It told browsers to require Certificate Transparency for the host and optionally report or enforce failures. It served its transitional role and is now obsolete: Chrome enforces CT for all publicly trusted certificates issued after April 2018 regardless of the header, and the Expect-CT header was deprecated in Chrome 107 (2022). Sending it adds bytes and signals a stale configuration to auditors. Remove it.

X-Content-Type-Options — NOT deprecated, still required

X-Content-Type-Options: nosniff is frequently lumped in with the legacy X- headers and wrongly stripped. It is current, standards-track, and recommended. nosniff instructs the browser to honor the declared Content-Type rather than MIME-sniffing the body, closing a class of attacks where a text/plain upload is reinterpreted as text/html or a script. It also enforces stricter checks on script and style requests. Keep it on every response.

X-Content-Type-Options: nosniff

Feature-Policy → Permissions-Policy

Feature-Policy was the original mechanism for allowlisting powerful browser features (camera, geolocation, microphone, autoplay). It was renamed and re-specified as Permissions-Policy in 2020, with a structured-field syntax (feature=(allowlist)) that differs from the old space-delimited form. Browsers still parse Feature-Policy for backward compatibility, but no new directives land there. Migrate to Permissions-Policy; emitting both is acceptable during a transition but the modern header is authoritative.

Sibling interaction: X-Frame-Options ALLOW-FROM

X-Frame-Options itself is not deprecated — DENY and SAMEORIGIN are still honored everywhere — but its ALLOW-FROM directive is. No current engine supports ALLOW-FROM; it was superseded by CSP frame-ancestors. If your framing policy needs an allowlist of permitted embedders, express it in CSP, not in a dead X-Frame-Options value. The detailed comparison lives in the cross-origin frame controls reference.

Directive Syntax & Spec

The table below is the authoritative decision matrix. “Recommended value” is what you should ship today; an em dash means there is no header to send and the control moved elsewhere.

Header Status Replacement / Successor Recommended value
X-XSS-Protection Removed from all engines CSP script-src (nonce/hash) 0
Public-Key-Pins Removed (Chrome 72, 2018) Certificate Transparency (native) — (do not send)
Public-Key-Pins-Report-Only Removed Certificate Transparency — (do not send)
Expect-CT Obsolete (Chrome 107, 2022) Certificate Transparency (native) — (do not send)
Feature-Policy Renamed (2020) Permissions-Policy migrate to Permissions-Policy
X-Frame-Options: ALLOW-FROM Directive unsupported CSP frame-ancestors use frame-ancestors
X-Content-Type-Options Current — keep nosniff
X-Frame-Options (DENY/SAMEORIGIN) Current — keep CSP frame-ancestors (additive) DENY or SAMEORIGIN

Malformed-syntax gotchas. The two most common errors when retiring these headers: (1) sending X-XSS-Protection: 1; mode=block because a copied “secure headers” snippet still recommends it — this re-arms the side-channel; (2) carrying Feature-Policy syntax into Permissions-Policy. The old header used geolocation 'none'; the new one uses geolocation=(). A Permissions-Policy value written in legacy syntax is silently dropped, leaving the feature un-restricted.

Platform-Specific Implementation

The goal at the platform layer is twofold: remove the dead headers (so they cannot be served by a stale snippet, a module default, or an upstream proxy) and set the safe replacement values. Every config below uses the platform’s explicit override mechanism and the always-equivalent flag so the directive applies to error responses too.

Nginx

# Remove dead headers that may arrive from an upstream app server.
proxy_hide_header X-Powered-By;
proxy_hide_header Public-Key-Pins;
proxy_hide_header Expect-CT;
proxy_hide_header Feature-Policy;

# Set safe, current values. `always` emits the header on 4xx/5xx error pages too.
add_header X-XSS-Protection "0" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;

Security impact: proxy_hide_header strips the named header from upstream responses before Nginx forwards them, so a framework that still injects X-Powered-By or a legacy Feature-Policy cannot leak through the proxy. add_header does not delete a header an upstream set unless that header is hidden first — hence the pairing. Verify: curl -sI https://example.com | grep -iE 'x-xss|expect-ct|public-key|feature-policy'

Apache

# Requires mod_headers (LoadModule headers_module modules/mod_headers.so).
Header always unset Public-Key-Pins
Header always unset Public-Key-Pins-Report-Only
Header always unset Expect-CT
Header always unset Feature-Policy

Header always set X-XSS-Protection "0"
Header always set X-Content-Type-Options "nosniff"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"

Security impact: Header always unset removes the header across all response codes including the internally generated error pages; the plain Header unset form skips many error responses, leaving a 500 page able to leak a pin or Expect-CT value. The always keyword is mandatory for both unset and set in a hardened config. Verify: apachectl configtest && curl -sI https://example.com | grep -i 'permissions-policy'

IIS (web.config)

<system.webServer>
  <httpProtocol>
    <customHeaders>
      <remove name="X-Powered-By" />
      <remove name="Public-Key-Pins" />
      <remove name="Expect-CT" />
      <remove name="Feature-Policy" />
      <add name="X-XSS-Protection" value="0" />
      <add name="X-Content-Type-Options" value="nosniff" />
      <add name="Permissions-Policy" value="geolocation=(), camera=(), microphone=()" />
    </customHeaders>
  </httpProtocol>
</system.webServer>

Security impact: <remove> clears any header inherited from a parent web.config or injected by an ASP.NET module before <add> applies the current value; without the <remove>, IIS appends a second header rather than replacing it, producing duplicates that browsers handle inconsistently. Verify: (Invoke-WebRequest -Uri https://example.com -Method Head).Headers

Node / Express (Helmet)

const helmet = require('helmet');

// Helmet 5+ no longer sets X-XSS-Protection by default; set it explicitly to 0.
app.use(helmet({
  xssFilter: false,                 // do NOT send "1; mode=block"
  contentSecurityPolicy: { /* your strict policy here */ },
}));
app.use(helmet.xXssProtection());   // emits "X-XSS-Protection: 0"
app.use(helmet.noSniff());          // emits "X-Content-Type-Options: nosniff"

// Helmet dropped Expect-CT in v6 and never shipped HPKP; nothing to remove there.
app.use(helmet.permittedCrossDomainPolicies()); // unrelated; kept for completeness

Security impact: modern Helmet already defaults X-XSS-Protection to 0 and removed its expectCt and hpkp middleware entirely, so an up-to-date Helmet install is already correct for these headers. The explicit calls above make the policy auditable in code review and guard against an old pinned Helmet version re-arming the filter. Verify: curl -sI http://localhost:3000 | grep -i x-xss-protection returns X-XSS-Protection: 0.

Cloudflare

In the dashboard, disable any legacy “Security Headers” app or Page Rule that injects Expect-CT. Use a Transform Rule (Modify Response Header) with a Remove action for Public-Key-Pins, Expect-CT, and Feature-Policy, and Set actions for X-XSS-Protection: 0, X-Content-Type-Options: nosniff, and your Permissions-Policy. Cloudflare’s older one-click “Expect-CT” toggle is itself deprecated; if it is enabled, turn it off. Verify: curl -sI https://example.com | grep -iE 'expect-ct|cf-ray' — the cf-ray confirms the response came through Cloudflare, and no expect-ct should appear.

Verification & Diagnostic Workflows

# 1. Confirm dead headers are absent and safe values are present.
curl -sI https://example.com | grep -iE 'x-xss-protection|public-key-pins|expect-ct|feature-policy|permissions-policy|x-content-type-options'

Expected output:

x-content-type-options: nosniff
x-xss-protection: 0
permissions-policy: geolocation=(), camera=(), microphone=()

public-key-pins, expect-ct, and feature-policy must produce no lines. If any appears, an upstream hop is re-injecting it.

# 2. Trace which hop sets a surviving header.
curl -sv https://example.com 2>&1 | grep -iE '^< (server|expect-ct|x-powered-by)'

For scanner-based confirmation, run an external grader such as the Mozilla Observatory or securityheaders.com; both flag Public-Key-Pins/Expect-CT as deprecated and warn on X-XSS-Protection: 1. In DevTools → Network, select the document request and read the Response Headers pane; a deprecated header surviving there is being added after your origin config, so check the CDN. For CI, fail the pipeline if a forbidden header is present:

# Fails (exit 1) if any deprecated header is served.
curl -sI https://staging.example.com \
  | grep -iqE 'public-key-pins|expect-ct|x-xss-protection: 1' \
  && { echo "Deprecated header present"; exit 1; } || echo "Clean"

Troubleshooting, Misconfigurations & Safe Rollback

Frequently Asked Questions

Should I send X-XSS-Protection: 1; mode=block? No. Set it to 0. The 1; mode=block value re-enables a heuristic filter that no major engine still ships and that historically introduced a cross-site information-leak side-channel. Deterministic protection comes from a strict Content Security Policy, not the auditor.

Is it safe to just delete Public-Key-Pins from my config? Yes — deleting it is the only correct action. The header is removed from all browsers, so it provides no protection, and any client that still holds a cached pin is a liability, not an asset. Certificate Transparency, enforced natively by browsers, is the modern replacement.

Is X-Content-Type-Options deprecated? No. X-Content-Type-Options: nosniff is a current, recommended control that prevents MIME-type sniffing. Keep it on every response. It is often wrongly grouped with the legacy X- headers.

Do I need to keep Feature-Policy for older browsers? No meaningful benefit. Browsers that understand feature restrictions understand Permissions-Policy. You may emit both during a brief transition, but the modern header is authoritative and the legacy one receives no new directives.

Is Expect-CT still useful? No. Browsers enforce Certificate Transparency for publicly trusted certificates without any header, and Expect-CT was deprecated in Chrome 107. Remove it to avoid signalling a stale configuration to auditors.