Lion Cipher Lab
Exploit and fix Prototype Pollution in your own Node.js app
- AppSec / DevSecOps
- Intermediate
- ~45 min
- Free · no account required
Why it matters
Prototype pollution is the signature server-side bug of the JavaScript ecosystem, and it hides in code that looks completely ordinary. In JavaScript, almost every object inherits from ONE shared object called Object.prototype: when you read a property that an object does not have, the language walks that object's PROTOTYPE CHAIN -- a series of fallback objects -- until it finds the property or reaches the top, which is Object.prototype. That design is why {}.toString() works even though you never defined toString. The vulnerability (MITRE files it as CWE-1321) appears when untrusted input can…
What you'll learn
- explaining Prototype Pollution (CWE-1321): in JavaScript almost every object inherits from a single shared object, Object.prototype, via its PROTOTYPE CHAIN; if untrusted input can write to the special keys __proto__ or constructor.prototype, it writes onto that shared object, so a property it never had appears on EVERY object in the program
- defining the vocabulary precisely: a PROTOTYPE is the fallback object JavaScript consults when a property is missing; the PROTOTYPE CHAIN is the series of those fallbacks ending at Object.prototype then null; __proto__ is the accessor that reads/writes an object's prototype link; a MERGE recursively copies a source object's keys into a target
- building a tiny, ZERO-dependency vulnerable app -- a plain Node.js http server that recursively merges untrusted JSON into a fresh object -- and seeing why the merge, not the framework, is the bug
- PROVING pollution with a curl POST of {"__proto__":{"polluted":"yes"}} and watching a brand-new empty object {} report polluted:"yes" -- proof the property landed on the shared prototype, not on your object
- ESCALATING to real impact on localhost: POSTing {"__proto__":{"isAdmin":true}} so a /admin route that denies access by the ABSENCE of an isAdmin flag flips from 403 Forbidden to the admin panel -- an authorization bypass with no credentials
- judgment: why the naive fix (block the literal key "__proto__") is defeated live by {"constructor":{"prototype":{...}}}, since constructor.prototype is a second path to the same Object.prototype -- and why blocklisting keys is inherently fragile
- FIXING it durably: reject __proto__/constructor/prototype keys AND only copy own properties, prefer Object.create(null) or a Map for untrusted maps, and -- the real answer -- validate input against a schema so unexpected keys never reach the merge; understanding why Object.freeze(Object.prototype) is defense-in-depth that can break some libraries
- distinguishing prototype pollution from ordinary injection: it does not corrupt one query or one page -- it poisons a language-level object shared by the whole process, so the damage surfaces far from the input, in code that never touched the request
Steps in this lab
- What prototype pollution is -- the vocabulary, in one breath
- Set up: confirm Node.js and make a clean folder
- Build the VULNERABLE app (an unsafe recursive merge of untrusted JSON)
- PROVE pollution: a fresh {} carries a property you never gave it
- ESCALATE (benign): flip an auth check from 403 to admin with no password
- The NAIVE fix -- and watch it fail live
- FIX it durably, then re-test
- What you did -- and the reflex to keep