Safeguard
Vulnerability Analysis

lodash property injection via merge functions (CVE-2018-16487)

CVE-2018-16487 let attackers pollute Object.prototype via lodash's merge, mergeWith, and defaultsDeep functions. Here's how it works and how to fix it.

Nayan Dey
Security Researcher
7 min read

A single unfiltered key in a merge call was enough to let attackers rewrite the behavior of every object in a Node.js process. CVE-2018-16487 affects lodash versions prior to 4.17.11, where the merge, mergeWith, and defaultsDeep functions failed to filter dangerous keys like __proto__ and constructor.prototype when recursively combining objects. An attacker who could get attacker-controlled JSON into one of these calls — a common pattern for config merging, options defaults, and request-body normalization — could pollute Object.prototype itself, injecting properties that every object in the application would then inherit. Because lodash sits in the dependency tree of an enormous share of the JavaScript ecosystem, this one bug quietly propagated into thousands of downstream packages, many of which never issued their own advisory.

Prototype pollution is not a theoretical class of bug. Depending on how a polluted property is later consumed, it has been used to bypass authorization checks, trigger denial of service, and in template-engine or child_process contexts, escalate to remote code execution. CVE-2018-16487 is the vulnerability that put "prototype pollution" on most security teams' radar, and it remains one of the most frequently rediscovered transitive dependency issues in SCA scan results today — not because it's hard to fix, but because lodash is bundled, vendored, and pinned in so many places that stale copies persist for years.

What Makes This Vulnerability Work

Lodash's merge family is designed to deep-merge nested objects — think merging a user-supplied config object into a set of defaults. The internal implementation (baseMergeDeep) walked the source object's keys and assigned them onto the destination without excluding special property names. When the source object contained a key like __proto__, constructor, or prototype, lodash treated it like any other key and merged its value onto the corresponding prototype chain element rather than onto a plain data property.

A minimal proof-of-concept looks like this:

const _ = require('lodash'); // vulnerable version < 4.17.11

const payload = JSON.parse('{"constructor": {"prototype": {"isAdmin": true}}}');

_.merge({}, payload);

// Every plain object in the process now inherits isAdmin: true
console.log({}.isAdmin); // true

In a real application, that payload is often the body of an HTTP request, a query-string object parsed into JSON, or a configuration file merged with defaults at startup. Once Object.prototype is polluted, any code elsewhere in the application that checks a property like isAdmin, role, or isValid without an explicit hasOwnProperty check can be silently subverted — no exception, no log entry, just a value that shouldn't exist suddenly showing up on every object.

Affected Versions and Components

  • Directly affected: lodash < 4.17.11
  • Fixed in: lodash 4.17.11 and later
  • Vulnerable functions: _.merge, _.mergeWith, _.defaultsDeep
  • Standalone packages: the modular lodash.merge, lodash.mergewith, and lodash.defaultsdeep packages were affected on their own independent version lines and required separate upgrades even after the main lodash package was patched
  • Indirect exposure: any package that bundled or pinned a pre-4.17.11 lodash as a transitive dependency, including numerous build tools, CLI utilities, and framework plugins popular in 2018–2019

Because lodash is one of the most-depended-upon packages in the npm registry, the practical blast radius of CVE-2018-16487 extended far beyond direct consumers. Security teams doing SCA sweeps in 2019 and 2020 routinely found the vulnerable version three or four levels deep in their dependency graphs, inherited from packages that had no idea they were shipping a prototype-pollution primitive.

Severity, Exploit Prediction, and KEV Status

CVE-2018-16487 is publicly tracked with a CVSS v3 base score in the medium range (commonly listed around 5.6, AV:N/AC:L/PR:N/UI:N — network-exploitable with low complexity and no privileges required, but with a capped integrity/availability impact since exploitation depends on how the polluted property is later consumed). Some vendor advisories score it higher, closer to 6.5–7.5, because the real-world impact is heavily context-dependent: in an application that trusts prototype-derived properties for authorization or template rendering, prototype pollution can chain into authentication bypass or RCE, which pushes the practical risk well above the base score.

The vulnerability's EPSS (Exploit Prediction Scoring System) probability is comparatively low relative to actively-exploited-in-the-wild CVEs — there's no widespread automated exploitation campaign targeting this specific CVE the way there is for, say, a Log4Shell-class bug. CVE-2018-16487 is not currently listed on CISA's Known Exploited Vulnerabilities (KEV) catalog. That combination — low EPSS, no KEV listing — is exactly why this CVE tends to get deprioritized in ticket queues, even though it remains one of the highest-volume findings in dependency scans years after the fix shipped. Age and ubiquity, not active exploitation, are what keep it relevant: it's a durable, well-understood attack primitive that keeps resurfacing wherever a stale lodash copy survives.

Timeline

  • 2018: Security researchers identify that lodash's merge-family functions do not filter __proto__/constructor.prototype keys, allowing prototype pollution through untrusted merge input.
  • November 2018: lodash 4.17.11 ships, filtering dangerous keys in merge, mergeWith, and defaultsDeep.
  • Early 2019: CVE-2018-16487 is formally published in the NVD, and the standalone lodash.merge / lodash.mergewith / lodash.defaultsdeep packages receive matching point releases.
  • 2019–present: The CVE is repeatedly rediscovered by SCA and dependency-audit tooling as organizations find pre-4.17.11 lodash pinned in build tooling, legacy services, and vendored bundles. It is frequently grouped in remediation advice alongside lodash's other well-known issues (CVE-2019-10744, CVE-2020-8203, CVE-2021-23337), since teams patching lodash typically address all of them by jumping to 4.17.21.

Remediation Steps

  1. Upgrade lodash to 4.17.21 (or at minimum 4.17.11). The latest release addresses this issue along with several subsequent lodash CVEs, so a single version bump clears multiple findings at once.
  2. Patch standalone modular packages independently. If your project depends on lodash.merge, lodash.mergewith, or lodash.defaultsdeep directly rather than through the main lodash package, upgrade each of those separately — they carry their own version numbers and are easy to miss in a lockfile audit.
  3. Audit transitive dependencies, not just direct ones. Run npm ls lodash (or your package manager's equivalent) to find every nested copy in your tree, including ones bundled inside build tools and CLI dependencies that never appear in your own package.json.
  4. Sanitize untrusted input before any merge/assign operation. Reject or strip keys such as __proto__, constructor, and prototype from any JSON that originates from a request body, query string, or user-supplied config before it reaches a merge function — regardless of library version, as defense in depth.
  5. Freeze the prototype where feasible. Object.freeze(Object.prototype) in a startup module can neutralize pollution attempts at the runtime level, though it should be tested carefully since it can break libraries that legitimately extend built-in prototypes.
  6. Prefer Map or Object.create(null) for untrusted key-value data. Neither structure inherits from Object.prototype, which eliminates the pollution surface for data that doesn't need prototype chain features.
  7. Lock and re-verify. After upgrading, regenerate your lockfile and re-run your SCA scan to confirm no other package has re-pinned a vulnerable lodash version.

How Safeguard Helps

Safeguard is built to catch exactly this class of long-tail, deeply-nested dependency risk before it ships. Our SBOM generation and ingest pipeline maps every direct and transitive copy of lodash across your services — including standalone lodash.merge-style packages — so a vulnerable version buried four layers deep in a build tool doesn't hide from your inventory. Reachability analysis then determines whether your code actually calls merge, mergeWith, or defaultsDeep on attacker-influenced input, letting your team prioritize the handful of genuinely exploitable paths instead of triaging every occurrence of the package with equal urgency. Griffin AI, our reasoning engine, traces those call paths end to end to explain in plain language how untrusted data could reach a vulnerable merge call in your specific codebase. When a fix is available, Safeguard opens an auto-fix pull request that bumps the dependency and adjusts lockfiles across affected services, turning what used to be a multi-repo cleanup effort into a reviewable diff your team can merge with confidence.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.