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 ERRFORMDATADEPTHEXCEEDED, 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 ? DEFAULTFORMDATAMAXDEPTH : 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 ERRFORMDATADEPTHEXCEEDED.
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 ? DEFAULTFORMDATAMAXDEPTH : 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: ERRFORMDATADEPTHEXCEEDED // "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 ERRFORMDATADEPTHEXCEEDED. 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>
---
Other sources
Axios is a promise-based HTTP client for the browser and Node.js. From 0.28.0 until 0.34.0 and 1.15.1 until 1.20.0, ToFormData processes inherited serialization options and visitor properties supplied through prototype pollution. A separate same-process prototype-pollution flaw supplies inherited dots, indexes, metaTokens, maxDepth, visitor, or Blob values before object serialization. The inherited options alter toFormData field naming and data interpretation, maxDepth can force request failure, Blob changes value handling, and a polluted visitor can execute when an attacker already has the stronger ability to inject a function. Serialized field naming and data interpretation can change, maxDepth can cause request failure, Blob can alter value handling, and a polluted visitor can execute under the stronger function-injection primitive. This issue is fixed in versions 0.34.0 and 1.20.0.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/axiosto a version that resolves this vulnerability.Fixed in 1.20.0 - Upgrade
Upgrade
npm/axiosto a version that resolves this vulnerability.Fixed in 0.34.0 - Upgrade
Upgrade
axiosto a version that resolves this vulnerability.Fixed in 0.34.0 - Upgrade
Upgrade
axiosto a version that resolves this vulnerability.Fixed in 1.20.0 - Configuration
Pass an own formSerializer object that explicitly sets safe values for visitor, maxDepth, dots, indexes, metaTokens, and Blob.
axios form serialization formSerializer = Own object with explicit safe values for visitor, maxDepth, dots, indexes, metaTokens, and Blob - Compensating control
Avoid serializing attacker-controlled objects as form data in a process with known prototype pollution.
Event History
Frequently Asked Questions
Which Axios versions require remediation?
Affected releases are 0.28.0 through versions before 0.34.0, and 1.15.1 through versions before 1.20.0. Upgrade to 0.34.0 or 1.20.0, as appropriate for the maintained release line.
What must an attacker be able to do to exploit this issue?
An attacker needs a separate same-process prototype-pollution weakness that lets them supply inherited properties before Axios serializes an object. Execution through a polluted visitor additionally requires the stronger ability to inject a function.
How can teams assess whether an application is exposed?
Identify whether the application uses an affected Axios version and whether untrusted input can reach another prototype-pollution flaw in the same process before toFormData serialization. The relevant inherited properties include dots, indexes, metaTokens, maxDepth, visitor, and Blob.