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.













