Lion Cipher Lab
Exploit and fix server-side prototype pollution to RCE in a Node.js app
- AppSec / DevSecOps
- Advanced
- ~75 min
- Free · no account required
Why it matters
Server-side prototype pollution is the JavaScript-ecosystem bug that most often turns 'we merged some JSON' into 'an attacker runs commands as our Node process.' In JavaScript almost every object inherits from ONE shared object, Object.prototype: read a property an object does not have and the language walks its PROTOTYPE CHAIN -- a series of fallback objects -- until it finds the property or reaches Object.prototype at the top. The vulnerability (MITRE files it as CWE-1321) appears when untrusted input can reach the special keys __proto__ or constructor.prototype during an object operation,…
What you'll learn
- explaining SERVER-SIDE prototype pollution (SSPP, CWE-1321): untrusted JSON reaching the special key __proto__ (or constructor.prototype) during an object operation on a Node.js backend writes onto Object.prototype -- the object nearly every other object inherits from -- so a property the attacker plants is visible on EVERY plain object in the running process
- pinning the real SINK: proving that JSON.parse alone does NOT pollute (it hands you an object whose OWN key is literally '__proto__'), and that the vulnerability is the UNSAFE RECURSIVE MERGE that follows that key into Object.prototype
- detecting SSPP BLIND, server-side, with no value reflected back: polluting a property that a DIFFERENT endpoint reads off a fresh object and watching its behavior change -- the server-side analogue of PortSwigger's json-spaces / status-code tells (and why modern frameworks patched the framework-internal versions)
- finding a real GADGET and driving it to REMOTE CODE EXECUTION: polluting env.NODE_OPTIONS so a later child_process spawn of a Node worker loads an attacker --require module and runs code, the moment the app forwards a config object's env into the child
- the judgment of gadget availability: the SAME pollution is INERT without a reachable sink -- modern Node hardened the direct child_process option gadgets, so the chain only fires because the app itself forwards the polluted env into spawn
- seeing the naive fix fail live: blocking the literal string '__proto__' is bypassed by {"constructor":{"prototype":{...}}}, a second road to the same Object.prototype
- fixing at the ROOT versus defense-in-depth: reject __proto__/constructor/prototype and copy only own properties, build untrusted maps with Object.create(null) or a Map, and schema-validate input -- versus Object.freeze(Object.prototype) and the --disable-proto Node flag as belts-and-braces with real costs
Steps in this lab
- The vocabulary of SSPP -- prototype, __proto__, sink, gadget, in one breath
- Set up: Node.js, a clean folder, and Express
- Build the VULNERABLE app -- an unsafe merge, plus a gadget that spawns a worker
- The first named failure mode: JSON.parse is NOT the sink -- the merge is
- DETECT the pollution BLIND -- no value reflected, but behavior changes
- Find the GADGET and reach RCE -- NODE_OPTIONS=--require
- The second named failure mode: a gadget is environment-specific
- The third named failure mode: the '__proto__' string blocklist is bypassable
- FIX it at the ROOT, then re-test the exact same attacks
- What you did -- and the reflex to keep