Safeguard
Application Security•

ASVS 5.0 V15.3.6: Prevent JavaScript Prototype Pollution

ASVS V15.3.6 asks JavaScript code to avoid prototype pollution. Use Map, null prototype objects and safe merges, then test with a __proto__ payload.

Safeguard Security Team
3 min read

ASVS 5.0 V15.3.6 requires JavaScript to be written so prototype pollution cannot happen, for example by using Set or Map instead of plain object literals as dictionaries. It is Level 2. The short version: never merge or assign untrusted keys into ordinary objects without filtering, and prefer structures that have no prototype chain to poison.

Why it matters

Every JavaScript object inherits from Object.prototype. If an attacker can write to a key named __proto__ or constructor.prototype, they change the default behavior of every object in the process. Effects range from flipped authorization checks (isAdmin suddenly present on everything) to denial of service and, in some template or option handling code, code execution.

The usual entry point is a deep merge, clone or "set by path" helper fed with parsed JSON or a query string.

How to implement it

Use a Map for key value data whose keys come from users:

const counts = new Map();
function hit(name) {
  counts.set(name, (counts.get(name) ?? 0) + 1);
}

If you need an object, create it without a prototype:

const dict = Object.create(null);
dict[userKey] = value;     // "__proto__" is just an ordinary key here

For merges, block dangerous keys and use own properties only:

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

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (FORBIDDEN.has(key)) continue;
    const v = source[key];
    if (v !== null && typeof v === "object" && !Array.isArray(v)) {
      target[key] = safeMerge(
        Object.hasOwn(target, key) && typeof target[key] === "object" ? target[key] : {},
        v
      );
    } else {
      target[key] = v;
    }
  }
  return target;
}

Two more defenses help. Run Node with --disable-proto=delete where compatible, and freeze the prototype at startup with Object.freeze(Object.prototype) after testing that your dependencies tolerate it. Keep merge, clone and path setting libraries updated, because most real world cases were vulnerabilities in those packages. A dependency scan, such as the free one at https://gold.safeguard.sh/scan, shows which versions you ship.

How to test it

  1. Find every deep merge, clone, extend and path assignment call that touches request data.
  2. Send a body like {"__proto__": {"polluted": true}} and {"constructor": {"prototype": {"polluted": true}}}.
  3. In a test, afterward check that ({}).polluted is undefined.
  4. Repeat with query strings, for example a[__proto__][polluted]=1 against parsers that build nested objects.
  5. Add the check as an automated regression test.

Common mistakes

  • Filtering only __proto__ and forgetting constructor.prototype.
  • Fixing your own helper while a vulnerable library version still merges input.
  • Testing only top level keys when the pollution works at depth.
  • Checking in instead of own property tests, which sees inherited keys.

Quick check

Does JSON.parse create a prototype pollution problem by itself?

It creates an own property named __proto__ and does not pollute. The danger appears when a later merge copies that key onto another object.

Which level is this requirement?

Level 2.

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.