Summary
Axios: Prototype Pollution Gadget in axios toFormData Options
Advisory 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().transformRequestpaths that serialize plain objects tomultipart/form-data.- URL-encoded form serialization paths that rely on the same helper.
formSerializeroption defaults when the relevant properties are absent as own properties.
Not affected:
- JSON request bodies.
- Requests that do not invoke
toFormData(). - Processes where
Object.prototypeis 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:
{
metaTokens: true,
dots: false,
indexes: false
}
That default object has Object.prototype in its prototype chain. toFormData() then reads behavior-affecting values directly:
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:
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.
Original report
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:
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():
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():
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:
// 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:
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
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:
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.d
References
- https://github.com/advisories/GHSA-x97p-jq2g-jp4f
- https://github.com/axios/axios/security/advisories/GHSA-x97p-jq2g-jp4f
- https://nvd.nist.gov/vuln/detail/CVE-2026-101909
- https://github.com/axios/axios/pull/11141
- https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a
- https://github.com/axios/axios/commit/d29be181f85f6fe93397a07b1f69606d9622637b
- https://github.com/axios/axios/releases/tag/v0.34.0
- https://github.com/axios/axios/releases/tag/v1.20.0
Related vulnerabilities
All Supply chain →- MEDIUMCVE-2026-107301
msgpack5: Partial options disable prototype protection
- HIGHCVE-2026-105858
Payload: Remote Code Execution through first-register
- CRITICALCVE-2026-105857
Payload Form Builder has an RCE issue
- CRITICALCVE-2026-105844
Payload: Prototype pollution in Payload Import Export plugin
- HIGHCVE-2026-104852
GraphQL Tools has prototype pollution in well-established utility function `mergeDeep`
- CRITICALCVE-2026-104848
Tinypool: Prototype Pollution gadget in worker options leads to Remote Code Execution