logo

Database

Prototype Pollution In axios

Description

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.

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: 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 #### 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 =

Mitigation

Update Impact

Minimal update. May introduce new vulnerabilities or breaking changes.

Ecosystem
Component
Affected version
Patched versions