Prototype Pollution: The Complete Practical Guide
Detecting Server-Side Prototype Pollution
How to detect blind server-side prototype pollution: polluting a property the framework reflects (status, exposedHeaders, json spaces) and a safe canary.
Published
Server-side prototype pollution is blind: the prototype never appears in a response, so you confirm it by polluting a property the framework reflects and watching for a change. This page covers the probes and the safe way to run them. Practice on a free live target.
Why it is blind
On the client you read Object.prototype in the console. On the server you cannot see the process, so you pollute a property that the framework reads from config and reflects into something observable, then compare against a clean baseline. You are inferring the write from a side effect.
Reflected-property probes
Properties frameworks read from options and expose in responses. Against Express and similar Node stacks:
status pollute it -> error responses return your custom code (e.g. 510)
exposedHeaders pollute it (CORS) -> value added to Access-Control-Expose-Headers
json spaces pollute it -> res.json() responses are pretty-printed/indentedA worked status probe:
POST /api/settings HTTP/1.1
Host: target.example
Content-Type: application/json
{"__proto__":{"status":510}}Then trigger an error path and compare:
HTTP/1.1 510 Not Extended
Content-Type: application/json
{"error":"..."}An unexpected 510 where the baseline returned 500 confirms the prototype moved.
The json spaces caveat
The json spaces probe (pollute it so JSON responses become indented) is a well-known one, but it was patched in Express 4.18.0, so it only lands on older Express (4.17.3 and earlier). Prefer status and exposedHeaders, which remain reliable, and treat json spaces as a bonus signal on dated stacks.
{"__proto__":{"json spaces":9}}If subsequent res.json() responses come back indented by nine spaces, the pollution worked and the Express is pre-4.18.0.
Prefer a benign canary
Server-side pollution is process-wide and persists until restart, so destructive flips disrupt every user. The safest probe sets a harmless unique property and reads it back through any reflection:
pollute a meaningless unique prop -> look for it echoed in any response field, header, or error
no load-bearing default is touched -> minimal collateral damage, still a clean yes/noEdge variants
content-type / charset a polluted option that changes the response content type
parameterLimit a polluted option that changes parsing behaviour observably
constructor form {"constructor":{"prototype":{"status":510}}} when __proto__ is filteredFixing it
- The detection only works because the sink exists: close the pollution with safe merges and null-prototype config.
- Do not read framework options off plain objects that inherit from a pollutable prototype.
- Validate request bodies so a probe never reaches the merge.
Full guidance is in preventing prototype pollution. Next, defeat filters that strip the keys in bypassing flawed sanitisation, or try detection on a free lab.