Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')

Description

Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') occurs when a product receives input specifying object attributes but does not properly control modifications of attributes of the object prototype. Attackers can inject malicious attributes via __proto__, constructor, or prototype properties to modify every object instance in the application or override critical attributes, potentially leading to denial of service, data corruption, or arbitrary code execution.

Risk

Prototype pollution has severe implications. All object instances affected. Security checks bypassed. Application data readable and modifiable. Denial of service through crashes. Remote code execution in some contexts. Authentication bypass possible. Authorization checks circumvented. Template injection enabled. Configuration tampering. High likelihood in JavaScript applications accepting nested object input.

Solution

Freeze prototypes using Object.freeze(Object.prototype) to prevent modifications. Block dangerous attributes by filtering __proto__, constructor, and prototype from user input. Use prototypeless objects via Object.create(null) which eliminates prototype access. Substitute object attribute access with Map methods. Validate untrusted objects against defined schemas before processing.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Attackers can read application data through modified prototype methods.
IntegrityScope: Integrity

Application data modifiable by injecting prototype attributes.
AvailabilityScope: Availability

Overriding attributes with incompatible types can cause application crashes.

Example Code

Vulnerable Code

// Vulnerable: Recursive object merge without prototype protection

// VULNERABLE: Deep merge allows prototype pollution
function vulnerableMerge(target, source) {
    for (let key in source) {
        if (source.hasOwnProperty(key)) {
            if (typeof source[key] === 'object' && source[key] !== null) {
                if (!target[key]) {
                    target[key] = {};
                }
                // VULNERABLE: Recursively merges without checking key
                vulnerableMerge(target[key], source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

// Attack scenario
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
const config = {};
vulnerableMerge(config, userInput);

// Now ALL objects have isAdmin: true
const newObj = {};
console.log(newObj.isAdmin);  // true - prototype polluted!

// VULNERABLE: Property access via path
function vulnerableSetProperty(obj, path, value) {
    const parts = path.split('.');
    let current = obj;

    for (let i = 0; i < parts.length - 1; i++) {
        if (!current[parts[i]]) {
            current[parts[i]] = {};
        }
        // VULNERABLE: Allows __proto__ traversal
        current = current[parts[i]];
    }

    current[parts[parts.length - 1]] = value;
}

// Attack
vulnerableSetProperty({}, '__proto__.polluted', 'yes');
console.log({}.polluted);  // 'yes'

// VULNERABLE: Using user input directly as object key
function vulnerableUserConfig(userId, key, value) {
    const userSettings = {};
    // VULNERABLE: No validation of key
    userSettings[key] = value;
    return userSettings;
}

// Attack: Create object with polluted prototype
vulnerableUserConfig('123', '__proto__', { admin: true });
// Vulnerable: Express.js route handler

const express = require('express');
const app = express();

app.use(express.json());

// VULNERABLE: Query string parsing
app.get('/api/search', (req, res) => {
    const options = {};

    // VULNERABLE: Direct assignment from query params
    for (let key in req.query) {
        // This allows __proto__[isAdmin]=true in query string
        options[key] = req.query[key];
    }

    // Later code may check options.isAdmin
    if (options.isAdmin) {
        // Privileged operation
    }
});

// VULNERABLE: Body parsing with nested objects
app.post('/api/config', (req, res) => {
    const config = {};

    // VULNERABLE: Deep merge with user input
    vulnerableMerge(config, req.body);

    // If body is {"__proto__": {"isAdmin": true}}
    // All objects now have isAdmin: true
});

// VULNERABLE: Template rendering with polluted prototype
app.get('/page', (req, res) => {
    const data = {};
    vulnerableMerge(data, req.query);

    // Template engine may use Object.prototype properties
    res.render('page', data);
    // Pollution can inject into template context
});

Fixed Code

// Fixed: Safe object merge with prototype pollution protection

// FIXED: Dangerous keys that should never be set
const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'];

function isDangerousKey(key) {
    return DANGEROUS_KEYS.includes(key);
}

// FIXED: Safe deep merge
function safeMerge(target, source) {
    if (!source || typeof source !== 'object') {
        return target;
    }

    // FIXED: Use Object.keys to avoid prototype properties
    for (const key of Object.keys(source)) {
        // FIXED: Block dangerous keys
        if (isDangerousKey(key)) {
            continue;  // Skip dangerous keys
        }

        if (typeof source[key] === 'object' &&
            source[key] !== null &&
            !Array.isArray(source[key])) {

            if (!target[key] || typeof target[key] !== 'object') {
                target[key] = {};
            }
            safeMerge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

// FIXED: Safe property setter
function safeSetProperty(obj, path, value) {
    const parts = path.split('.');

    // FIXED: Validate all path parts
    for (const part of parts) {
        if (isDangerousKey(part)) {
            throw new Error(`Invalid property path: ${path}`);
        }
    }

    let current = obj;
    for (let i = 0; i < parts.length - 1; i++) {
        if (!current[parts[i]]) {
            current[parts[i]] = Object.create(null);  // FIXED: Prototypeless
        }
        current = current[parts[i]];
    }

    current[parts[parts.length - 1]] = value;
}

// FIXED: Use prototypeless objects
function createSafeObject() {
    return Object.create(null);  // No prototype chain
}

// FIXED: Freeze the prototype to prevent pollution
function protectGlobalPrototypes() {
    Object.freeze(Object.prototype);
    Object.freeze(Array.prototype);
    Object.freeze(Function.prototype);
}

// FIXED: Use Map instead of object for dynamic keys
function safeUserConfig(userId, key, value) {
    const userSettings = new Map();
    // Map doesn't have prototype pollution issues
    userSettings.set(key, value);
    return userSettings;
}
// Fixed: Express.js with prototype pollution protection

const express = require('express');
const app = express();

// FIXED: Custom JSON parser that blocks dangerous keys
function safeJSONParse(json) {
    return JSON.parse(json, (key, value) => {
        // FIXED: Filter dangerous keys during parsing
        if (isDangerousKey(key)) {
            return undefined;  // Remove dangerous keys
        }
        return value;
    });
}

// FIXED: Middleware to sanitize request body
function sanitizeBody(req, res, next) {
    if (req.body && typeof req.body === 'object') {
        req.body = sanitizeObject(req.body);
    }
    next();
}

function sanitizeObject(obj, seen = new WeakSet()) {
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }

    // Prevent circular reference issues
    if (seen.has(obj)) {
        return obj;
    }
    seen.add(obj);

    // FIXED: Create clean object
    const clean = Array.isArray(obj) ? [] : Object.create(null);

    for (const key of Object.keys(obj)) {
        // FIXED: Skip dangerous keys
        if (isDangerousKey(key)) {
            continue;
        }

        clean[key] = sanitizeObject(obj[key], seen);
    }

    return clean;
}

app.use(express.json());
app.use(sanitizeBody);

// FIXED: Safe route handler
app.get('/api/search', (req, res) => {
    // FIXED: Create prototypeless object
    const options = Object.create(null);

    // FIXED: Validate and copy only safe keys
    for (const key of Object.keys(req.query)) {
        if (!isDangerousKey(key)) {
            options[key] = req.query[key];
        }
    }

    // Safe to use options
    res.json({ success: true });
});

// FIXED: Use schema validation
const Joi = require('joi');

const configSchema = Joi.object({
    theme: Joi.string().valid('light', 'dark'),
    language: Joi.string().max(5),
    notifications: Joi.boolean()
}).unknown(false);  // FIXED: Reject unknown keys

app.post('/api/config', (req, res) => {
    // FIXED: Validate against schema
    const { error, value } = configSchema.validate(req.body);

    if (error) {
        return res.status(400).json({ error: error.message });
    }

    // value is now validated and safe
    saveConfig(value);
    res.json({ success: true });
});
// Fixed: Comprehensive protection module

class PrototypePollutionGuard {
    static DANGEROUS_KEYS = new Set([
        '__proto__',
        'constructor',
        'prototype',
        '__defineGetter__',
        '__defineSetter__',
        '__lookupGetter__',
        '__lookupSetter__'
    ]);

    // FIXED: Check if key is dangerous
    static isDangerous(key) {
        return this.DANGEROUS_KEYS.has(key);
    }

    // FIXED: Recursively clean object
    static sanitize(obj, maxDepth = 10, currentDepth = 0) {
        if (currentDepth > maxDepth) {
            throw new Error('Maximum object depth exceeded');
        }

        if (obj === null || typeof obj !== 'object') {
            return obj;
        }

        if (Array.isArray(obj)) {
            return obj.map(item =>
                this.sanitize(item, maxDepth, currentDepth + 1)
            );
        }

        // FIXED: Create clean prototypeless object
        const result = Object.create(null);

        for (const [key, value] of Object.entries(obj)) {
            if (this.isDangerous(key)) {
                continue;
            }
            result[key] = this.sanitize(value, maxDepth, currentDepth + 1);
        }

        return result;
    }

    // FIXED: Safe property access
    static safeGet(obj, path) {
        const parts = path.split('.');

        for (const part of parts) {
            if (this.isDangerous(part)) {
                return undefined;
            }
        }

        let current = obj;
        for (const part of parts) {
            if (current === null || current === undefined) {
                return undefined;
            }
            if (!Object.prototype.hasOwnProperty.call(current, part)) {
                return undefined;
            }
            current = current[part];
        }

        return current;
    }
}

// Usage
const userInput = JSON.parse('{"__proto__": {"polluted": true}, "safe": "value"}');
const clean = PrototypePollutionGuard.sanitize(userInput);
// clean = { safe: "value" }, __proto__ removed

CVE Examples

  • CVE-2019-11358: jQuery before 3.4.0 vulnerable to prototype pollution through $.extend.
  • CVE-2019-10744: lodash before 4.17.12 vulnerable to prototype pollution in _.defaultsDeep.
  • CVE-2020-8203: lodash before 4.17.19 vulnerable to prototype pollution in _.zipObjectDeep.
  • CVE-2018-3721: lodash before 4.17.5 vulnerable to prototype pollution.

  • CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes (parent)
  • CWE-471: Modification of Assumed-Immutable Data (can follow)
  • CWE-1321: Improperly Controlled Modification of Object Prototype Attributes (self)

References

  1. MITRE Corporation. "CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')." https://cwe.mitre.org/data/definitions/1321.html
  2. OWASP. "Prototype Pollution Prevention Cheat Sheet"
  3. Snyk. "Prototype Pollution Attacks in NodeJS Applications"