Safeguard
Vulnerability Analysis

lodash merge/mergeWith prototype pollution (CVE-2018-3721)

A deep dive into CVE-2018-3721, the lodash merge/mergeWith prototype pollution flaw: its real-world impact, affected versions, and how to remediate it fast.

Nayan Dey
Security Researcher
9 min read

CVE-2018-3721 is a prototype pollution vulnerability in lodash's merge and mergeWith functions, affecting all versions of lodash prior to 4.17.5. An attacker who can influence the keys of an object passed into a vulnerable merge operation — for example, JSON from a request body, a query string, or a config file — can inject properties onto Object.prototype itself, corrupting the behavior of every object in the running process. Depending on how the polluted properties are later consumed, the practical impact ranges from denial of service to authentication bypass and, in the worst cases documented by researchers in follow-up applications, remote code execution. Because lodash sits transitively inside an enormous share of the npm ecosystem, this single fix in a utility library rippled through thousands of downstream packages and applications that had never directly called merge themselves.

What went wrong

Prototype pollution is a JavaScript-specific class of vulnerability that exploits the language's prototypal inheritance model. Every plain object in JavaScript inherits from Object.prototype unless explicitly created otherwise, and that shared prototype is mutable by default. If code recursively merges an attacker-controlled object into another object without filtering "dunder" keys, an attacker can supply a key like __proto__ (or, in some historical Node.js versions, constructor.prototype) whose value is itself an object. When the merge logic walks into that nested object, it resolves __proto__ as a live reference to Object.prototype rather than as an ordinary data key, and assigns attacker-chosen properties directly onto it.

lodash.merge() and lodash.mergeWith() are recursive deep-merge utilities designed to combine nested objects and arrays — exactly the kind of "walk every key, including nested ones" logic that is most exposed to this class of bug. Prior to the fix, lodash performed no denylisting of __proto__ during the recursive merge, so a payload such as:

const _ = require('lodash');

const payload = JSON.parse('{"__proto__":{"polluted":"yes"}}');
_.merge({}, payload);

console.log({}.polluted); // "yes" — every object in the process is now affected

...was enough to taint Object.prototype for the lifetime of the process. Because so much of a typical Node.js application's control flow depends on property lookups on plain objects (feature flags, template defaults, ORM options, HTTP header parsing, template-engine settings), a single polluted key can flip authorization checks, disable input sanitization, or crash the process by injecting unexpected properties that downstream code did not defensively check for.

This was not an isolated lodash defect — it was one of the first widely publicized instances of a pattern that researcher Olivier Arteau formally named "prototype pollution" in a 2018 NorthSec talk, and lodash's popularity made it the reference example the security community used to explain the class of bug for years afterward.

Affected versions and components

  • lodash: all versions before 4.17.5 are affected via merge, mergeWith, and functions built on top of them (notably defaultsDeep).
  • lodash-es, lodash.merge, lodash.mergewith, lodash.defaultsdeep: the standalone per-method and ES-module packages shared the same vulnerable merge implementation and required independent updates.
  • Any application, framework, or npm package that calls _.merge(), _.mergeWith(), or _.defaultsDeep() on data that includes attacker-influenced keys — this covers a very large share of Express middleware, configuration loaders, and template-rendering helpers that used lodash to combine "defaults" objects with "user-supplied options" objects.
  • Because lodash is one of the most-depended-upon packages in the npm registry, the vulnerable code shipped transitively into an enormous number of applications that had no direct lodash entry in their own package.json — it arrived several levels deep in the dependency tree.

Fixed version: lodash 4.17.5 and later contain the initial patch. Note that this first fix was later found to be incomplete for some payload shapes; lodash 4.17.11 (CVE-2018-16487) and 4.17.19 (CVE-2020-8203) shipped additional hardening against related merge and zipObjectDeep-based bypasses. Teams remediating CVE-2018-3721 today should upgrade straight to the current release line rather than stopping at 4.17.5.

CVSS, EPSS, and KEV context

NVD scores CVE-2018-3721 as CVSS v3.0 5.6 (Medium), driven by a network attack vector, low attack complexity, low privileges required, no user interaction, and a high integrity impact with no confidentiality or availability impact captured in the base vector — a scoring that reflects the base prototype-pollution primitive rather than any specific downstream exploit chain. That distinction matters: the base CVSS score consistently understates real-world risk for this bug class, because the score models "an attribute got overwritten," not "that attribute controlled an auth check and the attacker got admin." Security teams that triage this CVE purely by its Medium severity score have historically underestimated it.

EPSS (Exploit Prediction Scoring System) rates CVE-2018-3721 low — consistent with most prototype-pollution advisories, since EPSS reflects observed internet-wide exploitation telemetry, and turning a lodash prototype-pollution primitive into a working exploit requires an application-specific "gadget" (a place downstream code trusts a polluted property) rather than a generic, reusable exploit. That low EPSS score is precisely why prototype pollution is dangerous in specific dependency call paths and easy to dismiss in aggregate vulnerability feeds: a fleet-wide scanner sees one low-EPSS CVE, while an application that pipes user input into _.merge() right before an authorization check has a fully exploitable bug hiding behind that same low score.

CVE-2018-3721 is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog. That does not mean it has never been exploited — prototype pollution chains are typically discovered and exploited at the application layer, where the resulting CVE (if one is even filed) is attributed to the vulnerable application, not to lodash. This is a recurring pattern with supply-chain vulnerabilities in widely-reused utility libraries: the KEV catalog tracks exploited end-to-end vulnerabilities, and a foundational library bug can be exploited many times over without the upstream CVE itself ever appearing there.

Timeline

  • Early 2018 — Prototype pollution as a JavaScript-specific vulnerability class is formalized and popularized, most notably via Olivier Arteau's NorthSec research, sharpening industry attention on merge/clone/extend-style utility functions across the npm ecosystem.
  • February 2018 — lodash ships 4.17.5, adding checks to prevent merge, mergeWith, and defaultsDeep from writing through __proto__ and other prototype-chain keys.
  • May 2018 — CVE-2018-3721 is published, formally cataloging the pre-4.17.5 behavior.
  • Following months — Downstream package maintainers across the npm ecosystem bump their lodash ranges, and dependency-scanning tools begin flagging the CVE broadly, given how deep and common the transitive dependency is.
  • Late 2018 — Researchers demonstrate that the original fix does not close every payload variant; this incomplete-fix finding is tracked separately as CVE-2018-16487, resolved in lodash 4.17.11.
  • 2019–2020 — Additional related merge and deep-clone prototype-pollution issues are identified and patched (CVE-2019-10744, CVE-2020-8203), reinforcing that this bug class required several iterative fixes rather than a single clean patch.
  • Present day — CVE-2018-3721 remains a common finding in software composition analysis (SCA) results for legacy Node.js codebases and unpatched transitive dependencies, particularly in vendored code, monorepos with mismatched internal registries, or applications pinned to older lodash majors for compatibility reasons.

Remediation steps

  1. Upgrade lodash to the latest 4.x release (4.17.21 or newer) across every package.json in your dependency tree — direct and transitive. Don't stop at 4.17.5; the follow-on CVEs mean only the current release line has the full set of merge-safety hardening.
  2. Regenerate and audit your lockfile (package-lock.json, yarn.lock, or pnpm-lock.yaml) after bumping the version, and confirm with npm ls lodash / yarn why lodash that no stale transitive copy of a vulnerable version remains pinned by an unrelated dependency.
  3. Pin or override transitive versions using overrides (npm), resolutions (Yarn), or pnpm.overrides when an upstream dependency you don't control still pulls in a vulnerable lodash range, so you aren't blocked waiting on that maintainer to release a fix.
  4. Audit call sites that merge untrusted input. Search your codebase for _.merge(, _.mergeWith(, and _.defaultsDeep( calls where any argument originates from request bodies, query parameters, headers, or uploaded files, and treat those as high-priority even after the library patch — defense in depth matters because new merge-style gadgets are periodically discovered.
  5. Harden the runtime as a second layer. Enable --disable-proto=throw (or delete) in Node.js where feasible, and consider Object.freeze(Object.prototype) at process start for services that don't legitimately need to extend the prototype at runtime.
  6. Add automated regression coverage. Include a unit test that asserts ({}).__proto__.polluted (or similar) is never set after your application's merge/config-loading paths run against a crafted __proto__ payload, so a future dependency downgrade or code change doesn't silently reintroduce the exposure.
  7. Re-scan and verify. Re-run your SCA/dependency scanner after remediation to confirm the finding clears, and check that no vendored or bundled copy of lodash (e.g., inside a webpack bundle, a Lambda layer, or a Docker base image) still carries the old version.

How Safeguard Helps

Vulnerability feeds will flag CVE-2018-3721 in almost every legacy JavaScript codebase, but that flood of Medium-severity findings is exactly where triage fatigue sets in — and it's exactly where reachability analysis earns its keep. Safeguard's reachability engine traces whether your application code actually calls _.merge(), _.mergeWith(), or _.defaultsDeep() with attacker-influenced input, so you can separate the instances that are genuinely exploitable from the ones buried in unreachable dead code or test fixtures. Griffin AI, Safeguard's autonomous triage agent, correlates that reachability signal with your call graph and CVE metadata to explain why a given finding matters in your specific environment, not just that it exists. Safeguard's SBOM generation and ingestion pipeline continuously maps every transitive copy of lodash across your services — including nested and vendored copies that manual npm ls audits routinely miss — so stale, patched-elsewhere versions don't quietly resurface. And when remediation is confirmed safe, Safeguard opens an auto-fix pull request that bumps the affected dependency and updates your lockfile, turning a multi-repo lodash upgrade from a week of tracking spreadsheets into a batch of reviewable, mergeable PRs.

Never miss an update

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