FlawOpen

flawopen.com/Vulnerabilities/JavaScript Prototype Pollution

JavaScript Prototype Pollution

High Severity CWE-1321 Supply Chain & Runtime
ELI5 — A Shared Blueprint

Imagine a workshop where each new device checks a shared rulebook only when its own settings do not answer a question. If an unsafe parts clerk writes an attacker-controlled rule into that book, unrelated devices may read it as a default. Prototype pollution is the software version: unsafe dynamic property writes can alter an object's prototype, sometimes Object.prototype. The effect is not automatically global, and impact requires application code that later trusts the inherited property.

Target: Server-side or browser JavaScript that merges untrusted keys or writes attacker-controlled property paths
Vector: Untrusted keys such as __proto__ or a constructor.prototype path reaching an unsafe write
Impact: Depends on a reachable gadget: logic changes, denial of service, XSS, or in some server-side cases code execution; none is automatic
Remediation: Validate against an allowlist/schema; use Map or null-prototype dictionaries for untrusted keys

Core Concepts & Subsystem Terms

Object.prototype
The shared prototype inherited by ordinary objects. Pollution of this object can affect many objects, but not every JavaScript object or every runtime realm.
__proto__
A legacy accessor on ordinary objects that gets or sets their prototype. In JSON text, the same spelling is an ordinary property name until later code handles it unsafely.
Recursive Deep Merge
A copier that walks nested keys and writes them into another object. It becomes dangerous when untrusted keys can select prototype-related properties or unsafe paths.
CWE-1321
Improperly Controlled Modification of Object Prototype Attributes. The weakness is the uncontrolled prototype mutation; downstream impact depends on reachable application behavior.

Step-by-Step Attack Flow

Step 1

Untrusted Property Input

An endpoint accepts attacker-controlled object keys. Parsing JSON with an own __proto__ property alone does not mutate a prototype.

Step 2

Unsafe Dynamic Write

A recursive merge or path setter follows those keys and performs dynamic writes without validating each path segment.

Step 3

Target or Shared Prototype Changes

Depending on the target and write sequence, the operation changes a target's prototype or reaches a shared prototype such as Object.prototype. These outcomes are distinct.

Step 4

A Gadget Determines Impact

A later gadget reads the inherited value in a security-sensitive operation. The result depends on that gadget; authentication bypass or code execution must not be assumed.

The Mechanism & Root Cause

JavaScript property lookup follows an object's prototype chain. A vulnerable merge or path setter can let untrusted keys such as __proto__ or constructor.prototype steer a write into a prototype. An own __proto__ field produced by JSON.parse is not itself pollution; later code must interpret it through an unsafe assignment. A changed target prototype affects that target, while modifying Object.prototype affects ordinary objects that inherit from it. A separate gadget must consume the inherited property before there is security impact.

utils.js (Vulnerable Recursive Merge)Vulnerable
// VULNERABLE: attacker-controlled keys reach recursive writes
function merge(target, source) {
    for (const key in source) {
        const value = source[key];
        if (value && typeof value === 'object') {
            if (!target[key]) target[key] = {};
            merge(target[key], value);
        } else {
            target[key] = value;
        }
    }
    return target;
}
// JSON.parse creates an own __proto__ data property; the unsafe merge is the risk.
const input = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, input);
utils.js (Hardened Safe Merge)Hardened
// HARDENED: copy only fields this endpoint intentionally accepts
const PROFILE_KEYS = new Set(['displayName', 'theme']);

function parseProfile(input) {
    if (!input || typeof input !== 'object' || Array.isArray(input)) {
        throw new TypeError('profile must be an object');
    }

    const profile = Object.create(null);
    for (const key of Object.keys(input)) {
        if (!PROFILE_KEYS.has(key)) continue;
        const value = input[key];
        if (typeof value !== 'string' || value.length > 80) {
            throw new TypeError('invalid profile field');
        }
        profile[key] = value;
    }
    return profile;
}

The Attack & Exploit Sequence

Defensive Engineering & Prevention Rules

FAQ

Does JSON.parse() pollute the prototype by itself?

No. JSON.parse() creates an own data property named __proto__; it does not invoke the inherited setter. Risk arises if a later merge or assignment operation invokes that setter. See whether JSON.parse() causes prototype pollution.

Does Object.freeze(Object.prototype) fix it?

It can prevent writes to the frozen Object.prototype itself, but it does not stop a setter from changing an individual object’s prototype or protect every other prototype. It may also break code that extends built-ins. Use it only as defense in depth, not as the data-handling fix.

Is TypeScript protected?

No. TypeScript types are erased at runtime. A value declared as Record<string, string> can still contain a __proto__ key at runtime; validate untrusted keys and use safe object construction.

Explore related security topics and post-mortems:Complete Security Directory →