TL;DR
- AI editors generate deep-merge and object-spread patterns vulnerable to prototype pollution
- Attackers inject proto properties to override Object defaults and bypass auth
- Use Object.create(null), allowlists, or libraries with built-in prototype guards
Last week I was reviewing a side project a friend built entirely in Cursor. Express backend, MongoDB, the usual stack. The code looked clean. Linted. Tests passing. Then I spotted this in a settings endpoint:
function mergeConfig(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] = mergeConfig(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
That recursive merge has no guard against proto or constructor keys. It is textbook prototype pollution (CWE-1321).
Why AI editors keep writing this
The training data is the problem. StackOverflow is full of "how to deep merge objects in JavaScript" answers from 2015-2019 that use exactly this pattern. The top-voted answers predate any awareness of prototype pollution as an attack vector. LLMs reproduce what scored highest.
I asked Cursor to "write a function that deep merges two config objects" five times. Three out of five had no prototype guard. One used Object.assign (still vulnerable to shallow pollution). Only one used a library.
The pattern shows up in three common shapes:
Shape 1 -- Recursive merge with no key filter:
// CWE-1321: no __proto__ guard
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Shape 2 -- Express body parsed into config:
// CWE-1321: user input flows directly into object merge
app.put('/api/settings', (req, res) => {
Object.assign(appConfig, req.body);
res.json({ ok: true });
});
Shape 3 -- Spread into prototype-bearing objects:
// CWE-1321: spreading user input can override inherited properties
const userPrefs = { ...defaults, ...req.body };
What an attacker does with this
Send a JSON body like:
{"__proto__": {"isAdmin": true}}
Now every object in the process inherits isAdmin = true. Auth checks that rely on if (user.isAdmin) pass for everyone. Game over.
This is not theoretical. CVE-2019-10744 (lodash merge), CVE-2020-28498 (elliptic), CVE-2021-25928 (safe-obj) -- all prototype pollution. It is one of the most exploited vulnerability classes in Node.js.
The fix
Option A -- Allowlist keys explicitly:
const ALLOWED_KEYS = new Set(['theme', 'language', 'timezone']);
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (!ALLOWED_KEYS.has(key)) continue;
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = safeMerge(target[key] || Object.create(null), source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Option B -- Use Object.create(null) for config objects:
const config = Object.create(null); // no prototype chain
Option C -- Block dangerous keys:
const DANGEROUS = new Set(['__proto__', 'constructor', 'prototype']);
// skip any key in DANGEROUS during merge
Option D -- Use a safe library:
lodash.mergeWith (post-4.17.12 patch), deepmerge with customMerge, or structuredClone for simple cases.
I have been running SafeWeave to catch these. It hooks into Cursor and Claude Code as an MCP server and flags prototype pollution patterns before I move on. That said, even a semgrep rule targeting recursive merges with no hasOwnProperty check will catch the worst cases. The important thing is catching it early, whatever tool you use.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.