This article explains prototype pollution, a JavaScript-specific vulnerability where unguarded merge functions allow attackers to write properties onto Object.prototype, affecting every object in an application. The lab demonstrates the attack through ProfileApp, where a user without admin privileges can become admin by polluting the prototype to set isAdmin to true. The attack works because JavaScript property lookups walk up the prototype chain, making this a critical class of bug affecting both client-side and server-side code.
- How prototype pollution exploits the property lookup chainJavaScript resolves properties by checking the object first, then its prototype, then Object.prototype. In the lab, the user object has no isAdmin property, so the admin check would normally return undefined. By polluting Object.prototype with isAdmin: true through an unguarded merge, every object in the app—including the attacker's—now inherits that property, causing the check to pass.
- The unguarded recursive merge vulnerabilityProfileApp's merge function copies JSON settings into the user object recursively without filtering dangerous key names. When the settings contain a nested __proto__ key, the merge function follows it directly onto Object.prototype instead of onto the attacker's own object, bypassing the intended isolation of user data.
- Attack payload and execution mechanismThe attacker submits settings as nested JSON with a structure containing __proto__ and a nested isAdmin: true property. The recursive merge processes this structure and writes isAdmin onto Object.prototype rather than the attacker's record. When the admin view checks user.isAdmin, it finds the property through prototype inheritance rather than on the user's own object.
- Real-world impact and documented vulnerabilitiesPrototype pollution has affected major libraries including Lodash (CVE-2019-10744), jQuery, and minimist. On client-side code it can lead to DOM XSS; on Node.js servers it can corrupt application logic, bypass security checks, enable denial of service, or facilitate further attacks.
- Defensive strategy: key filtering and alternative structuresDefense requires rejecting the special keys __proto__, constructor, and prototype in any merge or deep-copy of untrusted input. Additional protections include creating lookup objects with Object.create(null) to eliminate prototype inheritance, using Map instead of plain objects for user-driven key/value data, and keeping merge libraries patched with versions hardened against this vulnerability.
- Why __proto__ and constructor keys are dangerousThe article identifies __proto__, constructor, and prototype as dangerous keys because they directly reference or modify an object's prototype chain. If a merge function copies these keys literally from user input without filtering, it allows attackers to write arbitrary properties onto Object.prototype or manipulate the constructor chain.
Bottom line: Prototype pollution occurs when recursive merge or copy functions fail to filter the special keys __proto__, constructor, and prototype, allowing attackers to modify Object.prototype and inject properties inherited by all objects in the application. Defending against it requires explicit key filtering, using Object.create(null) or Map for user-controlled data, and maintaining patched versions of merge libraries.
isAdmin field — but the admin check still reads user.isAdmin. In JavaScript, if an object does not have a property, the lookup walks up the prototype chain. Pollute the prototype and the check passes for everyone.📋 In this walkthrough
What this lab teaches
Prototype pollution is a JavaScript-specific bug. A recursive merge or deep-copy routine copies attacker-controlled keys into an object, and if it does not guard against the special key __proto__, the attacker can write properties onto Object.prototype — the object every other object inherits from.
The lesson: in JavaScript, missing-property lookups fall through to the prototype. Change the prototype and you change the default answer for every object in the app at once.
How ProfileApp saves your settings
You submit settings as JSON and a merge function copies each key into your user record. It recurses into nested objects without checking for dangerous key names, so a nested __proto__ key is followed straight onto the shared prototype rather than onto your own object.
The attack approach
- Notice settings are saved as nested JSON and merged into the user object.
- Recall that the admin gate checks user.isAdmin, and your object has no such property.
- Send a settings object whose nested structure targets __proto__ and sets isAdmin to true.
- Because the merge is recursive and unguarded, the write lands on Object.prototype — so every object, including yours, now answers true.
Full walkthrough (spoiler)
If the profile still shows you as a normal user, open the steps.
Show the step-by-step solution
- In the settings JSON, add a nested object keyed
__proto__containingisAdmin: true. - Save it. The recursive merge follows the __proto__ key and writes isAdmin onto Object.prototype instead of onto your record.
- Open View Profile. The check
user.isAdmin === truenow passes, because the lookup falls through your (flagless) object to the polluted prototype. - The lab detects the admin view and marks you solved.
Why it works
JavaScript resolves user.isAdmin by checking your object, then its prototype, then Object.prototype. Your object has no such key, so normally the answer is undefined. By writing to the prototype through an unguarded merge, you set the default value every object inherits — so the admin check passes without your record ever gaining the field.
Where this happens in the real world
Prototype pollution has hit major libraries — Lodash carried a documented prototype-pollution flaw (CVE-2019-10744) that affected a huge number of dependent projects, and jQuery, minimist and others have shipped the same class of bug. On the client it can lead to DOM XSS; on Node.js servers it can corrupt application logic or enable further attacks.
How to defend against it
- Block dangerous keys (
__proto__,constructor,prototype) in every merge, assign and deep-copy of user input. - Create lookup objects with Object.create(null) so they have no prototype to pollute.
- Use Map instead of plain objects for key/value data driven by user input.
- Keep merge libraries patched, and prefer ones that are hardened against this by default.
Try it, then go further
Reading about it is the easy part. Open the lab, try your own approach before peeking at the spoiler, and see how many attempts it takes.
