Safeguard
Application Security•

CWE-1321 Prototype Pollution: How It Happens and How to Fix It

Prototype pollution lets attackers add or change properties on every JavaScript object in your app. Here is how merge functions cause it and what actually stops it.

Safeguard Security Team
5 min read

CWE-1321, Improperly Controlled Modification of Object Prototype Attributes, better known as prototype pollution, hits JavaScript code that takes attacker-influenced keys and uses them to set attributes on an object. If the key reaches the object prototype, the attacker is no longer changing one object. They are changing the template that most objects in the process inherit from.

It matters most to Node.js services and to the npm packages they pull in, because the bug usually lives in a small utility library that merges or copies objects.

How it happens

Every JavaScript object has a prototype, and the language exposes it through special attributes: __proto__, constructor and prototype. MITRE's description is that the product receives input that says which attributes to initialize or update in an object, and does not properly control changes to the object prototype.

The usual shape is a recursive merge, a "set default values" helper, or a function that sets a value by a dotted path such as a.b.c. Any of them can be walked into __proto__ by a crafted key.

// Vulnerable pattern: recursive merge with untrusted keys
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object" && source[key] !== null) {
      if (!target[key]) target[key] = {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, payload);

const user = {};
console.log(user.isAdmin); // true, inherited from the polluted prototype

After that one call, every plain object in the process that does not define isAdmin itself reports true. Code that tests a flag like that now takes the wrong branch.

The record lists four observed CVEs, all in utility code of the kind shown above:

  • CVE-2018-3721: pollution by merging objects.
  • CVE-2019-10744: pollution by setting default values on object attributes recursively.
  • CVE-2019-11358: pollution by merging objects recursively.
  • CVE-2020-8203: pollution by setting object attributes from a dot-separated path.

Notice that these are library bugs. Your own code may never contain the pattern, and your application can still be exposed through a dependency several levels down.

What an attacker gains

MITRE lists two consequences. The first covers confidentiality, integrity and availability: an attacker can read and modify application data by injecting attributes that the application later reads, as shown above. The second is availability: an attacker can override existing attributes with values of an incompatible type, which may crash the process.

What the attacker can do with a polluted attribute depends entirely on which attributes the application and its libraries read. That is why the same root cause shows up with very different impact in different products.

How to find it

MITRE notes that static analysis (SAST) can find some instances by modeling data flow from untrusted input to a property assignment.

Simple checks you can run today:

  • Search your code for recursive merge, extend, clone and "set by path" helpers, and check whether any of them accept keys from request bodies, query strings or parsed files.
  • Add a test that merges {"__proto__": {"polluted": true}} and then asserts that ({}).polluted is undefined.
  • List your dependencies and look for the packages named in advisories. A dependency scan answers this faster than reading code. The free scanner at gold.safeguard.sh/scan runs in your browser with no login, keeps the file on your machine and covers up to 100 dependencies.
  • Reachability analysis helps with the next question. Having a vulnerable merge function in node_modules is one thing. Your code actually calling it with untrusted input is another, and that decides which upgrade goes first.

How to fix it

The record offers several fixes with different strengths.

  1. Block modification of attributes that resolve to the object prototype, such as __proto__ and prototype. Rated high.
  2. Create objects without a prototype using Object.create(null). Then there is no prototype to reach through the special attributes. Rated high.
  3. Freeze the prototype with Object.freeze(Object.prototype). This prevents modification completely, but MITRE says other methods are preferable where possible, especially inside libraries that other software depends on.
  4. Use Map instead of plain objects for key-value data. Map methods cannot reach or change the prototype. Rated moderate.
  5. Validate untrusted objects against a schema. Rated limited, so use it as an extra layer.

A fixed version of the merge function from earlier:

// Safer: skip dangerous keys and use prototype-less objects
const BLOCKED = new Set(["__proto__", "constructor", "prototype"]);

function merge(target, source) {
  for (const key of Object.keys(source)) {
    if (BLOCKED.has(key)) continue;
    const value = source[key];
    if (typeof value === "object" && value !== null) {
      if (!Object.prototype.hasOwnProperty.call(target, key)) {
        target[key] = Object.create(null);
      }
      merge(target[key], value);
    } else {
      target[key] = value;
    }
  }
  return target;
}

Also update the libraries. If a dependency in your tree has a known prototype pollution advisory, the fix is normally a patched version, and upgrading is cheaper than auditing every call site.

Try it yourself

Walk through a code scan that flags unsafe patterns like the merge above, then see how findings are triaged.

Click through it at your own pace, or open it full screen.

Quick check

Why does one polluted object affect other objects? Objects inherit missing attributes from the prototype. If the prototype gains an attribute, every object that does not define that attribute itself appears to have it.

Which fix does MITRE call out as less suitable for libraries? Freezing the object prototype. It works, but MITRE recommends other methods where possible, particularly in components that other software depends on.

Is schema validation enough on its own? No. MITRE rates it as limited effectiveness, so pair it with prototype-less objects, key blocking or Map.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.