DEV Community

Charles Kern
Charles Kern

Posted on

3 Prototype Pollution Bugs Cursor Keeps Writing Into Your Code

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 });
});
Enter fullscreen mode Exit fullscreen mode

Shape 3 -- Spread into prototype-bearing objects:

// CWE-1321: spreading user input can override inherited properties
const userPrefs = { ...defaults, ...req.body };
Enter fullscreen mode Exit fullscreen mode

What an attacker does with this

Send a JSON body like:

{"__proto__": {"isAdmin": true}}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Option B -- Use Object.create(null) for config objects:

const config = Object.create(null); // no prototype chain
Enter fullscreen mode Exit fullscreen mode

Option C -- Block dangerous keys:

const DANGEROUS = new Set(['__proto__', 'constructor', 'prototype']);
// skip any key in DANGEROUS during merge
Enter fullscreen mode Exit fullscreen mode

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.