SecurityArticle
Recursive merge helpers generated by AI tools frequently lack basic security guards, exposing JavaScript applications to prototype pollution.
We ask an AI assistant like Cursor to write a quick helper function to deep-merge two configuration objects. The model spits out an elegant, eight-line recursive function. It passes your unit tests. It handles nested objects perfectly. But it also introduces a critical security vulnerability (CWE-1321) that can compromise your entire Node.js process or client-side application.
This is not an isolated glitch. AI code assistants repeatedly generate vulnerable recursive merge patterns. The bug is silent, invisible during normal execution, and highly exploitable.
The Anatomy of the Generated Bug #
Let's look at the classic recursive merge function that LLMs love to write:
function merge(target, source) {
for (const key in source) {
if (source[key] && typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
On the surface, this looks like standard JavaScript. But the vulnerability triggers when the source
object contains untrusted user input, such as a parsed JSON request body.
Consider this payload:
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, payload);
When JSON.parse()
runs, it treats __proto__
as an ordinary string key, returning an object that has an own property named __proto__
. When our generated merge
function processes this key, it evaluates target["__proto__"]
. In JavaScript, __proto__
acts as a getter and setter for the object's prototype.
Instead of setting a property on the local target object, the assignment recurses and writes isAdmin: true
directly onto Object.prototype
. Because almost every object in JavaScript inherits from Object.prototype
through the prototype chain, every single object in the application now has isAdmin
set to true
.
console.log(({}).isAdmin); // true
If your application subsequently checks if (user.isAdmin)
for authorization, an unauthenticated attacker who sent that JSON payload now has administrative privileges.
Why LLMs Keep Recreating This Vulnerability #
Why does this pattern persist in AI-generated code? The answer lies in the training corpus.
For over a decade, developers have shared simple recursive merge snippets on StackOverflow, blogs, and GitHub. Most of these historical snippets do not guard special keys. Because the missing check never breaks the happy-path execution of a test suite, these insecure implementations remained popular and highly ranked.
LLMs reproduce the statistical shape of their training data. They generate code that looks correct and satisfies basic functional requirements. Since prototype pollution only manifests under malicious input, standard test suites generated by developers (or the AI itself) do not catch it.
The Multi-Key Trap: Why Partial Fixes Fail #
Fixing prototype pollution is not as simple as blocking __proto__
. This is a common trap that even popular open-source libraries have fallen into.
For example, the merge
package (vulnerabilities tracked under SNYK-JS-MERGE-1040469) attempted to prevent prototype pollution by checking for __proto__
, but failed to block constructor
or prototype
. An attacker can bypass a simple __proto__
check by targeting the constructor property:
// Bypassing __proto__ filters via constructor.prototype
const payload = JSON.parse('{"constructor": {"prototype": {"isAdmin": true}}}');
This is why robust sanitization must block all three dangerous keys: __proto__
, constructor
, and prototype
.
This exact issue was debated and resolved in the popular utility library dset
(PR #34 on GitHub). The maintainers added explicit checks to break the loop if any of these keys are encountered:
if (k === '__proto__' || k === 'constructor' || k === 'prototype') break;
To secure the AI-generated helper, we must apply the same logic:
function merge(target, source) {
for (const key in source) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
if (source[key] && typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Developer Workflow and Mitigations #
How should developers handle this risk in production workflows?
First, avoid hand-rolling recursive merge functions. Standard libraries like Lodash have gone through years of security hardening to address these edge cases. If you need a deep merge, use a well-maintained library rather than asking an AI to generate a custom helper.
Second, if you must merge user-controlled data into plain objects, consider using objects without prototypes. You can instantiate an object with Object.create(null)
. This breaks the prototype chain entirely, meaning there is no __proto__
or constructor
property to pollute.
const safeObject = Object.create(null);
// safeObject has no prototype chain, making it immune to prototype pollution
Alternatively, use a Map
instead of a plain JavaScript object for storing dynamic user-defined key-value pairs.
To catch these issues before they reach production, integrate static analysis tools into your CI/CD pipeline. A basic pre-commit hook running Semgrep or CodeQL will flag unsafe recursive merge patterns immediately. You can also use security scanning tools from Snyk to monitor your dependencies for known prototype pollution vulnerabilities.
AI code assistants are powerful accelerators, but they are historical mirrors, not security experts. When you ask an editor to generate utility functions, remember that it is pulling from a legacy of code written before modern security threats were fully understood. Guard your keys, validate your JSON schemas, and never trust a generated recursive function without a thorough manual review.
Sources & further reading #
Why Cursor Keeps Writing Prototype Pollution Into Your Merge Code— dev.to - Prototype Pollution in merge | Snyk— security.snyk.io - What is prototype pollution? | Web Security Academy— portswigger.net - How to prevent prototype pollution vulnerabilities in JavaScript | Snyk— snyk.io - fix: possible prototype pollution within merge by n1ru4l · Pull Request #34 · lukeed/dset— github.com
Ji-ho Choi· Security & Cloud Editor
Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.
Discussion 0 #
No comments yet
Be the first to weigh in.