---
id: GHSA-42h9-826w-cgv3
title: 'Axios: Excessive recursion in formDataToJSON can cause denial of service'
summary: 'Axios: Excessive recursion in formDataToJSON can cause denial of service'
severity: medium
cwe:
  - CWE-400
  - CWE-674
vendor: axios
product: axios
ecosystem: npm
affected:
  - 'axios >= 0.28.0, < 0.33.0'
  - 'axios >= 1.0.0, < 1.18.0'
patched:
  - axios 0.33.0
  - axios 1.18.0
published: '2026-07-20'
updated: '2026-07-20'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-42h9-826w-cgv3'
references:
  - url: 'https://github.com/axios/axios/security/advisories/GHSA-42h9-826w-cgv3'
  - url: 'https://github.com/axios/axios/pull/11000'
  - url: 'https://github.com/axios/axios/pull/11001'
  - url: >-
      https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d
  - url: >-
      https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2
  - url: 'https://github.com/axios/axios/releases/tag/v0.33.0'
  - url: 'https://github.com/axios/axios/releases/tag/v1.18.0'
  - url: 'https://github.com/advisories/GHSA-42h9-826w-cgv3'
tags:
  - ghsa
  - npm
ingestedAt: '2026-07-20T18:42:16.756Z'
---

## Overview

## 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

## Affected packages

- `axios >= 0.28.0, < 0.33.0`
- `axios >= 1.0.0, < 1.18.0`

## Remediation

Upgrade to a patched release:

- `axios 0.33.0`
- `axios 1.18.0`
