How to Prevent Prototype Pollution in Node.js

Prototype pollution is a critical vulnerability in Node.js applications that allows attackers to inject malicious properties into the global Object.prototype. This can lead to application crashes, bypass of security controls, or even remote code execution. This article provides a direct, actionable guide on how to secure your Node.js applications against prototype pollution using secure coding practices, input validation, and defensive configurations.

Use Map Instead of Plain Objects

The simplest way to avoid prototype pollution is to avoid using plain JavaScript objects ({}) as key-value stores for user-controlled input. Instead, use the ES6 Map class. Map does not inherit from Object.prototype, making it completely immune to prototype-based attacks.

// Vulnerable to prototype pollution
const userRoles = {};
userRoles[userInputKey] = userInputValue;

// Secure against prototype pollution
const userRoles = new Map();
userRoles.set(userInputKey, userInputValue);

If you must use a plain object, create it without a prototype using Object.create(null). This ensures the object has no inheritance chain, meaning __proto__ and constructor properties do not exist on it.

const safeObject = Object.create(null);
safeObject[userInputKey] = userInputValue; // Safe

Freeze the Object Prototype

You can prevent modifications to JavaScript’s default prototypes by freezing them when your application starts. Placing Object.freeze(Object.prototype) at the entry point of your Node.js application stops attackers from injecting new properties or altering existing ones globally.

// Run this at the very beginning of your entry file (e.g., app.js or index.js)
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);

While highly effective, test your application thoroughly after applying this, as some third-party libraries may rely on modifying default prototypes and could break.

Implement Strict Input Validation

Prototype pollution typically occurs when an application recursively merges or deep-clones user-provided JSON payloads without validation. You should sanitize and validate all incoming request bodies using schema validators like Ajv or Joi.

Configure your validators to reject any payloads containing forbidden keys, specifically: * __proto__ * constructor * prototype

Example using Joi:

const Joi = require('joi');

const schema = Joi.object({
  // Define expected fields explicitly and disallow unknown properties
}).unknown(false); 

Use Secure Libraries for Merging and Cloning

Custom implementation of deep-merge or cloning functions is a common source of prototype pollution. Avoid writing custom recursive merge functions. Instead, use established libraries and keep them updated.

If you use utilities like lodash, ensure you are using the latest version, as older versions had known prototype pollution vulnerabilities in functions like _.defaultsDeep, _.merge, and _.set.

If you must write a custom merge function, explicitly block the dangerous keys:

function safeMerge(target, source) {
  for (let key in source) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue; // Skip dangerous keys
    }
    // Perform merge logic
  }
  return target;
}

Keep Node.js and Dependencies Updated

Vulnerabilities are frequently discovered in popular npm packages. Regularly run vulnerability scanners to detect and patch prototype pollution risks in your dependency tree. Use the built-in npm tool:

npm audit

For automated continuous monitoring, integrate tools like Snyk or GitHub Dependabot into your CI/CD pipeline to catch vulnerable packages before they reach production.