---
id: "CVE-2026-101909"
page: "https://infrastack.ai/live/CVE-2026-101909"
kind: "vulnerability"
aliases: ["CVE-2026-101909", "GHSA-x97p-jq2g-jp4f"]
title: "Axios: Prototype Pollution Gadget in axios toFormData Options"
severity: "high"
cvss: 8.3
epss: 0.00497
exploited: false
status: "open"
category: "package"
products: [{"name": "axios", "note": ">=0.28.0", "fixed": [">=0.34.0"], "affected": ">= 0.28.0", "ecosystem": "npm"}, {"name": "axios", "note": ">=1.15.1", "fixed": [">=1.20.0"], "affected": ">= 1.15.1", "ecosystem": "npm"}, {"name": "axios", "fixed": ["0.34.0"], "affected": ">= 0.28.0, < 0.34.0", "ecosystem": "npm"}, {"name": "axios", "fixed": ["1.20.0"], "affected": ">= 1.15.1, < 1.20.0", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["axios", "cwe-1321", "multipart", "node.js", "npm", "package", "prototype-pollution"]
not_affected_if: ["json-only", "no-form-serialization", "no-prototype-pollution"]
published_at: "2026-09-16T08:59:17+00:00"
first_seen_at: "2026-11-09T22:53:18+00:00"
updated_at: "2026-11-09T22:59:05+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/axios/axios/security/advisories/GHSA-x97p-jq2g-jp4f", "first_seen_at": "2026-11-09T22:53:18+00:00"}, {"source": "ghsa", "url": "https://github.com/advisories/GHSA-x97p-jq2g-jp4f", "first_seen_at": "2026-11-09T22:57:50+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-101909", "first_seen_at": "2026-11-09T23:10:12+00:00"}]
exploit_sources: [{"type": "epss", "score": 0.00497, "url": "https://api.first.org/data/v1/epss?cve=CVE-2026-101909"}]
---

# Axios: Prototype Pollution Gadget in axios toFormData Options

Link for people: https://infrastack.ai/live/CVE-2026-101909 (this .md is for agents)

Axios reads several form serializer options without own-property checks, so a prototype pollution elsewhere in the same process can change how multipart and URL-encoded request bodies are built. An attacker who already controls Object.prototype can read submitted form values through a polluted visitor function or cause nested form submissions to fail with ERR_FORM_DATA_DEPTH_EXCEEDED.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:axios affected >= 0.28.0 fixed >=0.34.0
- npm:axios affected >= 1.15.1 fixed >=1.20.0
- npm:axios affected >= 0.28.0, < 0.34.0 fixed 0.34.0
- npm:axios affected >= 1.15.1, < 1.20.0 fixed 1.20.0

## How to check

- Check the installed axios version with `npm ls axios`, and look for axios `>= 0.28.0, < 0.34.0` or `>= 1.15.1, < 1.20.0` in the lockfile.
- Find whether the app calls `axios.toFormData()` or posts plain objects with `Content-Type: multipart/form-data` or URL-encoded bodies.
- Check whether the process has any prototype pollution source, such as a compromised transitive dependency.

## Fix

- `axios` `0.34.0` for the 0.x line
- `axios` `1.20.0` for the 1.x line

## Timeline

- 2026-11-09T22:53:18+00:00 ghsa-repo https://github.com/axios/axios/security/advisories/GHSA-x97p-jq2g-jp4f
- 2026-11-09T22:57:50+00:00 ghsa https://github.com/advisories/GHSA-x97p-jq2g-jp4f
- 2026-11-09T23:10:12+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-101909

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/axios/axios/security/advisories/GHSA-x97p-jq2g-jp4f
- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/axios/axios/security-advisories/GHSA-x97p-jq2g-jp4f
- [fix] Patch: github.com https://github.com/axios/axios/pull/11141
- [fix] Patch: github.com https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a
- [fix] Patch: github.com https://github.com/axios/axios/commit/d29be181f85f6fe93397a07b1f69606d9622637b
- [fix] Release: github.com https://github.com/axios/axios/releases/tag/v0.34.0
- [fix] Release: github.com https://github.com/axios/axios/releases/tag/v1.20.0

## From the sources

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

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
