All posts

Field guide

Prototype pollution in AI code: the merge that rewrites every object

A recursive merge is a common utility, and the obvious version copies every key it is given. Feed it one named __proto__ and you are no longer editing your object, you are editing every object in the app. Here is how this JavaScript-specific bug works and how to merge without opening it.

Last updated

Prototype pollution is a JavaScript vulnerability with an outsized blast radius. It happens when code copies keys from untrusted input into an object without filtering, and one of those keys is __proto__, constructor, or prototype. Writing through that key reaches Object.prototype, which nearly every object inherits from, so a single request can change behavior across the entire application.

Why AI tools write it

A deep merge is a natural thing to ask for and a natural thing to write, and the straightforward version uses a for-in loop that copies every enumerable key. To a model matching on syntax, that reads as correct, and it passes any test built from normal data. The __proto__ case never appears in ordinary input, so the missing guard is invisible until someone sends it on purpose.

// the vulnerable pattern: copy whatever keys arrive
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];   // key could be "__proto__"
    }
  }
  return target;
}
merge({}, JSON.parse(req.body));

The concept in full, including what an attacker does once the prototype is polluted, is in what prototype pollution is.

Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.

See a live scan →

The fix: refuse the dangerous keys

Skip __proto__, constructor, and prototype in any merge over untrusted data, iterate with Object.keys, and build onto an object with a null prototype so there is nothing to pollute in the first place.

const BLOCKED = new Set(["__proto__", "constructor", "prototype"]);

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (BLOCKED.has(key)) continue;
    const val = source[key];
    if (val && typeof val === "object" && !Array.isArray(val)) {
      target[key] = safeMerge(target[key] ?? Object.create(null), val);
    } else {
      target[key] = val;
    }
  }
  return target;
}
safeMerge(Object.create(null), JSON.parse(req.body));

Even simpler where you can: use a maintained library's merge, since the popular ones are already hardened, and validate input against a schema so unexpected keys never reach the merge. The full walkthrough is in the fix for an unsafe object merge.

Frequently asked questions

What exactly is the prototype that gets polluted?

In JavaScript, almost every object inherits from Object.prototype, a shared object that provides default properties. When code writes to a key like __proto__ on a normal object, it can reach that shared prototype and add or change a property on it. Because the prototype is shared, the change appears on every object in the program at once, which is what makes a single bad write have app-wide effects.

What can an attacker actually do with it?

It depends on what your code reads off objects afterward, and the range is wide. At the mild end it breaks logic in confusing ways. At the serious end, the attacker sets a property your code later trusts, like an option that enables a dangerous path or a flag such as isAdmin, turning a data write into privilege escalation or, in some setups, code execution. The effect often shows up far from the merge, which makes it hard to trace.

Which code is vulnerable?

Any code that copies keys from untrusted input into an object without filtering: a hand-rolled deep merge, a recursive assign, or setting nested properties from a path string. If the input can contain a key named __proto__, constructor, or prototype and your code copies it through, you are exposed. Query strings, JSON bodies, and config objects from users are the usual sources.

How do I prevent it?

Skip the dangerous keys __proto__, constructor, and prototype in any merge or assign over untrusted data, iterate with Object.keys and build onto an object created with a null prototype, and validate input against a schema so unexpected keys are dropped before merging. Where you can, use a maintained library's merge, since the popular ones are already hardened against this.

Check your merge and assign helpers

A recursive merge over request data is worth a look, and a scan flags the pattern by shape so you do not have to audit every utility by hand. Run a free scan and see whether any merge in your app copies keys it should not.

Prbl scans your live app or your codebase for exactly the kinds of issues above.

Prototype Pollution in AI Code: The Merge That Rewrites Every Object: Prbl