Safeguard
Vulnerability Analysis

CVE-2018-16487: Prototype pollution in lodash via merge/m...

CVE-2018-16487 let attackers pollute Object.prototype through lodash's merge, mergeWith, and defaultsDeep — a bypass of an earlier fix, patched in 4.17.11.

Aman Khan
AppSec Engineer
7 min read

Lodash is one of the most depended-upon packages in the JavaScript ecosystem, quietly embedded — often several layers deep — in everything from build tooling to backend services. That ubiquity is exactly what made CVE-2018-16487 worth taking seriously: a prototype pollution flaw in lodash's merge, mergeWith, and defaultsDeep functions that let an attacker who controls part of the object passed into one of these calls inject properties onto Object.prototype itself, corrupting behavior for every object in the running process. Depending on how a downstream application used the polluted properties, the practical impact ranged from denial of service to logic bypasses (and, in less common but real chained scenarios, remote code execution) — a broad blast radius for a bug that lived in a handful of utility functions.

What the vulnerability actually does

Lodash's deep-merge functions are recursive by design: _.merge(target, source) walks the source object and copies its properties into the target, recursing into nested objects along the way. Prototype pollution happens when the source object contains a key like __proto__ or constructor.prototype, and the merge logic doesn't distinguish those special keys from ordinary data properties. Instead of writing to a normal object property, the recursive merge follows the prototype chain and writes onto Object.prototype, meaning every plain object in the JavaScript runtime — past, present, and future — now inherits the attacker-supplied property.

CVE-2018-16487 is specifically notable because it was a bypass of an earlier fix. Lodash had already patched a related issue, CVE-2018-3721, in version 4.17.5 by blocking the literal __proto__ key during merges. CVE-2018-16487 showed that the same outcome was reachable through a different payload shape — {constructor: {prototype: {...}}} — which sailed past the 4.17.5 guard and reached Object.prototype through the constructor property instead. It's a good illustration of why prototype pollution fixes need to account for the whole family of prototype-chain access patterns, not just the most obvious one.

Affected versions and components

  • Vulnerable functions: merge, mergeWith, and defaultsDeep
  • Affected range: lodash versions from 4.17.5 up to (but not including) 4.17.11 — i.e., the versions that had already patched CVE-2018-3721 but not this bypass
  • Fixed in: lodash 4.17.11
  • Also affects: lodash.merge, lodash.mergewith, and lodash.defaultsdeep, the standalone per-function packages published alongside the main lodash module, since they share the same underlying implementation
  • Indirect exposure: because lodash is so frequently pulled in as a transitive dependency (bundlers, CLI tools, templating engines, request libraries, and countless internal utilities all lean on it), many projects were exposed without ever declaring lodash directly — the vulnerable version was sitting several node_modules levels down

Any application that passes user-controlled JSON (request bodies, query parameters, config uploads, webhook payloads, etc.) into _.merge, _.mergeWith, or _.defaultsDeep — directly or via a dependency that does so internally — was in the blast radius.

CVSS, EPSS, and KEV context

NVD scores CVE-2018-16487 as:

  • CVSS v3.1: 5.6 (Medium)AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L
  • CVSS v2.0: 6.8 (Medium)AV:N/AC:M/Au:N/C:P/I:P/A:P

The "High" attack complexity in the v3.1 vector reflects that exploitation isn't a one-size-fits-all remote code execution primitive — the actual impact depends heavily on what the host application does with the polluted properties afterward. That's a recurring theme with prototype pollution: the library bug is the entry point, but the severity is really determined by what code runs downstream and reads those inherited properties (a template engine, a permissions check, a child_process call, and so on).

As of this writing, CVE-2018-16487 does not appear on CISA's Known Exploited Vulnerabilities (KEV) catalog, and it has not been associated with a widely publicized mass-exploitation campaign. Its EPSS score has fluctuated over its lifetime and reporting on it varies by source, so treat any specific percentile you see as a snapshot rather than a fixed fact — check a live EPSS feed if you need a current number for risk scoring. In practice, the more useful signal for prioritization is reachability: is the vulnerable function actually called with attacker-influenced input anywhere in your dependency graph.

Timeline

  • Earlier fix (context): lodash 4.17.5 patches CVE-2018-3721 by blocking __proto__ in merge/mergeWith/defaultsDeep
  • Vulnerability reported: disclosed to the lodash maintainers via HackerOne (report #380873), demonstrating that the constructor.prototype payload shape bypassed the 4.17.5 fix
  • Fix released: lodash 4.17.11 closes the gap by hardening the merge/clone internals against prototype-chain keys more comprehensively
  • NVD publication: the CVE entry was published to the NVD on February 1, 2019
  • Downstream remediation: package maintainers across the ecosystem spent the following months bumping lodash pins and republishing fixed releases, since many popular packages had vendored or pinned pre-4.17.11 ranges

It's also worth noting that lodash received further prototype-pollution and related fixes after 4.17.11 — including issues affecting defaultsDeep/zipObjectDeep and, later, a template-injection fix in 4.17.21. If you're remediating CVE-2018-16487 today, don't just clear the minimum bar of 4.17.11 — go to the latest 4.x release so you pick up those subsequent hardening fixes too.

Remediation steps

  1. Upgrade lodash to 4.17.11 or later — and prefer the latest 4.x release rather than the minimum patched version, to also cover later prototype-pollution and template-injection fixes.
  2. Check standalone method packages too. If your project (or a dependency) uses lodash.merge, lodash.mergewith, or lodash.defaultsdeep directly instead of the full lodash package, those need the same version bump.
  3. Find transitive copies. Run npm ls lodash / yarn why lodash (or your package manager's equivalent) to enumerate every resolved copy, since lodash is commonly nested multiple levels deep through build tools and utility libraries. Use overrides (npm) or resolutions (Yarn) to force a safe version across the whole tree when a transitive maintainer hasn't republished.
  4. Audit call sites. Search your codebase for _.merge, _.mergeWith, and _.defaultsDeep calls where any argument originates from user input — request bodies, query strings, uploaded config files, webhook payloads — and treat those as high-priority to patch or re-review even after upgrading.
  5. Add defense in depth. Reject or strip __proto__, constructor, and prototype keys at the input-validation layer before objects reach any merge logic. Consider Object.freeze(Object.prototype) in Node.js services where compatibility allows it, and prefer Object.create(null) or Map for objects that will hold externally-controlled keys.
  6. Re-verify with a lockfile diff. After patching, confirm the resolved version in your lockfile actually changed — a package.json range bump without a lockfile update (or a stale CI cache) is a common reason "fixed" dependencies ship unpatched anyway.

How Safeguard Helps

CVE-2018-16487 is a textbook case for why supply chain security can't stop at "is the package name in a manifest." The vulnerable code was often several dependency layers removed from the application team that ultimately shipped it, and the same version string (lodash@4.17.54.17.10) could be simultaneously safe in one context and exploitable in another depending on whether attacker-controlled data ever reached merge, mergeWith, or defaultsDeep.

Safeguard's software composition analysis continuously resolves your full dependency graph — direct and transitive — so vulnerable lodash copies buried under build tools, CLI utilities, or internal libraries surface even when they're absent from your top-level package.json. Our reachability analysis goes a step further, tracing whether the specific vulnerable functions are actually invoked in a path an attacker can influence, so your team can prioritize the handful of instances that matter over the noise of every incidental lodash pin in the tree.

On top of detection, Safeguard's CI/CD policy gates can block merges or builds that introduce a known-vulnerable lodash range before it ever reaches production, while our SBOM generation and continuous monitoring keep an auditable record of what was affected, when it was patched, and where remaining exposure lives — the kind of evidence that matters both for incident response and for SOC 2 / compliance reporting. Combined with drift detection between declared manifests and resolved lockfiles, Safeguard helps ensure that a fix like "bump to lodash 4.17.11" actually lands everywhere it needs to, not just in the repository where someone remembered to check.

Never miss an update

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