Prototype Pollution: The Complete Practical Guide

Bypassing Flawed Prototype Pollution Sanitisation

How to defeat prototype pollution filters: reach Object.prototype via constructor.prototype, beat a single-pass key stripper, and vary the key form.

Published

Filters that block prototype pollution almost always block the wrong thing: a specific key string rather than the structural write. This page covers the standard bypasses, each defeating a specific weak filter. Practice on a free live target.

When proto is blocked: constructor.prototype

obj.__proto__ and obj.constructor.prototype both reach Object.prototype. Block the first and the second still gets there, as a nested key path:

# __proto__ filtered:
?constructor[prototype][polluted]=yes
{"constructor":{"prototype":{"isAdmin":true}}}

The merge walks constructor then prototype then your property, assigning onto the shared prototype. It is a two-hop walk, so it needs a sink that recurses that far, which the common vulnerable mergers do.

Defeating a single-pass key stripper

A filter that removes the substring __proto__ exactly once, without re-checking, is defeated by nesting the word inside itself. Remove the inner copy and a clean one remains:

__pro__proto__to__      -> strip the middle "__proto__" -> leaves "__proto__"

The same trick works for a single-pass constructor stripper, using the parallel nesting. These are two independent payloads, one per filtered word:

__pro__proto__to__          beats a single-pass "__proto__" strip
constconstructorructor      beats a single-pass "constructor" strip

Removing the inner constructor from constconstructorructor leaves constructor.

Vary the key form

The accepted key syntax depends on the parser and filter. Try all forms:

__proto__[polluted]=yes         bracket form (qs-style parsers)
__proto__.polluted=yes          dotted form (other parsers)
["__proto__"]["polluted"]       array/bracket form in some sinks

A filter that blocks dotted but allows bracket (or vice versa) falls to whichever form the parser also accepts.

Case and encoding games

Naive filters compare before normalising:

URL-encode the key          %5f%5fproto%5f%5f for __proto__ (%5f = "_")
mixed delivery              split the key across forms the filter does not normalise

These are situational, but a filter that does a raw string compare before decoding misses them.

Why block-lists lose

Every bypass above is a different string reaching the same prototype. A block-list must catch all of them, and missing one reopens the hole. The durable fix is structural, not a denylist: null-prototype objects, safe merges, and allow-list schema validation. That is the next spoke.

Edge variants

constructor[prototype][x]       the primary __proto__ fallback
__pro__proto__to__              single-pass strip bypass
constconstructorructor          single-pass constructor strip bypass
["__proto__"]["x"]              array key form

Fixing it

  • Do not rely on key block-lists. Use Object.create(null) maps and safe merge utilities with no prototype to reach.
  • Reject __proto__/constructor/prototype recursively and after decoding, as a defence-in-depth layer only.
  • Validate against a schema so only known-good keys survive.

Full guidance is in preventing prototype pollution. Next, close the whole class in preventing prototype pollution, or for every payload see the cheat sheet.

← Back to Prototype Pollution: The Complete Practical Guide