Skip to content
Security Article

Why AI Editors Keep Generating Prototype Pollution Vulnerabilities

Recursive merge helpers generated by AI tools frequently lack basic security guards, exposing JavaScript applications to prototype pollution.

Ji-ho Choi
Ji-ho Choi
Security & Cloud Editor · Jul 10, 2026 · 5 min read
Why AI Editors Keep Generating Prototype Pollution Vulnerabilities

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

  1. Why Cursor Keeps Writing Prototype Pollution Into Your Merge Code — dev.to
  2. Prototype Pollution in merge | Snyk — security.snyk.io
  3. What is prototype pollution? | Web Security Academy — portswigger.net
  4. How to prevent prototype pollution vulnerabilities in JavaScript | Snyk — snyk.io
  5. fix: possible prototype pollution within merge by n1ru4l · Pull Request #34 · lukeed/dset — github.com
Ji-ho Choi
Written by
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

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading