Last reviewed: 2026-10-15
Direct answer
Prevent prototype pollution in coding-agent JavaScript patches by controlling which properties untrusted input may create, which objects receive them, and which inherited values downstream code will accept. Prefer a fixed schema and explicit field construction to a generic recursive merge. Use Map methods for arbitrary dictionaries, null-prototype records when an object is required, and own-property checks before reading security-sensitive fields. Reject an invalid input before applying any of it.
Blocking only __proto__ is incomplete. The MDN prototype pollution guide
explains that a dynamic path can also reach a prototype through constructor.prototype. It distinguishes a pollution source, which changes a prototype, from code that later consumes an inherited value. Review both ends: a harmless-looking options merge can change a request method or authorization decision elsewhere.
The OWASP prevention cheat sheet
recommends Map, Set, and null-prototype objects and explicitly describes Node’s --disable-proto=delete as defense in depth. That flag removes one entry point; it does not make a generic merge safe. The contract and test workflow below are recommendations for reviewing a patch, rather than a claim that one helper secures an entire application.
Who this is for
This guide is for maintainers reviewing agent-written JavaScript or TypeScript that parses configuration, combines request options, turns query parameters into objects, updates nested settings, or builds dictionaries from external names. Repository configuration, model-produced JSON, issue fields, tool results, and imported files can all supply untrusted keys. TypeScript types disappear at runtime, so a declared interface does not validate those inputs.
Start with the changed data flow. Identify where bytes become an object, where keys become property access, and where the resulting values affect behavior. Pair the review with the site’s security scan gate . A scanner can flag suspicious assignments, but the reviewer still needs to establish the intended keys and consumers.
Key takeaways
- A successful
JSON.parse()establishes syntax. It does not authorize keys or make later assignment safe. - Prefer explicit application fields. For arbitrary names, use
map.set()andmap.get(); bracket assignment on aMapcreates ordinary object properties. - A null-prototype root protects that record. It does not recursively convert nested objects or protect a later ordinary-object merge.
- Inspect every component of a dynamic path, including the final key, before mutation. An allowlist is easier to reason about than a growing list of forbidden spellings.
- Use
Object.hasOwn(record, key)when ownership matters. Neitherkey in recordnor a truthy lookup distinguishes inherited values. - Test the changed target’s prototype and downstream behavior as well as
Object.prototype; a merge can change only its target. - Run adversarial cases in disposable processes so a deliberately contaminated prototype cannot affect unrelated tests.
Sources checked
- MDN: JavaScript prototype pollution
, reached on 2026-10-15, supports dynamic-key sources,
constructor.prototypeaccess, JSON-versus-assignment behavior, own-property checks, defaults, null-prototype objects, andMap. Its displayed editorial modification date is 2025-10-17. It remains relevant as a maintained explanation of JavaScript semantics, not evidence of a new vulnerability announcement. - OWASP: Prototype Pollution Prevention Cheat Sheet
, reached on 2026-10-15, supports collection choices and the limits of freezing, sealing, and disabling
__proto__. The served page’s HTTPLast-Modifiedwas 2026-10-14; that is a representation timestamp, not a verified editorial event date. - MITRE: CWE-1321 , reached on 2026-10-15 as part of CWE 4.20, defines improperly controlled prototype modification and explains effects on attribute existence checks and built-in methods. It supports the weakness classification, rather than proving that a particular diff is exploitable.
- Node.js: Command-line API
, reached on 2026-10-15 as the v26.12.0 documentation, specifies the
deleteandthrowmodes of--disable-proto. These are runtime semantics; verify support and compatibility in the application’s actual Node version.
All four were live primary technical references when checked. Their stable semantics support an evergreen review task. The example’s application fields, input limit, rejection policy, and test matrix are deliberately chosen engineering recommendations.
Contract details to verify
Separate parsing, normalization, and use
Write the accepted shape before asking an agent to implement a fix. For a display-options endpoint, that might be exactly two optional fields: locale, from a short supported list, and compact, a boolean. Everything else is an error. The normalized output always owns both fields and contains no nested values. Authorization, filesystem paths, transport methods, and process options are outside that contract.
MDN notes that JSON.parse() treats __proto__ as an ordinary own property. The hazard appears when later code interprets it as an assignment or traversal instruction. For example, Object.assign({}, parsed) can invoke the target’s inherited setter and change that target’s prototype without changing Object.prototype. Conversely, object spread does not invoke that setter, but it still copies unapproved fields and is shallow. Neither operation establishes a schema.
For nested data, validate every object level with the appropriate schema and reject additional properties where the contract excludes them. A root-only check leaves nested records unchecked. If a generic path setter is unavoidable, validate the complete path before writing anything, reject prototype-reaching components under a documented policy, and traverse only intended own containers. Silently skipping a forbidden component changes the requested operation and can write somewhere else.
A deliberately narrow Node.js example
This function accepts JSON text for the flat display-options contract. It checks every input key before constructing a fresh null-prototype result, copies only approved scalar values, and supplies explicit defaults. Save it in a module appropriate for your project; it uses no third-party packages.
const allowedOptionKeys = new Set(["locale", "compact"]);
const supportedLocales = new Set(["en", "fr"]);
function parseDisplayOptions(text) {
if (
typeof text !== "string" ||
Buffer.byteLength(text, "utf8") > 4096
) {
throw new TypeError("options_input_invalid");
}
const raw = JSON.parse(text);
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
throw new TypeError("options_shape_invalid");
}
for (const key of Object.keys(raw)) {
if (!allowedOptionKeys.has(key)) {
throw new TypeError("options_key_denied");
}
}
const locale = Object.hasOwn(raw, "locale") ? raw.locale : "en";
const compact = Object.hasOwn(raw, "compact") ? raw.compact : false;
if (!supportedLocales.has(locale) || typeof compact !== "boolean") {
throw new TypeError("options_value_invalid");
}
const result = Object.create(null);
result.locale = locale;
result.compact = compact;
return result;
}
For {"locale":"fr","compact":true}, require a null prototype and exactly those two own values. For {}, require own defaults of "en" and false. Inputs containing __proto__, constructor, prototype, or any other unknown key must throw before a result is returned. Nested objects fail because the two accepted fields require scalar values.
The 4,096-byte check is an example application limit on an already decoded string. Enforce transport-body and decompression limits before allocating that string. This function is not a recursive sanitizer, a validator for arbitrary JavaScript objects, or an authorization mechanism. Do not broaden it by merging its input into a shared defaults object.
Make consumers reject inherited authority
Repairing a write path leaves other pollution sources possible. Check consumers independently. MDN recommends Object.hasOwn() before relying on property ownership and explicit defaults for expected fields. Object.keys() or Object.entries() avoids the inherited enumerable properties included by for...in.
An authorization decision should use an authenticated, normalized identity record and a deliberately defined boolean permission. Requiring an own property whose value is exactly true can stop an inherited permission from granting access. It does not establish who supplied that property: accepting an own isAdmin: true directly from request JSON remains an authorization bug. Keep caller-controlled options separate from identity and policy records.
Null-prototype records lack inherited methods. Call Object.hasOwn(record, key), rather than record.hasOwnProperty(key), which may be absent or attacker-controlled. If an API or library requires ordinary objects, construct its documented fields explicitly and inspect how it handles missing fields. Converting a safe dictionary back into an ordinary object can reintroduce unsafe consumers.
Happy-path operator workflow
- Inventory changed dynamic assignments, recursive merges, clone helpers, query parsers, and settings consumers, including library calls reached by the patch.
- Write the accepted keys, types, nesting rules, defaults, maximum input size, and rejection behavior. Identify each field’s authority and origin.
- Replace a generic merge with explicit construction where the feature permits it. Use
Mapmethods for open-ended dictionaries and null-prototype records for appropriate object interfaces. - Validate the whole input before applying changes. For a stateful operation, stage a complete validated result and commit it through the application’s normal transaction boundary.
- Add the adversarial cases below, covering both pollution sources and inherited-value consumers. Run them against the exact proposed revision.
- Review the final diff and test evidence together. Preserve the runtime version, dependency lock revision, test identifiers, rejection classes, and prototype assertions.
Error-path operator workflow
Map malformed JSON, invalid shape, forbidden keys, invalid values, and size violations to controlled failures at the boundary. Keep error records to a stable category and correlation identifier. A parser’s raw exception message or an unknown key can contain input data; do not echo it into central logs.
Discard the candidate result after any validation failure. Do not retry through a permissive parser, silently drop arbitrary keys, mutate the original defaults, or continue with a partially applied path. If a production process may already have a contaminated prototype, fix the source and replace the affected process through the normal operational procedure. Deleting one suspicious property is not evidence that all modified objects and inherited consumers are clean.
Prove the boundary with adversarial tests
Use synthetic payloads and fresh child processes. A test should assert the returned data, the relevant prototype identities, the absence of unrelated inherited properties, and any attempted downstream action. If demonstrating a vulnerable helper, isolate that demonstration from the fixed implementation’s tests.
| Case | Required observation |
|---|---|
| Valid display options and empty options | Exact own fields, explicit defaults, and a null prototype |
Parsed own __proto__ key | Controlled rejection; no returned candidate and no prototype change |
constructor or prototype key | Controlled rejection, including a nested-looking payload |
Generic path containing constructor, prototype, or a forbidden final key | Whole operation rejected before any existing state changes |
| Wrong scalar type, array, null, malformed JSON, or excessive bytes | Controlled failure without permissive fallback |
| A valid key followed by an invalid key | Existing application state remains unchanged |
| An isolated fixture gives a prototype a permission or request-method property | Consumer ignores inherited authority and uses the approved own value or denial default |
| A merge changes only its target’s prototype | Target-prototype assertion detects it even when Object.prototype is unchanged |
The suite runs with and without a supported --disable-proto mode | Core validation remains effective independently of runtime hardening |
When testing __proto__, construct the payload as JSON text and parse it. The JavaScript literal { __proto__: value } has special prototype-setting semantics, so it is not an equivalent fixture for an own JSON property. That difference can make a test pass without exercising the real input path.
The flat example cannot exercise a recursive path setter; test such a setter separately if the application retains one. Add generated key sequences only after the named cases establish the oracle. The site’s property-based testing guide explains how to preserve a bounded, reproducible failing example.
The exact snippet above was checked on Node.js v22.22.2 with 86 assertions, including 27 invalid inputs, isolated VM contexts with inherited contamination, and child processes using both --disable-proto modes. The checks also reproduced target-only prototype mutation by Object.assign and the different spread behavior. These results cover this helper and those synthetic fixtures. The fetched Node documentation describes v26.12.0; no application-wide or production test is claimed.
Failure modes
The patch filters only __proto__. Dynamic traversal through constructor.prototype can still reach a prototype. Prefer a fixed schema; otherwise validate every path component before mutation.
The output root has a null prototype, but nested children are ordinary objects. The protection is local. Review every created container and every later conversion or merge.
The test checks only for a marker on Object.prototype. A target can acquire a different prototype while the global prototype stays unchanged. Assert target identity and consumer behavior as well.
The agent changes Object.assign to spread and calls the problem solved. Spread avoids that setter invocation but preserves arbitrary fields and nested references. Validate and construct the application contract.
A Map is used with bracket notation. map[key] accesses object properties, rather than map entries. Require set, get, and has throughout that dictionary’s lifecycle.
Runtime hardening replaces input validation. The Node CLI reference
says delete removes Object.prototype.__proto__ and throw raises ERR_PROTO_ACCESS on access. Neither mode removes constructor.prototype. Test compatibility before enabling it, and keep application validation intact.
Freezing or sealing is treated as universal protection. OWASP notes that freezing is shallow and can break libraries; sealing leaves existing writable values mutable. Avoid changing built-in prototypes as an incidental patch fix. If the platform deliberately hardens them, review and test that as a separate runtime decision.
FAQ
Is prototype pollution the same as unsafe JSON parsing?
No. Valid JSON can contain an own property named __proto__. Pollution occurs when application behavior permits a prototype to be modified. Its effects appear when later code consumes inherited values. Trace parsing through every subsequent merge and consumer.
Does schema validation provide a complete fix?
Only for the shape and boundary it actually enforces. Check nested schemas, unknown-property rules, coercion, defaults, and downstream operations. MITRE’s CWE-1321 entry classifies schema validation as a limited mitigation; do not generalize one successful validator call to the whole process.
Should every object become a Map?
No. Use a Map when the feature is a dictionary with arbitrary keys. Fixed application records and third-party interfaces often need objects. Give those records explicit fields or a suitable null prototype, then verify what their consumers expect.
Does an own boolean permission prove a request is authorized?
No. It prevents one inherited-value mistake. The permission still needs to come from a trusted authentication and authorization path. A caller must not be able to grant itself that own property.
Reader next step
Choose one dynamic object-building path in the next coding-agent patch. Write its accepted shape and trace one resulting value to its consumer. Add named tests for an own JSON __proto__ key, a constructor.prototype path where applicable, inherited permission values, and a target-only prototype change. Approve the change only when the exact revision rejects invalid input before mutation and its consumers use the intended own values.