Why Cursor Generates Prototype Pollution in Deep Merge Functions

작성자

카테고리:

← 피드로
DEV Community · Charles Kern · 2026-07-25 개발(SW)
Cover image for Why Cursor Generates Prototype Pollution in Deep Merge Functions

Charles Kern

TL;DR

  • AI editors write recursive object merges that trust every key in the source object, including __proto__ and constructor.prototype
  • A single crafted payload silently overwrites Object.prototype, infecting every object in the Node process until the server restarts
  • The fix is a three-key guard at the top of the loop: skip __proto__, constructor, and prototype before any recursion

I asked Cursor to write a deep merge function. I needed to layer environment-specific config over defaults. A pattern I have built a dozen times. The function it produced was 15 lines, handled nested objects correctly, and passed every test I wrote.

Three days later I discovered I could POST {"__proto__": {"isAdmin": true}} to any endpoint that fed user JSON into that function, and the server would treat every subsequent object as having isAdmin set to true. The entire Node process was compromised from one request. The infection lasted until the server restarted.

The function Cursor wrote was prototype pollution, CWE-1321, in its cleanest form.

The Vulnerable Code

Recursive merge functions that iterate over object keys without filtering become a direct write channel into Object.prototype. Any key named __proto__ in a source object overwrites the prototype chain of every object created in the process after that point.

// CWE-1321 - AI-generated deep merge, no key validation
function deepMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      target[key] = target[key] || {};
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// Attacker sends: {"__proto__": {"isAdmin": true}}
deepMerge({}, JSON.parse(userInput));
console.log({}.isAdmin); // true - Object.prototype is now poisoned

Enter fullscreen mode Exit fullscreen mode

The function looks reasonable. It is the exact pattern that appears in hundreds of tutorials on deep merge in JavaScript. AI editors learned it from those tutorials, and the tutorials almost never include the key guard.

Why This Keeps Happening

AI editors generate prototype-pollutable merge functions because the recursive merge pattern is one of the most-copied JavaScript snippets online, and the overwhelming majority of those snippets omit the safety check. The model has seen this problem solved correctly, functionally, returns the right merged value, far more often than it has seen it solved safely.

There is also a subtler reason the vulnerability survives: the dangerous behavior only manifests when user-controlled input flows into the function. A unit test written against hardcoded fixture objects never triggers it. The function tests green, gets merged, ships. The training data for the next model generation logs it as a correct implementation.

Real CVEs exist for this pattern. lodash was hit with CVE-2019-10744 (CVSS 9.8) over a prototype-pollutable merge. jQuery had a similar issue (CVE-2019-11358). Multiple Express middleware packages have been patched for it. This is not a theoretical edge case.

The Fix

Block any key that can reach the prototype chain before the recursion descends. Three keys cover the entire attack surface: __proto__, constructor, and prototype. One continue statement at the top of the loop is the entire fix.

// Safe deep merge - block prototype-chain keys
function deepMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue; // never descend into these
    }
    if (typeof source[key] === 'object' && source[key] !== null) {
      target[key] = target[key] || {};
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

Enter fullscreen mode Exit fullscreen mode

Python equivalent for dict merges that process untrusted input:

# Python safe recursive merge
BLOCKED_KEYS = {'__proto__', '__class__', '__subclasshook__'}

def deep_merge(target, source):
    for key, value in source.items():
        if key in BLOCKED_KEYS:
            continue
        if isinstance(value, dict) and isinstance(target.get(key), dict):
            deep_merge(target[key], value)
        else:
            target[key] = value
    return target

Enter fullscreen mode Exit fullscreen mode

One complementary approach is to initialize merge targets with Object.create(null) — prototype-free objects that cannot be polluted via __proto__ access. Use it as a defense-in-depth measure, not a replacement: constructor.prototype attacks survive Object.create(null), so the key check is still necessary.

FAQ

Q: Does prototype pollution in JavaScript actually lead to exploitable security bugs?
A: Yes. lodash CVE-2019-10744 (CVSS 9.8) is the clearest example. A successful attack overrides properties like isAdmin, role, or authenticated on every object in the process. Anything that reads a property from an untrusted object and acts on it becomes a path to privilege escalation or auth bypass.

Q: Can TypeScript or a linter catch this at the type level?
A: No. TypeScript types Object.keys as returning string[], which is correct. It cannot distinguish "__proto__" from "username" at the type level. You need a SAST rule specifically targeting recursive merge patterns without key guards. Semgrep has a community rule for this; SafeWeave’s SAST scanner catches it with a more precise AST-level pattern.

I’ve been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags these patterns before I move on. Even a basic pre-commit hook with semgrep and gitleaks will catch most of what is in this post. The important thing is catching it early, whatever tool you use.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다