Fixing Lodash _.merge Prototype Pollution Safely
Lodash's _.merge method has historically posed critical
security risks through Prototype Pollution vulnerabilities and
unpredictable array index overwrites during deep merges. Completely
resolving these issues requires upgrading to Lodash 4.17.21 or higher,
employing _.mergeWith alongside an explicit customizer
function to handle arrays safely, and hardening the runtime environment
against recursive prototype contamination.
Upgrading Lodash to Patch Known CVEs
The foundational step to eliminate known Prototype Pollution vectors
(such as CVE-2018-16487 and CVE-2019-10744) is upgrading the library.
Lodash versions prior to 4.17.21 improperly handled
properties like __proto__, constructor, and
prototype, enabling attackers to inject arbitrary
properties into the global Object.prototype.
Verify your installation and enforce the patched version in
package.json:
"dependencies": {
"lodash": "^4.17.21"
}This ensures the internal implementation of _.merge
blocks direct assignments to prototype properties.
Safely
Handling Deep Array Merging with _.mergeWith
By default, _.merge reconciles arrays by index rather
than replacing or concatenating them. Merging ['a', 'b']
with ['c'] results in ['c', 'b'], mutating
nested elements in ways that introduce logical flaws and security edge
cases.
To safely control array merging, replace _.merge with
_.mergeWith and define an explicit customizer that either
concatenates or completely overwrites target arrays:
const _ = require('lodash');
function safeCustomizer(objValue, srcValue, key) {
// Prevent prototype pollution keys manually as defense-in-depth
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
return objValue;
}
// Safely concatenate arrays rather than mutating index-by-index
if (_.isArray(objValue)) {
return objValue.concat(srcValue);
}
}
const safeMerge = (target, ...sources) => {
return _.mergeWith(target, ...sources, safeCustomizer);
};If the desired business logic requires incoming arrays to completely
overwrite existing ones, return srcValue instead of
objValue.concat(srcValue).
Sanitizing Untrusted Inputs
Relying entirely on a third-party merge function leaves applications vulnerable to zero-day vectors. Validate and sanitize inputs before they reach the merge layer by stripping dangerous object keys:
function sanitizeInput(obj) {
return JSON.parse(JSON.stringify(obj), (key, value) => {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
return undefined;
}
return value;
});
}Alternatively, use strict schema validation libraries such as Zod or Ajv to discard unmapped recursive keys prior to execution.
Runtime Hardening
To guarantee that no deep merge vulnerability can alter inherited prototypes across your application:
- Freeze the Prototype: Call
Object.freeze(Object.prototype)during application startup to prevent runtime modifications. - Use Null-Prototype Objects: For data containers
holding dynamic user payloads, instantiate objects using
Object.create(null)or use the nativeMapstructure, ensuring there is no prototype chain to pollute.