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;
}

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.