Never merge user-controlled objects into application objects using Object.assign, spread, or deep-merge without validation. Prototype pollution lets an attacker inject __proto__ properties that modify the behavior of every object in your application — enabling denial of service, authentication bypass, or remote code execution.
Prototype pollution allows an attacker to inject properties into Object.prototype via __proto__, which then appear on every object in the application. This can bypass authentication checks (if user.isAdmin reads from the polluted prototype), cause denial of service (crashing template engines), or achieve remote code execution via gadget chains.
BeforeMerge scans your pull requests against this rule and 3+ others. Get actionable feedback before code ships.
JavaScript objects inherit from Object.prototype. If an attacker can set a property on Object.prototype (via __proto__, constructor.prototype, or similar), that property appears on every object in the application.
Prototype pollution occurs when user-controlled data is merged into application objects without filtering dangerous keys. Common vectors include:
Object.assign(target, userInput) where userInput contains __proto__JSON.parse() output passed directly to merge operations (JSON can contain __proto__ keys)The impact ranges from denial of service (crashing template engines like Handlebars or Pug) to authentication bypass (polluting isAdmin or role properties) to remote code execution (via gadget chains in libraries like lodash or express).
Never merge user-controlled objects into application objects without filtering __proto__, constructor, and prototype keys. Use Object.create(null) for lookup tables. Prefer allowlist-based property copying over blocklist-based filtering.
// Prototype pollution via Object.assign
app.post("/settings", (req, res) => {
const defaults = { theme: "light", language: "en" };
const settings = Object.assign(defaults, req.body);
// Attacker sends: { "__proto__": { "isAdmin": true } }
// Now EVERY object has isAdmin === true
});
// Deep merge without prototype filtering
import merge from "lodash.merge";
const config = merge({}, defaultConfig, userConfig);
// userConfig.__proto__.polluted = true// Allowlist-based property copying
app.post("/settings", (req, res) => {
const allowedKeys = ["theme", "language", "timezone"] as const;
const settings: Record<string, string> = { theme: "light", language: "en" };
for (const key of allowedKeys) {
if (req.body[key] !== undefined) {
settings[key] = String(req.body[key]);
}
}
});
// Zod schema validation strips unknown properties
import { z } from "zod";
const settingsSchema = z.object({
theme: z.enum(["light", "dark"]).default("light"),
language: z.string().max(5).default("en"),
});
app.post("/settings", (req, res) => {
const settings = settingsSchema.parse(req.body);
// Only validated, known properties exist
});Search for object merging with user input:
grep -rn 'Object.assign\|\.\.\. *req\.body\|merge(.*req' --include="*.ts"Use npm audit to check for known prototype pollution vulnerabilities in dependencies.
Object.assign(target, userInput) with allowlist-based property copyingObject.prototype in critical paths: Object.freeze(Object.prototype) (caution: may break some libraries)Object.create(null) for lookup tables that should not inherit prototype properties