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.
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
- Symptom:
X-XSS-Protection: 1; mode=blockstill served after you set0. A second config layer (CDN app, a stale Helmet version, or an.htaccessblock) is overriding the origin. Fix: locate the layer withcurl -sv, remove its directive, and ensure only one source sets the header. Rollback is harmless — the safe value is0, so there is nothing destructive to revert. - Symptom: clients cannot reach the site after an HPKP rotation gone wrong. A cached pin no longer matches the served chain. Fix: there is no remote remedy; affected clients must wait out the pin
max-ageor clear their pin cache. This is exactly why HPKP is removed. Never re-introducePublic-Key-Pins. Rely on Certificate Transparency instead. - Symptom: a powerful feature (camera, geolocation) is no longer restricted after migration. The
Permissions-Policyvalue is written in legacyFeature-Policysyntax and was silently dropped. Fix: convertgeolocation 'none'togeolocation=()andcamera 'self'tocamera=(self). - Symptom: scanner still reports
Expect-CTdeprecation. The header is cached at the CDN edge. Fix: remove it at origin and the edge, then purge the CDN cache; re-test against the origin IP to confirm. - Safe rollback principle. Every change here is subtractive (removing dead headers) or sets a value with no enforcement downside (
0,nosniff). The only genuinely destructive header in this family isPublic-Key-Pins, and the correct rollback is to never deploy it.
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.
Related
- Web Security Headers Fundamentals — the parent reference for all header controls.
- Removing X-Powered-By and Server headers safely — suppress version-disclosing headers across the stack.
- Content Security Policy (CSP) Essentials — the modern replacement for heuristic XSS filtering.
- Referrer-Policy & Permissions-Policy Explained —
Permissions-Policyis the successor toFeature-Policy. - Cross-Origin Frame Controls (X-Frame-Options) — why
ALLOW-FROMis dead andframe-ancestorsreplaces it.