high · 8.2 Patched

follow-redirects sensitiveHeaders regex escaping can expose credential headers

CVE-2026-111755 · GHSA-8r9p-f939-6h3c · published 20 Oct 2026

How serious is it

  • Severity: high, CVSS score 8.2 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-111755?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • follow-redirects (npm) >= 1.16.0, < 1.16.1

How to check

  1. Run npm ls follow-redirects in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade follow-redirects to 1.16.1 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

### 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`: ```js this._headerFilter = new RegExp("^(?:" + sensitiveHeaders.concat(options.sensitiveHeaders).map(escapeRegex).join("|") + ")$", "i"); ``` `escapeRegex` at `index.js:703-705`: ```js 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: ```js function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } ``` This was verified to restore correct matching for all affected names while preserving the built-in matches. 2. 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: - https://github.com/follow-redirects/follow-redirects/security/advisories/GHSA-r4q5-vmmm-2653 - https://github.com/follow-redirects/follow-redirects/pull/284 - https://github.com/follow-redirects/follow-redirects/commit/844c4d302ac963d29bdb5dc1754ec7df3d70d7f9 ### PoC Environment: Node.js v24.18.0 on Windows 11. Run each script …

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database. Last updated 23 Oct 2026. All vulnerabilities · RSS · JSON