follow-redirects array-form headers bypass cross-origin credential stripping
CVE-2026-111756 · GHSA-q3jh-wrc6-jgq8 · 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-111756?
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
- Run
npm ls follow-redirectsin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in 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.
- Upgrade
follow-redirectsto 1.16.1 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
### Summary
follow-redirects removes sensitive credential headers (Authorization, Proxy-Authorization, Cookie, and any caller-declared `sensitiveHeaders`) before following a redirect that crosses to a different host or downgrades to a less secure protocol. The removal routine iterates the caller's headers container with `for...in` and matches header names against a filter. This assumes the headers container is a plain object keyed by header names. Node's underlying `http.request`, which follow-redirects is a drop-in replacement for, also accepts headers as an alternating array of names and values. For an array, `for...in` yields the numeric indices `"0"`, `"1"`, `"2"` and so on, which never match a header-name filter, so nothing is removed. An application that sends array-form headers and follows an attacker-controlled cross-origin redirect therefore forwards its Authorization, Cookie, and Proxy-Authorization headers verbatim to the attacker-controlled destination, with no error or warning. The `sensitiveHeaders` option added to fix CVE-2026-40895 / GHSA-r4q5-vmmm-2653 is equally ineffective, because it relies on the same helper. This is an unauthenticated confidentiality vulnerability: any remote server the victim application contacts can harvest the victim's credential headers by responding with a cross-origin redirect.
### Details
The cross-origin credential-stripping control is implemented by `removeMatchingHeaders(regex, headers)` at `index.js:630-640`:
```js
function removeMatchingHeaders(regex, headers) {
var lastValue;
for (var header in headers) { // for...in over an ARRAY yields "0","1","2"...
if (regex.test(header)) { // the header-name filter never matches a numeric index
lastValue = headers[header];
delete headers[header];
}
}
return (lastValue === null || typeof lastValue === "undefined") ?
undefined : String(lastValue).trim();
}
```
It is invoked from `_processResponse` for three purposes, all with the caller-supplied `this._options.headers`:
- `index.js:490` - strip the credential filter (`this._headerFilter`, which covers the built-in list Authorization/Proxy-Authorization/Cookie plus any `sensitiveHeaders`) when the redirect crosses origin or downgrades protocol.
- `index.js:470` - strip the `Host` header (`/^host$/i`).
- `index.js:466` - strip `content-*` headers when a 301/302/303 converts POST to GET.
The caller-supplied headers container is passed straight through to each redirected request via `nativeProtocol.request(this._options)` at `index.js:352`. `_sanitizeOptions` (index.js:300-331) only replaces `options.headers` when it is falsy, so an array is left untouched.
Node's `http.request` accepts `options.headers` as an alternating array of names and values (for example `["Authorization", "Bearer ...", "Cookie", "session=..."]`). This is a supported way to control header ordering and duplication. For such an array, `for (var header in headers)` iterates the numeric indices `"0"`, `"1"`, `"2"`, and the header-name regex never matches any of them, so `removeMatchingHeaders` deletes nothing. Every one of the three strip operations above silently no-ops:
- The built-in credential headers (Authorization, Cookie, Proxy-Authorization) are forwarded to the cross-origin redirect target.
- Any `sensitiveHeaders` the caller declared are forwarded too.
- The stale `Host` of the previous origin is re-sent to the new origin.
- On a POST-to-GET redirect the `content-*` headers are retained, leaving `Content-Length` inconsistent with the now-empty body.
The primary and highest-impact consequence is disclosure of credential headers. The stale `Host` re-send and the `Content-Length` inconsistency are secondary correctness defects arising from the same root cause.
Root-cause history: the `for...in`-based `removeMatchingHeaders` used for the built-in credential strip has been present since the cross-host Authorization drop was introduced (commit `724087d`, first released in v1.11.0) and is unchanged through 1.16.0. The `sensitiveHeaders` layer was introduced in 1.16.0 (commit `844c4d3`). The behavior was verified directly on both 1.15.11 and 1.16.0.
Suggested remediation: handle or explicitly reject Node's array-form headers. The strictest fix is to detect `Array.isArray(options.headers)` in `_sanitizeOptions` and throw a clear `TypeError` instructing callers to pass an object, so misuse fails loudly instead of silently downgrading security. If array support is desired, extend `removeMatchingHeaders` to walk the array in name/value pairs:
```js
function removeMatchingHeaders(regex, headers) {
if (Array.isArray(headers)) {
var lastValue;
for (var i = 0; i < headers.length; i += 2) {
if (regex.test(headers[i])) { lastValue = headers[i + 1]; headers.splice(i, 2); i -= 2; }
}
return (lastValue === null || typeof lastValue === "undefined") ? undefined : String(lastValue).trim();
}
// existing object logic
}
```
Apply the same guard to the `Host` and `content-*` removal paths. Add regression tests that exercise array-form headers across a cross-origin redirect; the current suite only covers object-form headers.
References:
- https://github.com/follow-redirects/follow-redirects/security/advisories/GHSA-r4q5-vmmm-2653
- https://github.com/follow-redirects/follow-redirects/commit/724087d696c0bcd55c13ee0f3560c4558138e422
- https://github.com/follow-redirects/follow-redirects/commit/844c4d3 (sensitiveHeaders option)
### PoC
Environment: Node.js v24.18.0 on Windows 11. The underlying Node array-header behavior is long-standing and not version-specific. Run each script from a checkout of follow-redirects at 1.16.0 so that `require("./index.js")` resolves to the library under test.
Precondition check (Node accepts and serializes array-form headers):
```
node -e 'var http=require("http");var s=http.createServer((q,r)=>r.end(JSON.stringify(q.headers)));s.listen(0,"127.0.0.1",()=>{var p=s.address().port;htt…
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
- github.com/follow-redirects/follow-redirects/security/advisories/GHSA-q3jh-wrc6-jgq8
- github.com/follow-redirects/follow-redirects/commit/9865ca45490e4d0f7b6944870318408344570aff
- github.com/follow-redirects/follow-redirects/releases/tag/v1.16.1
- github.com/advisories/GHSA-q3jh-wrc6-jgq8
Merged from GitHub Advisory Database. Last updated 23 Oct 2026. All vulnerabilities · RSS · JSON