AuditCoreAuditCore
highCWE-1321OWASP A08:2021 — Software and Data Integrity Failures

How to fix Prototype Pollution

JavaScript-specific bug class. Attacker sends { '__proto__': { 'isAdmin': true } }, your _.merge() call adds isAdmin: true to Object.prototype, every object in your app now thinks it's admin. Lodash, jQuery, Express body-parser, hundreds of packages have shipped this. Here's the fix.

What is Prototype Pollution?

Prototype pollution exploits JavaScript's prototype chain. When you set a property on `__proto__` (or `constructor.prototype`) of any object, you're modifying the global Object prototype that ALL objects inherit from. `({}).__proto__.isAdmin = true` makes every object in your app appear to have an `isAdmin: true` property. Authorization checks like `if (user.isAdmin)` now succeed for unauthenticated users.

The vulnerability appears in code that recursively merges/clones/sets nested object properties from user input. The classic: `_.merge(config, req.body)` with body `{__proto__: {isAdmin: true}}`. Lodash, jQuery extend, Express body-parser deep-extend, query-string nested parse — all have had prototype pollution CVEs at some point. Even `JSON.parse` is safe, but `JSON.parse` + `Object.assign(target, source)` followed by recursive merge is not.

Impact ranges from logical bugs (flag set unexpectedly) to RCE in some Node.js applications via property pollution that affects template engines (Pug compile-time pollution → SSTI), Express middleware (body-parser pollution → auth bypass), or argument resolution (when polluted prototype's properties affect spawn/exec calls).

What an attacker can do

The concrete impact of leaving Prototype Pollution unpatched.

Authorization bypass via polluted isAdmin / role flags

Every object has the polluted property; checks like `user.isAdmin` succeed without authentication.

Denial of service via toString / valueOf pollution

Polluting Object.prototype.toString breaks everything that calls toString — entire app crashes.

RCE via templating / argument confusion

Pug + polluted prototype → SSTI → RCE. Same with some serialization libraries.

Subtle business logic bugs

Polluted `defaultValue` properties affect every config, every settings object — debugging nightmare in production.

How do I know if I'm vulnerable?

Manual: search for `_.merge`, `_.set`, `_.defaultsDeep`, `Object.assign` in recursive contexts, `JSON.parse` followed by deep manipulation, jQuery's `.extend(true, ...)`. Check whether the source object can come from user input. If yes, vulnerable.

Automated: AuditCore's Prototype Pollution Scanner sends `__proto__` and `constructor.prototype` payloads to JSON endpoints and query parameters. Detection: check for polluted state in subsequent requests (`x?__proto__.x=y`, then `y` should not appear elsewhere). Semgrep flags vulnerable lodash/extend usage in source code.

How to fix Prototype Pollution

5 ordered steps. Apply them in order — each builds on the previous.

  1. 1

    Use Object.create(null) for objects holding user data

    Objects without a prototype can't be polluted because they have no prototype to pollute.

    Use for any object that gets nested merge/extend with user input — config objects, options bags, request state.

    javascriptStep 1
    // ❌ Default object has Object.prototype
    const config = {};
    deepMerge(config, userInput);  // pollution sets Object.prototype.X
    
    // ✅ Object.create(null) has no prototype
    const config = Object.create(null);
    deepMerge(config, userInput);  // sets properties on config only
    
    // Note: Object.create(null) doesn't have toString, hasOwnProperty etc.
    // Use Object.prototype.hasOwnProperty.call(obj, key) when checking keys
  2. 2

    Use Map / Set instead of plain objects when keys come from input

    Map and Set are immune to prototype pollution because they don't use the prototype chain for storage.

    If you're using an object as a hashmap (`obj[key] = value`), use Map. Map.set is safe; obj[key] is not when key is user-controlled.

    javascriptStep 2
    // ❌ Object as hashmap with user-controlled keys
    const cache = {};
    for (const item of userItems) {
      cache[item.id] = item;  // if item.id === '__proto__', pollution
    }
    
    // ✅ Use Map
    const cache = new Map();
    for (const item of userItems) {
      cache.set(item.id, item);  // safe — Map keys can be anything
    }
  3. 3

    Block __proto__ and constructor at the deserialization boundary

    Reject any input containing `__proto__`, `constructor`, or `prototype` keys. Belt-and-suspenders defense.

    Express middleware can do this globally. Per-request validation with a schema library (Zod, Joi) also catches it.

    javascriptStep 3
    // Express middleware — sanitize __proto__ recursively
    function sanitizePollution(obj, depth = 0) {
      if (depth > 20) return;  // recursion limit
      if (obj === null || typeof obj !== "object") return;
      for (const key of Object.keys(obj)) {
        if (key === "__proto__" || key === "constructor" || key === "prototype") {
          delete obj[key];
        } else {
          sanitizePollution(obj[key], depth + 1);
        }
      }
    }
    
    app.use((req, res, next) => {
      sanitizePollution(req.body);
      sanitizePollution(req.query);
      sanitizePollution(req.params);
      next();
    });
  4. 4

    Upgrade vulnerable dependencies (lodash, query-string, etc.)

    Many of the historical pollution vulns are in popular packages. Run AuditCore's Trivy integration to find them; upgrade to patched versions.

    lodash <4.17.21 (CVE-2021-23337), set-value <4.0.1, mixin-deep <2.0.1, dot-prop <5.1.1 — all vulnerable. Newer versions block __proto__ properly.

    jsonStep 4
    {
      "dependencies": {
        "lodash": "^4.17.21",     // CVE-2021-23337 fixed
        "set-value": "^4.0.1",
        "mixin-deep": "^2.0.1",
        "dot-prop": "^5.1.1"
      }
    }
    
    // Run `npm audit` regularly; AuditCore's Trivy integration checks these on every scan
  5. 5

    Object.freeze(Object.prototype) on app startup

    Nuclear option: freeze Object.prototype so it cannot be mutated. Runtime cost is negligible; some libraries break (rare).

    Test thoroughly — third-party code that legitimately polyfills `Object.prototype.X` will fail. Most modern code doesn't do that.

    javascriptStep 5
    // In your app entry point, before requiring third-party code
    Object.freeze(Object.prototype);
    Object.freeze(Array.prototype);
    Object.freeze(Function.prototype);
    
    // Now even successful pollution silently fails:
    ({}).__proto__.isAdmin = true;
    console.log({}.isAdmin);  // undefined (silently swallowed in non-strict mode)
    // In strict mode, throws TypeError

How to verify the fix

AuditCore — the Prototype Pollution Scanner sends payloads and verifies pollution by triggering polluted properties in subsequent requests.

Manual: send `curl -X POST -H 'Content-Type: application/json' -d '{"__proto__":{"polluted":true}}' https://app.example.com/api/something`. Then in browser dev tools console: `({}).polluted` — if it returns `true`, you have prototype pollution.

FAQ

Frequently asked questions

Is prototype pollution only a Node.js problem?+

It's JavaScript-wide — works in browsers too. Most reported impact is server-side (Node.js) because that's where it leads to auth bypass and RCE. Browser-side pollution can affect frontend logic but rarely escalates to full compromise.

Does using TypeScript prevent prototype pollution?+

No. TypeScript types are erased at runtime; `any` types and runtime input bypass type checks. The lodash `_.merge(target, anyInput)` is just as dangerous in TS as in JS.

What's the difference between __proto__ and constructor.prototype attacks?+

Two paths to the same target. `obj.__proto__` is a shortcut to `Object.getPrototypeOf(obj)`. `obj.constructor.prototype` reaches it via the constructor chain. Some sanitizers block one but not the other; block both.

Is JSON.parse safe?+

JSON.parse itself is safe — it doesn't use prototype chains, and the resulting object's `__proto__` key (if explicitly in JSON) is set as a regular own property, not the prototype. The danger is what you DO with the parsed object — `Object.assign({}, parsed)` is safe, but recursive merge into an existing target is not.

Can prototype pollution be exploited remotely without code review?+

Yes — automated scanners (including AuditCore's) test for it via JSON payloads. If your endpoint accepts JSON and merges into shared state, you're testable. Bug bounty hunters mass-scan for it.

Don't just guess — scan and verify

AuditCore Free Trial scans your homepage for Prototype Pollution and 50+ other vulnerability classes. No credit card. Results in 60 seconds.