A single crafted object passed into _.defaultsDeep() is enough to rewrite the prototype of every Object in a Node.js process — silently adding properties that downstream code was never designed to see. That is the essence of CVE-2019-10744, a prototype pollution vulnerability in versions of lodash prior to 4.17.12 affecting the defaultsDeep function. Because lodash is one of the most widely depended-upon packages in the JavaScript ecosystem — pulled in transitively by thousands of frameworks, build tools, and application libraries — this vulnerability had an unusually large blast radius, and it remains a fixture in dependency scans of legacy Node.js and front-end bundles years after the patch shipped.
Prototype pollution bugs are deceptively easy to underestimate. They don't hand an attacker a shell directly; they corrupt the shared Object.prototype, which every object in the JavaScript runtime inherits from. Depending on what the polluted property is and how downstream code consumes it, the practical impact ranges from denial of service (crashing the process via unexpected property values) to authentication bypass, access-control bypass, and — in applications that pass polluted objects into child_process, template engines, or eval-adjacent sinks — full remote code execution. The vulnerability class is quiet at the point of injection and loud at the point of consequence, which is exactly why it is so often missed in code review.
What Went Wrong
_.defaultsDeep(object, sources...) recursively merges default values into a destination object, walking nested keys as it goes. The function did not exclude special property names like constructor, prototype, and __proto__ from that recursive merge. An attacker who can control the shape of an object passed as a source — commonly via JSON body parsing, query-string parsing, or any user-controlled deep-merge input — can supply a payload such as:
{"constructor": {"prototype": {"isAdmin": true}}}
When defaultsDeep walks into constructor.prototype, it treats prototype as just another key to assign into, and because constructor on a plain object resolves to Object, the assignment reaches Object.prototype itself. From that point forward, every plain object in the process — including ones that have nothing to do with the original request — inherits isAdmin: true unless it explicitly overrides the property. Any code elsewhere in the application that does a loose check like if (user.isAdmin) on a freshly created object can now be tricked into passing, even though nobody explicitly granted that flag.
This is the same underlying pollution pattern that showed up in lodash's merge, mergeWith, and zipObjectDeep around the same period (tracked under related CVEs), and it is a recurring theme across the broader JavaScript ecosystem in libraries like deep-extend, merge, and jQuery.extend — anywhere a library performs a recursive object merge without guarding the prototype chain.
Affected Versions and Components
- Package:
lodash(npm) - Affected versions: lodash
< 4.17.12 - Fixed version:
4.17.12and later - Vulnerable function:
defaultsDeep(related pollution paths also existed inmerge,mergeWith, anddefaultsin nearby versions) - Exposure path: any application, service, or build tool that calls
defaultsDeep(directly or transitively through a dependency) with any object whose shape is influenced by external input — request bodies, query parameters, configuration files sourced from untrusted locations, or npm package metadata processed by build tooling.
Because lodash is a transitive dependency of an enormous share of the npm ecosystem, the practical affected surface extends well past applications that call lodash directly. Many teams discover this CVE not in their own code, but nested three or four levels deep in a dependency tree — in a templating library, a config-merging utility, or an internal tool that hasn't been touched in years.
Severity, Exploit Likelihood, and Exploitation Status
CVE-2019-10744 is rated Critical, with a CVSS v3.1 base score of 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) in the National Vulnerability Database — reflecting a network-exploitable, low-complexity vulnerability with no privileges or user interaction required, and a theoretical worst case that spans confidentiality, integrity, and availability. In practice, real-world severity is highly context-dependent: the vulnerability is "critical" in the abstract because prototype pollution can be leveraged into RCE, denial of service, or auth bypass depending entirely on how the polluted object is subsequently consumed by the application. A codebase that merges attacker-controlled JSON directly into config objects consumed by a template engine is at far higher real-world risk than one that only ever calls defaultsDeep on static, internally defined option objects.
This CVE is not currently listed in CISA's Known Exploited Vulnerabilities (KEV) catalog, and we are not aware of confirmed in-the-wild exploitation campaigns specifically attributed to it. Its EPSS (Exploit Prediction Scoring System) probability has historically sat in a moderate band rather than the very top percentile reserved for actively-weaponized bugs — consistent with a vulnerability that is easy to demonstrate in a proof of concept but requires a specific, exploitable sink in the surrounding application to turn into a meaningful compromise. That said, given lodash's ubiquity, low EPSS should not be read as low priority: scanners and attackers alike treat any unpatched prototype-pollution primitive in a widely-deployed library as a standing opportunity, and PoC exploit code for defaultsDeep pollution has been public since shortly after disclosure.
Timeline
- June 2019 — The prototype pollution issue in
defaultsDeep(and related deep-merge functions) is reported to the lodash maintainers. - July 2019 — lodash 4.17.12 is released, adding guards that block writes through
constructor,prototype, and__proto__keys during recursive merges, closing the pollution path. - Late 2019 — CVE-2019-10744 is formally assigned and published to the NVD, cataloging the issue for dependency-scanning and vulnerability-management tooling.
- 2019–present — The CVE becomes a persistent long-tail finding: because lodash is so frequently pinned or vendored via transitive dependencies, this CVE continues to surface in SBOM and SCA scans of codebases that haven't refreshed their dependency tree in years, alongside its sibling prototype-pollution CVEs in the same library family.
Remediation Steps
- Upgrade lodash to 4.17.12 or later. This is the definitive fix — the patched versions strip dangerous keys (
__proto__,constructor,prototype) out of the recursive merge path used bydefaultsDeep,merge, and related functions. - Check for pinned or vendored copies. Run
npm ls lodash(or the Yarn/pnpm equivalent) to find every resolved version across your dependency tree, including nested copies pulled in by unrelated packages. A single outdated transitive copy is enough to leave the flaw exploitable if your application code — or any dependency — calls the vulnerable function on attacker-influenced input. - Force resolution where a direct upgrade isn't possible. If an upstream dependency hasn't bumped its own lodash pin, use
overrides(npm),resolutions(Yarn), orpnpm.overridesto force the patched version across the tree as an interim mitigation while you wait for or push the upstream fix. - Freeze the prototype defensively where feasible. In services that cannot be patched immediately,
Object.freeze(Object.prototype)(with careful regression testing) can act as a stopgap that blocks pollution attempts at the runtime level, independent of the library version. - Audit merge call sites for untrusted input. Search your codebase for
defaultsDeep,merge, andmergeWithcalls where any source argument originates from request bodies, query strings, headers, or externally supplied configuration, and add explicit key allow-lists or schema validation ahead of the merge. - Re-scan and confirm. After upgrading, regenerate your SBOM and re-run software composition analysis to confirm no vulnerable lodash version remains anywhere in the resolved dependency graph, including dev dependencies and build tooling.
How Safeguard Helps
Safeguard is built to cut through exactly this kind of long-tail, transitive-dependency noise. Our reachability analysis determines whether your application's actual code paths ever invoke defaultsDeep (or merge/mergeWith) on data that originates from an untrusted source, so your team can prioritize the handful of services where CVE-2019-10744 is truly exploitable instead of triaging every repository that happens to have an old lodash copy buried three levels deep. Griffin, Safeguard's AI security analyst, correlates that reachability signal with the specific merge call sites in your code, explains the pollution path in plain language, and drafts the justification your team needs for risk acceptance or urgent remediation. Continuous SBOM generation and ingestion keep every resolved lodash version — direct, transitive, and vendored — visible across your fleet as new services ship and dependency trees shift. And where an upgrade is safe and mechanical, Safeguard opens an auto-fix pull request bumping lodash to a patched release, so the fix lands in your CI pipeline as a reviewable diff rather than a ticket that sits in a backlog.