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" stripRemoving 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 sinksA 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 normaliseThese 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 formFixing 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/prototyperecursively 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.