In JavaScript, a single write to a shared prototype changes the behaviour of every object in the process, including objects the attacker never touched. Prototype pollution, catalogued as CWE-1321, turns ordinary code patterns such as recursive merges, deep defaults and path-based setters into a way to inject properties that other code later trusts. It has affected some of the most widely deployed libraries, including jQuery before 3.4.0 and lodash before 4.17.12, and in 2026 the CISA Known Exploited Vulnerabilities (KEV) catalogue added a prototype pollution flaw in Adobe Acrobat and Reader. This guide explains the mechanism conceptually, where it appears in browser and Node.js code, how to detect it, and how to design objects that cannot be polluted. It is for JavaScript and TypeScript developers, application security engineers and code reviewers, and contains no exploit strings.
Key takeaways
- It is a two-phase bug. MDN describes a pollution step, where input adds a property to a prototype, and an exploitation step, where unrelated code reads that property and misbehaves.
- Unsafe object handling is the source. Recursive merge, clone, deep-defaults and path setters that copy attacker-chosen keys are the classic entry points, as the CWE-1321 example CVEs show.
- Null-prototype objects and Map remove the problem by design. CWE-1321 rates
Object.create(null)as highly effective andMapas moderately effective; OWASP ASVS 5.0 V15.3.6 asks for Set and Map over object literals. - Key blocklists are a fallback, not a fix. Blocking
__proto__alone missesconstructor.prototype; MDN and OWASP both say no single defence is sufficient. - Dependencies carry most of the risk. Much real exposure comes from vulnerable utility libraries, so software composition analysis and fast upgrades matter as much as your own code.
How the attack works
When JavaScript looks up a property that an object does not have, it walks the prototype chain up to Object.prototype. Prototypes are ordinary, mutable objects at runtime. CWE-1321 defines the weakness as accepting input that sets or updates object attributes without properly controlling modifications to the object prototype. MITRE places it under CWE-915 (improperly controlled modification of dynamically determined object attributes) and notes it can precede CWE-471, modification of assumed-immutable data.
- Pollution: code that assigns properties using keys derived from input, in the pattern MDN summarises as nested dynamic assignment, can be steered to write to a prototype instead of to the intended object. Two paths matter: the
__proto__accessor, and theconstructor.prototyperoute, which still works when the accessor is disabled. - Exploitation: application or library code later reads a property it expects to be absent, such as an option, a flag or a configuration value, and receives the polluted value through inheritance. MDN calls this a data-only attack: the attacker never runs code directly, they supply data that the application processes.
- JSON.parse is not the culprit on its own: MDN explains that JSON treats
__proto__as an ordinary key, so parsing is harmless, but merging the parsed result withObject.assign()or afor...incopy loop invokes the setter. Object spread syntax does not invoke setters. - Gadgets: the code that turns a polluted property into impact. Without a gadget, pollution may only cause errors; with one, it can bypass checks or reach script execution.
Where it shows up
| Source pattern | Typical location | Known examples |
|---|---|---|
| Recursive merge or extend | Configuration loaders, option handling, state management | CVE-2019-11358: jQuery before 3.4.0, jQuery.extend(true, ...); CVE-2018-3721 (merge, per CWE-1321) |
| Recursive default setting | Utility libraries filling in missing options | CVE-2019-10744: lodash before 4.17.12, defaultsDeep |
| Dot or bracket path setters | Form handling, query string parsers that build nested objects, ORMs | CVE-2020-8203 (dot-separated path, per CWE-1321) |
| Query string and hash parsing in the browser | Single-page apps parsing location.search into deep objects | MDN notes libraries are more exposed than apps because they cannot allowlist keys |
| Copying records by field name | Back-end handlers copying database or request fields onto objects | MDN server-side example |
| Embedded JavaScript engines | Document readers and desktop products that run scripts | CVE-2026-34621, Adobe Acrobat and Reader (KEV, added 13 April 2026) |
Client-side vs server-side
- Client-side: MDN lists configuration objects passed to browser APIs as gadgets, for example
fetch()options whose method or body can be altered, iframesrcdochandling and HTML sanitiser configuration. The usual outcome is DOM-based cross-site scripting. See the Cross-Site Scripting (XSS) Prevention Guide and the Client-Side Security Guide. - Server-side (Node.js): the whole process shares one
Object.prototype, so a pollution in one request can affect every later request until the process restarts. Gadgets include authorisation checks that treat a missing property as false, and options passed to template engines or child process helpers. See the Node.js Security Guide.
Impact
| Impact | How it arises | Severity driver |
|---|---|---|
| Authorisation bypass | A check such as "is this user an admin" falls through to an inherited polluted value | Whether access decisions read optional properties without own-property checks |
| Data tampering | CWE-1321: attributes added or replaced on every object, including hasOwnProperty, toString or valueOf | How widely the polluted name is read |
| Denial of service | CWE-1321: overriding attributes with incompatible types crashes the application | Long-lived Node.js processes amplify a single request |
| Script execution in the browser | A gadget writes a polluted value into HTML, a sanitiser setting or a script source | Presence of DOM gadgets; Content Security Policy strength |
| Code execution on the server | A gadget passes polluted options to code that spawns processes or compiles templates | Library gadgets present in the dependency tree |
NVD scores reflect this range: 6.1 for the jQuery issue (user interaction, scope change) and 9.1 for the lodash issue.
Detection
- Static analysis: CWE-1321 rates SAST as highly effective. Use taint rules that trace request data, URL parameters and parsed JSON into recursive merge, assignment-by-path and computed property writes.
- Software composition analysis: scan lockfiles for packages with CWE-1321 advisories and upgrade promptly; many fixes are minor version bumps. See the Software Supply Chain Security Guide.
- Code review cues: computed property writes with two levels of input-derived keys, home-grown deep merge or clone helpers,
for...inloops without own-property checks, and authorisation reads of optional fields. - Dynamic testing: include prototype pollution checks for query string, JSON body and hash fragment handling in your DAST and penetration test scope. See the Web Application Security Testing Guide.
- Runtime signals: in Node.js,
--disable-proto=throwmakes any__proto__access raiseERR_PROTO_ACCESS, which turns attempts into loggable errors; watch for spikes in that error code and for unexplained crashes across many requests.
Metrics
- Dependency exposure: number of services with a known CWE-1321 vulnerable package, and median days to upgrade.
- Unsafe helper count: number of custom deep merge, clone or path-set helpers not using null-prototype targets or key validation.
- Schema coverage: percentage of API endpoints validating bodies with a schema that rejects additional properties.
- SAST findings: open CWE-1321 findings and their age.
- Runtime hardening: percentage of Node.js services started with
--disable-proto.
Prevention and hardening
Controls compared
| Control | Effect | Limits (per CWE-1321, OWASP and MDN) |
|---|---|---|
Map and Set | Lookups return only stored entries, so polluted prototypes cannot leak in | Only for key-value stores; APIs such as fetch() still need plain objects |
Object.create(null) or { __proto__: null } literal | No inherited prototype, so nothing to pollute and nothing inherited | Must be chosen deliberately; some APIs expect normal objects; the obj.__proto__ accessor is not safe |
Schema validation (for example Ajv or Zod with additionalProperties: false) | Rejects unexpected keys at the boundary | CWE-1321 rates effectiveness as limited on its own; dynamic assignment still needs key checks |
| Key validation | Reject keys that resolve to the prototype: __proto__, constructor, prototype | Blocklists are a fallback; prefer allowlists of expected keys |
Object.hasOwn() and Object.keys() | Read or iterate own properties only | Must be applied at every access site; for...in still walks the chain |
Object.freeze(Object.prototype) or SES lockdown | Built-in prototypes cannot be changed | Shallow; polyfills must load first; failed writes are silent in sloppy mode; test library compatibility |
Node.js --disable-proto=delete|throw | Removes or traps the __proto__ accessor (added v12.17.0 and v13.12.0) | Node-only, defence in depth; constructor.prototype remains reachable |
Decision guidance
- Library authors: you cannot allowlist your callers' keys, so build merge and set targets with null prototypes, skip prototype-resolving keys, and avoid relying on users freezing globals. CWE-1321 recommends approaches other than freezing for libraries.
- Application teams: validate at the boundary with strict schemas, use
Mapfor dictionaries keyed by user data, and replace custom deep merge helpers with maintained, patched ones. - Freezing: best suited to Node.js services and controlled front ends where you own the load order. Pilot it, because some libraries modify built-in prototypes and will break.
- Small teams: start with dependency upgrades, a lint rule against custom deep merges and
--disable-proto=throw. Large organisations should add SAST taint rules and a shared safe-object utility package. - TypeScript: types are erased at runtime and do not stop pollution; runtime validation is still required.
Rollout plan
- Patch dependencies: run SCA across all repositories and upgrade packages with CWE-1321 advisories, starting with internet-facing services.
- Find sources: search for custom merge, clone, defaults and path-set helpers, and for query parsers configured to build nested objects.
- Fix sources: switch targets to null-prototype objects or
Map, add key allowlists, and add unit tests asserting that prototype-resolving keys do not changeObject.prototype. - Harden the boundary: add strict schema validation to request bodies.
- Add runtime controls in audit first: start Node.js services in staging with
--disable-proto=throwand logERR_PROTO_ACCESS; fix legitimate uses, then promote to production. Roll back by removing the flag from the start command if a dependency fails. - Pilot freezing on one service, then expand; keep the change behind a start-up flag so it can be disabled quickly.
- Lock in: make SAST and SCA findings blocking in CI. See the Secure Coding Guide.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| CWE | CWE-1321 (child of CWE-915 and CWE-913) | Weakness definition |
| OWASP ASVS 5.0 | V15.3.6 | JavaScript written to prevent prototype pollution, for example with Set or Map |
| MITRE CAPEC | CAPEC-1, CAPEC-77, CAPEC-180 (listed by CWE-1321) | Related attack patterns |
| MITRE ATT&CK | T1190 Exploit Public-Facing Application; T1059.007 JavaScript | Exploitation of internet-facing services; script execution outcomes |
| NIST SP 800-53 Rev. 5 | SI-10, SA-11, SI-2, RA-5, SR-3 | Input validation, developer testing, flaw remediation, scanning, supply chain controls |
| NIST CSF 2.0 | PR.PS-06, ID.RA-01 | Secure development practices; vulnerability identification |
Common failure patterns
- Blocking only
__proto__: theconstructor.prototypepath remains open, as both MDN and OWASP point out. - Home-grown deep merge: a short helper copied from a forum copies every key recursively into a plain object.
- Stale transitive dependencies: the direct dependency was upgraded but an old copy of a vulnerable utility library remains deeper in the tree.
- Authorisation reading optional fields: access checks that treat "not present" as "false" without
Object.hasOwn()become gadgets. - Relying on TypeScript types or JSON.parse safety: neither stops the merge step that actually pollutes.
- Freezing after polyfills fail: freezing too early breaks polyfills, and teams remove the freeze entirely rather than fixing load order.
Practitioner checklist
- Run SCA on every JavaScript repository and upgrade packages with CWE-1321 advisories, including transitive ones.
- Replace custom deep merge, clone and path-set helpers with patched libraries or null-prototype implementations.
- Use
MapandSetfor collections keyed by user data, per ASVS V15.3.6. - Create dictionaries and merge targets with
Object.create(null). - Validate request bodies with schemas that reject additional properties, and allowlist keys in dynamic assignment.
- Use
Object.hasOwn()in authorisation and configuration reads. - Start Node.js services with
--disable-proto=throwafter an audit period, and monitorERR_PROTO_ACCESS. - Pilot freezing built-in prototypes where you control load order.
- Add SAST taint rules for CWE-1321 and unit tests for every merge helper.
Standards and references
- MITRE: CWE-1321 Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
- OWASP: Prototype Pollution Prevention Cheat Sheet
- MDN Web Docs: JavaScript prototype pollution
- OWASP: Application Security Verification Standard 5.0
- Node.js: Command-line API
- NIST NVD: CVE-2019-11358
- NIST NVD: CVE-2019-10744
- CISA: Known Exploited Vulnerabilities Catalog
Related NHI Mgmt Group resources: Web Application Attacks Guide · Node.js Security Guide · Malicious Package and Dependency Confusion Guide · Insecure Deserialisation Guide