Last updated: May 1, 2026
Prototype pollution is a JavaScript-specific vulnerability where attacker-controlled input modifies the prototype of base objects (Object, Array). Once polluted, every object inherits attacker-controlled properties — leading to RCE in some Node.js apps, XSS in some browsers, authentication bypass in some apps. This module covers the mechanism, detection, and defenses.
JavaScript prototype basics
Every object in JavaScript has a prototype. Property lookups walk up the chain: object → its prototype → its prototype’s prototype → … → Object.prototype → null.
const a = {};
console.log(a.toString); // function (inherited from Object.prototype)
Object.prototype.foo = "polluted";
console.log({}.foo); // "polluted" — every new object has it
If attacker can write to Object.prototype, every object in the application gets new properties, retroactively.
How pollution happens
Common vulnerable pattern: deep-merge functions, Object.assign loops, query-string parsers that nest, JSON parsers that allow __proto__.
// Vulnerable deep merge (simplified)
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
const userInput = JSON.parse('{"__proto__":{"isAdmin":true}}');
merge({}, userInput);
// Now: ({}).isAdmin === true for ANY object created afterwards
Attack vectors
Authentication bypass
// App checks user.isAdmin somewhere
if (user.isAdmin) { /* admin actions */ }
// Pollute Object.prototype.isAdmin = true → every user is admin
Property injection on config objects
Apps initialize config from defaults, then merge user options. Pollution introduces unwanted config — e.g., cors.origin = "*", session.secret = "known".
RCE via gadget
Some libraries check options.X where X is a child-process invocation. Pollution sets options.shell = "/bin/sh" and options.argv0 = "evil". Subsequent child_process.spawn(...) uses polluted options.
Notable: prototype pollution + ejs/handlebars template engine = RCE chains documented in CVE-2021-25928 family.
XSS via DOM-based pollution (browser)
jQuery’s $.extend, lodash’s _.merge — vulnerable in older versions. Attacker manipulates URL hash or query string; client-side merge pollutes prototype; subsequent code uses polluted property.
Sources of pollution
- Recursive merge functions (lodash.merge before patch, lodash.set, jQuery.extend, Hoek)
- Query-string parsers that interpret
?a[b]=cas nested objects - JSON parsers that allow special property names
- YAML parsers (some)
- Application-specific deep-clone or default-merging logic
Detection
Black-box testing
# Send a request body containing __proto__ payload
curl -X POST -H "Content-Type: application/json" \
-d '{"__proto__":{"polluted":"yes"}}' \
https://target/api/setting
# Then probe a different endpoint that reflects an object property
curl https://target/api/diagnostic
# If response includes "polluted":"yes" — pollution succeeded
# Try variants:
# {"constructor": {"prototype": {"polluted":"yes"}}}
# {"a":{"__proto__":{"polluted":"yes"}}} (nested)
Static analysis
- Semgrep rules for unsafe merge patterns
- Snyk Code, CodeQL — JavaScript ruleset includes prototype pollution detectors
- Manual: grep for
for...inwith assignment, recursive merge functions
Runtime detection
Freeze Object.prototype at startup; any pollution attempt throws:
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
// ... per type ...
// Attempt to set Object.prototype.X now throws TypeError in strict mode
Defenses — at the code level
Use Object.create(null)
Objects created with no prototype have no prototype chain to pollute:
const config = Object.create(null);
config.x = 1;
// config.__proto__ === undefined; pollution doesn't help attacker
Use Map instead of Object for user-input data
Map has no prototype-chain semantics for keys. Iteration is explicit.
Validate keys before assignment
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue; // skip dangerous keys
}
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
Use vetted libraries
lodash.merge has been patched against pollution since 4.17.11. Use current versions. Set –use-strict and consider --frozen-intrinsics Node.js flag.
Schema validation
Validate input against a schema (Joi, Zod, Yup) before passing to merge logic. Schemas reject unexpected properties by default with strict mode.
Famous CVEs
- CVE-2018-3721 — lodash < 4.17.5
- CVE-2019-10744 — lodash 4.17.15 set-property issue
- CVE-2019-11358 — jQuery < 3.4.0 $.extend
- Several in mongoose, sequelize, parse-server, hapi/Hoek
- Various Node.js core mitigations added 16+; framework-level reactions
Severity calibration
- Critical: pollution + reachable gadget chain = RCE
- High: pollution + auth bypass; persistence in shared application state
- Medium: pollution that affects only a specific path; limited gadget
- Low: pollution discovered but no demonstrable impact in this application
Severity often depends on the gadgets present, which depends on the libraries in use. Pollution alone isn’t always exploitable.
Server vs client
- Server-side (Node.js): pollution affects all subsequent requests in the process; high impact in any process model that doesn’t isolate per-request
- Client-side (browser): pollution affects only the current page; persistence requires further hooks
Both are real; server-side is typically more severe.
Closing the advanced web sequence
This concludes the advanced web vulnerability modules added to the Web App Pentest track (M14 HTTP Smuggling, M15 Deserialization, M16 Race Conditions, M17 Prototype Pollution). The Web track now covers the OWASP Top 10 plus the modern advanced classes that distinguish a senior pentester from a checklist follower.
Module Quiz · 15 questions
Pass with 80%+ to mark this module complete. Unlimited retries. Each question shows an explanation.
Practical depth on what to tune, what to harden, and how this maps to Indian regulatory expectations.
Prototype pollution — what it is and why it escalates to RCE
Prototype pollution is a JavaScript-specific bug where attacker-controlled input merges into Object.prototype, affecting every object in the application. A typical sink is recursive merge or set-by-path in user-supplied JSON. The pollution itself is harmless (“set __proto__.admin = true on Object.prototype”) — but combined with downstream code that checks obj.admin, it becomes auth bypass. Combined with code that uses obj.shell in a child_process call, it becomes RCE.
The broad-impact realitya single pollution sink in a utility library affects every consumer transitively. Lodash, jQuery, jsonpath, set-value have all shipped pollution fixes.
Defences + India context
Detecting prototype pollution in CI / dependency review
Static analysis is the primary defence at scale. Tools:
Object.freeze(Object.prototype) at startup — any pollution attempt throws; works as a tripwire.__proto__ or constructor.prototype keys. Indian Node.js stack reality: Indian fintech and e-commerce Node.js back-ends are heavily affected because most rely on the npm dependency ecosystem with deep transitive trees. SBOM + dependency scanning in CI is now a baseline expectation under sectoral regulator scrutiny.Prototype pollution gadgets — what RCE chains look like
A pollution alone is not RCE; it becomes RCE when combined with code that uses the polluted property in a dangerous way. Common gadgets in popular libraries:
req.body.foo with default fallback — pollution defeats the fallback.env or shell.Performance discipline that strengthens security
A practical performance discipline that applies regardless of the specific vulnerability class is to instrument every endpoint with three SLOs: latency at the 50th, 95th, and 99th percentiles. The 50th percentile reflects typical user experience, the 95th catches tail behaviour visible to a meaningful fraction of users, and the 99th captures the long-tail outliers that often hide bugs (timeouts on a stale dependency, locking contention under load, garbage-collection pauses on a hot endpoint). Every release should compare these percentiles against the previous baseline; regressions of more than ten percent on the 95th percentile are typically worth investigating before shipping. Modern observability stacks make this routine — Datadog APM, New Relic, Honeycomb, Grafana Tempo with OpenTelemetry instrumentation, and on the open-source side Jaeger plus Prometheus plus Grafana — but the discipline matters more than the tool. Indian product teams that have adopted this percentile-driven culture report fewer surprise outages and a smaller incident-response load over time, which compounds into a stronger security posture: the same instrumentation that catches a slow endpoint also catches an exploitation attempt that forces unusual code paths.
Observability that catches attacks alongside outages
Observability for security overlaps with observability for reliability but adds a few specific signals worth instrumenting. First, application-layer authentication and authorisation events should produce structured logs with the user identifier, the action attempted, the source IP, the User-Agent fingerprint, and the outcome. Second, every state-changing API call should log its idempotency key, the request body shape, and the resulting object identifier. Third, every external dependency call should log the destination, the response code, and the latency. Together these three categories produce a clean event stream that a SIEM can correlate to detect both functional regressions and active attacks. The cost is moderate — log storage in Elasticsearch or Splunk runs perhaps thirty to sixty rupees per gigabyte per year at typical Indian-tier prices — and the payoff is dramatic: incidents that previously took days to investigate are typically resolved in hours when the relevant evidence has been pre-indexed. Indian regulators increasingly expect this level of operational logging, particularly RBI under the Cyber Security Framework Annex 1 and SEBI under the CSCRF master circular.
DPDP and sectoral obligations a practitioner cannot ignore
Under India’s Digital Personal Data Protection Act 2023, the operational obligations on any web application that handles personal data extend beyond simple consent collection. Section 8 requires that data fiduciaries implement reasonable security safeguards — interpreted in practice as reflecting a documented information security programme, regular vulnerability assessment, and demonstrable controls aligned to a recognised standard such as ISO 27001 or NIST CSF. Section 9 imposes data principal rights including access, correction, and erasure, which means the application must be architected so that these requests can be fulfilled within the prescribed timeframes (currently expected to be on the order of thirty days). Section 8(6) imposes breach notification: when a personal-data breach occurs, the data fiduciary must notify the Data Protection Board and affected data principals within prescribed timeframes (the draft Rules suggest seventy-two hours for material breaches). For the practitioner this translates into specific engineering tasks: data inventory and lineage tracking, retention controls and automated deletion at end-of-purpose, audit-grade logging of every access to personal data, breach-detection telemetry, and a formal incident-response playbook with regulatory-notification templates. The 2024-2025 inspections by RBI and SEBI of regulated entities have already cited DPDP-overlay obligations, so the alignment is not theoretical; it shapes what auditors expect to see in the next inspection cycle.
A small threat-modelling routine before every release
A short threat-modelling exercise specific to this vulnerability class is worth running before each release. The classic STRIDE framework — Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege — provides a useful checklist when applied to the specific data flow at hand. Begin by drawing the data flow for the feature being changed: external actors at the edge, the trust boundaries they cross, the back-end services involved, the data stores read or written, and the third-party integrations called. For each trust boundary crossing, ask which STRIDE categories apply and what controls mitigate them. The output is a small set of testable assertions: this endpoint must not allow tenant A to read tenant B’s records, this mutation must require step-up authentication, this third-party call must validate the response signature, and so on. Convert each assertion into an automated test or a manual verification step in the release checklist. The discipline pays off in two ways: first, it catches design-level bugs before they reach code review; second, it produces an auditable record of security thinking that regulators value. Indian product teams that have institutionalised this practice report fewer security incidents and shorter mean-time-to-remediate when issues are found, which compounds into better customer trust and lower regulatory friction over time.
Further reading
Additional FAQs
Is prototype pollution only a JavaScript issue?
Yes — specifically a JS prototype-chain semantic. Other languages have analogous issues (Python class-attribute pollution) but the practical attack class is JS-dominant.
How do I find prototype pollution sinks?
Static analysis (Semgrep, CodeQL) for recursive merge / set-by-path patterns; dynamic testing with crafted JSON containing __proto__; review of util library calls in source.
Custom team training + practitioner advisory
Beyond the free academy — we run private workshops, vCISO advisory, and red-team exercises tailored to your stack. For Indian SMBs scaling past their first hire.