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:

  1. Freeze the Prototype: Call Object.freeze(Object.prototype) during application startup to prevent runtime modifications.
  2. Use Null-Prototype Objects: For data containers holding dynamic user payloads, instantiate objects using Object.create(null) or use the native Map structure, ensuring there is no prototype chain to pollute.