| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Server-Side Prototype Pollution |
| Difficulty | Practitioner |
| Objective | Pollute the server-side Object.prototype via a JSON merge in the address update endpoint to set isAdmin: true and delete carlos |
Privilege Escalation via Server-Side Prototype Pollution¶
I logged in as wiener:peter and found a billing and delivery address form:
Intercepting the form submission:
The request POSTs JSON to /my-account/change-address, and the response returns a JSON representation of the updated user object — including isAdmin: false. POST/PUT endpoints that accept JSON and echo back the modified object are the prime candidates for server-side prototype pollution, because the response shows whether injected properties propagate.
Server-side prototype pollution runs in Node.js — there's no DevTools console on the server. The oracle is the HTTP response: if a property injected via __proto__ appears in the server's JSON response, pollution is confirmed. JSON.parse() in Node.js treats __proto__ as a real key; if the app then merges the parsed object using an unsafe recursive merge (common in Express apps), Object.prototype gets polluted process-wide — affecting every object for all users until the server restarts.
Adding a __proto__ key with a benign test property:
{
"address_line_1": "Wiener HQ",
...,
"__proto__": {
"teto": "miku"
}
}
Response:
{"username":"wiener",...,"isAdmin":false,"teto":"miku"}
teto: "miku" appeared in the response — never in the original user object, inherited from the prototype. Pollution confirmed. The response also handed us the gadget: isAdmin: false was visible in the user serialization, meaning the access control check reads it from the object. We can override it via the prototype.
Injecting isAdmin: true:
{
"__proto__": {
"isAdmin": true
}
}
Response: "isAdmin":true. Refreshing the page:
Admin panel link appeared. Navigating to /admin:
Deleted carlos.
Lab solved :P