AppSec · Jul 1, 2026 · 6 min read

Prototype pollution: using a property oracle to confirm without crashing the target

Injecting a uniquely named property and observing it appear in a second, unrelated request is the confirming artifact - no gadget chain runs during the probe.

Prototype pollution is the JavaScript bug where writing to one object quietly rewrites the defaults for all of them. An attacker-controlled key reaches Object.prototype, and suddenly every object in the process inherits a property it never set. Confirming it does not require running a gadget or crashing anything - a property oracle shows the pollution took, and a second, unrelated request proves it stuck.

01A controllable key reaches the prototype

The usual entry points are recursive merge utilities - lodash.merge, jQuery.extend, defaults-deep - that copy attacker JSON into a target object key by key. If the input contains __proto__ (or constructor.prototype) and the merge does not guard against it, the write lands on the shared prototype instead of the instance.

input {"__proto__": {"isAdmin": true}} ──► recursive merge write Object.prototype.isAdmin = true (shared by every object) later ({}).isAdmin ──► true an unrelated object now inherits it
The merge intended to set a property on one object; it set a default on all of them.

02The property oracle confirms without a gadget

Confirmation uses a benign marker, not an exploit. Send a request whose merge path carries a uniquely named property on __proto__, then make a second, unrelated request and check whether that property is now present where it has no business being. Its appearance is the confirming artifact; no privileged flag is set, no gadget runs.

proto-oracle.httphttp
# 1) pollute with a uniquely named, harmless property
POST /api/profile  {"__proto__": {"_apNonce_62615533": true}}

# 2) a DIFFERENT request whose response reflects object defaults
GET /api/settings
200 {... "_apNonce_62615533": true ...}  # the nonce leaked across requests == confirmed
Proven - HighPrototype pollution via recursive merge - /api/profile
POST /api/profile {"__proto__":{"_apNonce_62615533":true}} GET /api/settings (unrelated handler) ──► response carries _apNonce_62615533: true - pollution persisted across requests
Benign nonce property, no privilege flag set, no gadget executed. Rated High; escalates to Critical where the polluted key feeds an authorization check.

03Severity is the sink the property reaches

Pollution on its own is a primitive; the number comes from what reads the polluted property in the running app. A property that flips a boolean checked by an authorization function is privilege escalation. One that lands in a template option or a command-builder default can reach XSS or command execution. One that nothing sensitive ever reads is low. We rate the confirmed sink, not the write.

Polluted property reachesConsequenceRating
An authorization booleanPrivilege escalationCritical
A template / render optionServer-side or DOM XSSHigh - Critical
A child-process defaultArgument or command injectionCritical
Nothing security-relevantInert default changeLow

04The probe changes with the runtime

Server-side pollution in Node must surface in an HTTP response or a measurable side channel - a debug endpoint, a serialized default, a timing shift - because there is no DOM to read. Client-side pollution shows up in the page. The engine picks the oracle that fits the runtime rather than assuming a browser is present.

__proto__
the key that escapes the instance
2
requests: pollute, then observe
0
privilege flags set to confirm

Freeze prototypes, reject __proto__ keys at the merge boundary, or use a null-prototype target. The confirming nonce disappears the moment the merge stops walking into the prototype.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →