How do you prevent prototype pollution attacks in JavaScript?
Assesses fundamental understanding of JavaScript conventions, runtime behavior, and memory/performance considerations.
Hiring managers look for precision, avoidance of ambiguous jargon, and ability to explain trade-offs under real production conditions.
Prototype pollution is a security vulnerability where attackers inject properties into Object.prototype, affecting all objects in the application.
Vulnerable code:
function merge(target, source) {
for (let key in source) {
target[key] = source[key];
}
return target;
}
// Attack payload
const malicious = JSON.parse('{"__proto__": {"polluted": "yes"}}');
merge({}, malicious);
console.log({}.polluted); // "yes" - all objects are polluted!
Prevention 1: Use Object.create(null):
// Create objects without prototype
const safeObj = Object.create(null);
safeObj.__proto__ = { polluted: 'yes' };
console.log(safeObj.polluted); // undefined
// For configuration objects
const config = Object.create(null);
config.apiUrl = 'https://api.example.com';
Prevention 2: Check for dangerous keys:
function safeMerge(target, source) {
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
for (let key in source) {
if (dangerousKeys.includes(key)) {
continue; // Skip dangerous keys
}
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
return target;
}
Prevention 3: Use Map instead of objects:
const safeMap = new Map();
safeMap.set('__proto__', 'value');
// No pollution risk
Prevention 4: Freeze Object.prototype:
Object.freeze(Object.prototype);
Object.freeze(Object);
// Now pollution attempts will fail
Object.prototype.polluted = 'no';
console.log({}.polluted); // undefined
Prevention 5: Validate object paths:
function setDeepProperty(obj, path, value) {
const parts = path.split('.');
const dangerous = ['__proto__', 'constructor', 'prototype'];
// Validate each part of the path
if (parts.some(part => dangerous.includes(part))) {
throw new Error('Invalid property path');
}
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
if (!(parts[i] in current)) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
}
Prevention 6: Use libraries with protection:
// Use lodash's merge with customizer
const _ = require('lodash');
function safeMergeCustomizer(objValue, srcValue, key) {
const dangerous = ['__proto__', 'constructor', 'prototype'];
if (dangerous.includes(key)) {
return objValue; // Keep original value
}
}
const result = _.mergeWith({}, source, safeMergeCustomizer);
Prevention 7: Schema validation:
const Ajv = require('ajv');
const ajv = new Ajv();
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' }
},
additionalProperties: false // Reject unknown properties
};
const validate = ajv.compile(schema);
function safeProcess(data) {
if (!validate(data)) {
throw new Error('Invalid data');
}
return data;
}
Prevention 8: JSON.parse with reviver:
function safeJSONParse(text) {
return JSON.parse(text, (key, value) => {
const dangerous = ['__proto__', 'constructor', 'prototype'];
if (dangerous.includes(key)) {
return undefined; // Filter out dangerous keys
}
return value;
});
}
const safe = safeJSONParse('{"__proto__": {"polluted": "yes"}}');
console.log({}.polluted); // undefined
Candidate Response Strategy & Interview Tips
- Start with a concise one-sentence summary: Deliver a direct, confident answer first before expanding into nuances.
- Demonstrate real-world trade-offs: Discuss where this approach excels and when you would avoid it in production systems.
- Discuss complexity & edge cases: Proactively explain time/space complexity or boundary conditions (null values, scale limits).
- Prepare for interviewer follow-ups: Technical hiring panels frequently probe deeper into concurrency, backward compatibility, or alternative libraries.