Prototype Pollution

Object Prototype Poisoning

Prototype Pollution at a glance

What it is: Untrusted objects are merged or extended into application objects, allowing attackers to set special keys like `__proto__`, `constructor`, or `prototype` that change defaults for all plain objects.
Why it happens: Prototype pollution occurs when applications deep-merge untrusted input into objects or configs, letting attackers modify object prototypes through unsafe merge operations or utilities.
How to fix: Never deep-merge untrusted input; copy only trusted keys into plain objects, strip prototype fields at all depths, and use safe object creation or JSON normalization.

Overview

In JavaScript, plain objects inherit from Object.prototype. If untrusted input is merged into objects without filtering, attackers can set special keys like __proto__, constructor, or prototype. Libraries that perform deep merge or extend operations can propagate these keys into the global prototype chain. Once polluted, every new object can appear to have attacker-chosen properties, impacting control flow, logging, serialization, and even rendering in the browser.

sequenceDiagram participant Browser participant App as App Server participant Merge as Deep Merge Utility Browser->>App: POST /search {'__proto__':{'isAdmin':true}} App->>Merge: merge(defaults, body) Merge-->>App: polluted defaults App-->>Browser: Confirms behavior; later checks see isAdmin on new objects note over App: Prototype chain poisoned across the process
A potential flow for a Prototype Pollution exploit

Where it occurs

It occurs when applications merge untrusted input into configuration or objects—such as request bodies, CLI arguments, or URL parameters—using deep merge utilities without proper validation.

Impact

Pollution can flip boolean checks such as if (obj.isAdmin), inject unexpected headers, crash processes by overriding toString, or cause persistent XSS in client apps. In multi-tenant servers, process-wide effects increase the blast radius.

Prevention

Prevent prototype pollution by avoiding deep merges of untrusted input, whitelisting keys, stripping reserved properties, normalizing via JSON, restricting merges to plain objects, and enforcing schema-validated, strongly typed data.

Examples

Switch tabs to view language/framework variants.

Merging request body into defaults allows `__proto__` pollution

A handler merges untrusted JSON into a config object. An attacker sets `__proto__` to inject properties into all plain objects.

Vulnerable
JavaScript • Express + lodash.merge — Bad
const merge = require('lodash.merge');
app.post('/search', (req,res)=>{
  const cfg = merge({limit:10, flags:{exact:false}}, req.body); // BUG
  const q = {term: req.body.term || ''};
  // later code creates plain objects that inherit polluted props
  res.json({ok:true, cfg});
});
  • Line 2: Deep merge of untrusted object without prototype guards

lodash.merge composes object graphs. If you pass untrusted objects with special keys like __proto__, they can poison the prototype of all future objects.

Secure
JavaScript • Express + lodash.merge — Good
const merge = require('lodash.merge');
function safeMerge(dst, src){
  if (src && typeof src === 'object') {
    if (Object.prototype.hasOwnProperty.call(src,'__proto__') || Object.prototype.hasOwnProperty.call(src,'constructor')) return dst;
  }
  return merge(dst, src);
}
app.post('/search', (req,res)=>{
  const input = JSON.parse(JSON.stringify(req.body)); // strips prototypes
  const cfg = safeMerge({limit:10, flags:{exact:false}}, input);
  res.json({ok:true, cfg});
});
  • Line 9: Sanitize input to plain JSON and refuse `__proto__` or `constructor` keys before merge

Treat untrusted input as data only, serialize-deserialize to drop prototypes, and block special keys before deep merge.

Engineer Checklist

  • Block __proto__, constructor, and prototype keys at all depths

  • Use Object.create(null) for header maps and dictionaries

  • Avoid deep-merge of user input; copy allows-listed keys into fresh objects

  • Normalize and validate request bodies with a schema before merging

  • Audit logging, serialization, and feature flag code paths for reliance on inherited properties

End-to-End Example

An API merges user-provided options into a global config with `lodash.merge`. An attacker sends a body that sets `__proto__.isAdmin = true`. Later, a permission check that creates `{}` and tests `.isAdmin` unexpectedly passes.

Vulnerable
JAVASCRIPT
// Node.js/Express + lodash - Vulnerable prototype pollution

const _ = require('lodash');

const appConfig = {
  theme: 'light',
  language: 'en',
  features: {}
};

app.post('/api/settings', authenticateToken, (req, res) => {
  // VULNERABLE: Deep merge of untrusted user input
  // Attacker sends: { "__proto__": { "isAdmin": true } }
  // This pollutes Object.prototype for the entire application!
  _.merge(appConfig, req.body);
  
  res.json({ message: 'Settings updated', config: appConfig });
});

app.get('/api/admin/users', authenticateToken, (req, res) => {
  // VULNERABLE: Authorization check using polluted prototype
  const user = {};
  user.id = req.user.id;
  user.name = req.user.name;
  
  // After pollution, ALL objects inherit isAdmin: true!
  if (user.isAdmin) {  // This is now true for everyone!
    return res.json(getAllUsers());
  }
  
  res.status(403).json({ error: 'Admin only' });
});

// ALSO VULNERABLE: Object.assign with nested objects
app.patch('/api/preferences', (req, res) => {
  const userPrefs = { notifications: true };
  
  // VULNERABLE: Object.assign doesn't deep clone, but can still be exploited
  // with constructor.prototype pollution
  Object.assign(userPrefs, req.body);
  
  res.json(userPrefs);
});
Secure
JAVASCRIPT
// Node.js/Express - SECURE against prototype pollution

// Utility to filter dangerous keys recursively
function sanitizeObject(obj) {
  const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
  
  if (typeof obj !== 'object' || obj === null) return obj;
  
  if (Array.isArray(obj)) {
    return obj.map(sanitizeObject);
  }
  
  const sanitized = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key) && !dangerousKeys.includes(key)) {
      sanitized[key] = sanitizeObject(obj[key]);
    }
  }
  return sanitized;
}

const appConfig = {
  theme: 'light',
  language: 'en',
  features: {}
};

app.post('/api/settings', authenticateToken, (req, res) => {
  // SECURE: Whitelist allowed fields, no deep merge
  const allowedFields = ['theme', 'language', 'timezone'];
  const updates = {};
  
  for (const field of allowedFields) {
    if (req.body[field] !== undefined) {
      // Coerce to expected type
      updates[field] = String(req.body[field]);
    }
  }
  
  Object.assign(appConfig, updates);
  res.json({ message: 'Settings updated', config: appConfig });
});

app.get('/api/admin/users', authenticateToken, (req, res) => {
  // SECURE: Explicit permission check, not relying on object properties
  const userId = req.user.id;
  
  // Check against database or explicit role system
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Admin only' });
  }
  
  res.json(getAllUsers());
});

// SECURE: Use JSON round-trip to remove prototype pollution
app.patch('/api/preferences', (req, res) => {
  // JSON round-trip removes __proto__ and other dangerous keys
  const sanitizedInput = JSON.parse(JSON.stringify(req.body));
  
  // Additional sanitization
  const safeInput = sanitizeObject(sanitizedInput);
  
  // Whitelist specific fields
  const userPrefs = {
    notifications: Boolean(safeInput.notifications),
    email_updates: Boolean(safeInput.email_updates),
    theme: String(safeInput.theme || 'light')
  };
  
  res.json(userPrefs);
});

// ALTERNATIVE: Use null-prototype objects for maps
app.post('/api/user-data', (req, res) => {
  // Create object with no prototype
  const userData = Object.create(null);
  
  // Safe to assign - no prototype chain to pollute
  userData.name = req.body.name;
  userData.email = req.body.email;
  
  res.json(userData);
});

Discovery

This vulnerability is discovered by testing object merge operations or query parameter parsing with special keys like __proto__ or constructor.prototype and observing whether properties are added to JavaScript object prototypes.

  1. 1. Baseline object merge test

    http

    Action

    Send normal nested object to understand merge behavior

    Request

    POST https://app.example.com/api/settings
    Headers:
    Content-Type: application/json
    Body:
    {
      "preferences": {
        "theme": "dark",
        "language": "en"
      }
    }

    Response

    Status: 200
    Body:
    {
      "message": "Settings updated",
      "merged_config": {
        "preferences": {
          "theme": "dark",
          "language": "en"
        }
      }
    }

    Artifacts

    merge_operation_confirmed baseline_established
  2. 2. Prototype pollution via __proto__

    http

    Action

    Inject __proto__ key to pollute Object.prototype

    Request

    POST https://app.example.com/api/settings
    Headers:
    Content-Type: application/json
    Body:
    {}

    Response

    Status: 200
    Body:
    {
      "message": "Settings updated",
      "test_result": "New empty object {}.polluted === true",
      "note": "Prototype chain poisoned! All new objects inherit polluted properties"
    }

    Artifacts

    prototype_pollution_confirmed object_prototype_modified global_state_affected
  3. 3. Constructor.prototype pollution

    http

    Action

    Test alternate pollution vector via constructor.prototype

    Request

    POST https://app.example.com/api/config
    Headers:
    Content-Type: application/json
    Body:
    {
      "constructor": {
        "prototype": {
          "adminMode": true,
          "bypassAuth": true
        }
      }
    }

    Response

    Status: 200
    Body:
    {
      "message": "Config merged",
      "verification": "({}).adminMode === true",
      "note": "Alternate pollution vector successful"
    }

    Artifacts

    constructor_pollution prototype_chain_modified
  4. 4. Denial of service via toString pollution

    http

    Action

    Break core JavaScript operations by polluting toString

    Request

    POST https://app.example.com/api/settings
    Headers:
    Content-Type: application/json
    Body:
    {}

    Response

    Status: 500
    Body:
    {
      "error": "TypeError: Cannot convert object to primitive value",
      "stack_trace": "at Object.toString...\nat JSON.stringify...\nat logger.info...",
      "note": "Application crashes on any string coercion operations"
    }

    Artifacts

    application_crash dos_confirmed serialization_failure

Exploit steps

An attacker exploits this by polluting object prototypes with malicious properties that affect application logic globally, potentially achieving denial of service, authentication bypass, or remote code execution depending on how the polluted properties are used.

  1. 1. Privilege escalation via isAdmin pollution

    Pollute prototype with admin flag

    http

    Action

    Inject isAdmin=true into Object.prototype to bypass authorization

    Request

    POST https://app.example.com/api/search
    Headers:
    Content-Type: application/json
    Body:
    {}

    Response

    Status: 200
    Body:
    {
      "message": "Search completed",
      "auth_check_result": "Authorization check: ({}).isAdmin === true",
      "admin_access": "Granted",
      "note": "All subsequent requests see isAdmin=true on empty objects, bypassing authorization"
    }

    Artifacts

    authorization_bypass admin_access_granted privilege_escalation process_wide_pollution
  2. 2. Remote code execution via template pollution

    Pollute template engine properties for RCE

    http

    Action

    Inject properties that affect template rendering to execute code

    Request

    POST https://app.example.com/api/render
    Headers:
    Content-Type: application/json
    Body:
    {}

    Response

    Status: 200
    Body:
    {
      "rendered": "DATABASE_URL=postgresql://admin:Pr0dP@ssw0rd2024@db.internal:5432/production\nAWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\nAWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
      "note": "Template engine executed attacker code using polluted properties"
    }

    Artifacts

    rce_confirmed secrets_extracted template_pollution_exploit code_execution
  3. 3. Application-wide denial of service

    Crash all request handling

    http

    Action

    Pollute critical methods to break JSON serialization and logging

    Request

    POST https://app.example.com/api/config
    Headers:
    Content-Type: application/json
    Body:
    {}

    Response

    Status: 500
    Body:
    {
      "error": "Service Unavailable",
      "impact": "All subsequent requests fail with TypeError",
      "affected_operations": [
        "logging",
        "JSON.stringify",
        "string concatenation",
        "HTTP responses"
      ],
      "duration": "Until process restart",
      "note": "Process-wide DoS affecting all users"
    }

    Artifacts

    application_down process_wide_dos service_disruption multi_tenant_impact
  4. 4. Data exfiltration via polluted callbacks

    Inject malicious error handlers

    http

    Action

    Pollute callback URLs to intercept sensitive error data

    Request

    POST https://app.example.com/api/webhook
    Headers:
    Content-Type: application/json
    Body:
    {}

    Response

    Status: 200
    Body:
    {
      "message": "Webhook configured",
      "attacker_receives": {
        "error_logs": "Database connection errors with credentials",
        "stack_traces": "Full application source paths and logic",
        "user_data": "PII from failed operations",
        "note": "All errors and logs now sent to attacker endpoints"
      }
    }

    Artifacts

    data_exfiltration error_log_theft credential_leak source_disclosure

Specific Impact

Authorization checks that rely on object properties fail in unpredictable ways. Attackers may enable admin features or disable safeguards. In some cases, polluted toString breaks logging and incident monitoring, causing availability issues and delayed detection.

Because pollution is process-wide, multiple tenants or requests can be affected until the process restarts, increasing blast radius and remediation time.

Fix

Replace deep-merge of user input with allow-listed copying into null-prototype objects. Strip reserved keys recursively and normalize inputs with JSON serialization. Add unit tests that attempt to set __proto__ and confirm rejection.

Detect This Vulnerability in Your Code

Sourcery automatically identifies prototype pollution vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free