Why Most Lodash Vulnerability Reports Get Rejected
Written by Ulises Gascón
Jul 22, 2026 — 9 min readAround 95% of the vulnerability reports we receive for Lodash get rejected. Not out of gatekeeping, and not because we disagree with the facts. Most come from people genuinely trying to make the ecosystem safer, and many describe a real security problem: the proof of concept runs, and something breaks. We close them anyway, because a working PoC is not the same as a library vulnerability. The flaw exists, but it lives in the application, in the JavaScript runtime, or in Lodash's own documented behavior.
In The Future of Lodash we set out a plan to rebuild the project's security posture, and much of it now exists: a public threat model, a security team, and CVEs issued through the OpenJS Foundation's CNA. None of that, on its own, decides where a given report falls: the threat model draws the line, but each report still has to be triaged against it individually. The rest of this post is about how we draw that line, so you can work out where your own finding falls before you file a report.
The numbers
Since we turned on GitHub security advisories, the inbox looks like this:
- 50 reports this year alone, out of 64 total and many now AI-assisted.
- Around 95% closed as duplicate, invalid, or out of scope.
- 3 became CVEs: two prototype pollution (
_.unsetand_.omit), one code injection through_.templateimports.
What counts as a Lodash bug
We measure every report against the Lodash threat model. Its core idea is shared responsibility: some things are the library's job to get right, and some are the consumer's. If you want the background, my talk What is a Vulnerability and What's Not? walks through it. We borrowed a lot from the Node.js and Express threat models, but ours comes down to a single sentence:
Lodash is a utility library operating entirely within the trust boundary of its caller. Vulnerabilities in scope are limited to cases where Lodash fails to uphold its documented behavior in the presence of untrusted input, without assuming compromise of trusted components such as the runtime, the operating system, or the invoking application code.
A failure in any of those trusted components is out of scope, no matter how real the impact. Lodash trusts five things:
- The JavaScript runtime. If you can reproduce your finding without Lodash, it is probably a runtime bug (V8, SpiderMonkey, Node core), not a library one.
- The host environment. If JavaScript's own built-ins (
Object,Array,Function) were tampered with before Lodash loaded, the compromise is upstream. - The calling code. The application that invokes Lodash is the input-validation layer; Lodash does not decide what is safe to pass in.
- Dependencies and surrounding code. Typosquats, tampered packages, or code that overwrites or wraps Lodash are all upstream, not bugs in the library.
- Process privileges. Lodash inherits whatever the process holds; running as root is an operational concern.
What Lodash does not trust is the data you pass into its functions. It cannot know which keys are sensitive in your application, which paths your users control, or which template strings are static in your code. Those decisions are yours. If you can make Lodash break its documented behavior with input the caller could reasonably hand it, that is a library bug. Everything else is the application's job.
None of this means the vulnerability is not real. A report being out of scope for Lodash does not close the hole in your application. That is why the shared responsibility in the threat model matters: the half that is yours does not disappear because Lodash declined to own it. Knowing where that line falls is what keeps your users safe, not the CVE.
The patterns we keep closing
Over time, the same rejected reports keep coming back in two shapes: a denial of service and a prototype pollution. Each finding is real, but the fix is in the application, not in Lodash.
The DoS that crashes the app
DoS reports arrive in a few flavors: a deeply nested object passed to _.merge or _.cloneDeep, a pathological string handed to a regex, an input large enough to exhaust memory. They share a shape. A normal operation works right up until the input reaches the far edge of its range, then the process dies with something like RangeError: Maximum call stack size exceeded.
That edge is not unique to Lodash. Every abstraction has one. Try it yourself: open a browser console and run this, no library involved:
const copy = []
// A normal-sized array copies fine
copy.push(...Array.from({ length: 1000 }, (_, i) => i))
// The same call on a large one blows the stack
copy.push(...Array.from({ length: 500000 }, (_, i) => i))
// RangeError: Maximum call stack size exceeded
The first call works. The second turns half a million elements into half a million arguments, and the stack gives out. Array.prototype.push is not vulnerable, and neither is the spread operator. They have an edge, and a large enough input finds it.
Whether reaching that edge is a vulnerability comes down to one question: does it break normal, reasonable usage? If Lodash fell over on ordinary inputs, that would be our bug, and we would fix it. But a deeply nested object built to exhaust the stack, or a regex crafted to backtrack forever, is not ordinary input; it is a payload aimed at the edge. And "reasonable" is subjective. Only the application can define it, because Lodash is general-purpose and cannot know what nesting depth is normal for your API and what is an attack.
So the guardrails live one layer up: cap request body size, reject inputs past a maximum depth, set per-request timeouts. And if your application genuinely needs to work at that scale, treat it as the edge case it is and stress-test it, because a half-million-property object will break in more places than Lodash: your serializer, your database driver, the JSON parser that built it. The _.merge crash is usually the first thing to give, not the only one.
The prototype pollution PoC that works
Here is a simplified prototype pollution PoC with the textbook shape (for the wider tour of how these bugs work, see Olivier Arteau's talk Prototype Pollution Attacks in Node.js Applications). At a glance it looks like a global compromise. Open a browser console and run it, no library involved, and watch where the write actually lands:
const data = JSON.parse('{"__proto__":{"polluted":"yes"}}')
const c = Object.assign({}, data)
// looks polluted!
console.log(c.polluted) // 'yes'
c.polluted really is 'yes', so at a glance the PoC works. Now look closer:
// other objects are fine
console.log(({}).polluted) // undefined
// the global prototype is untouched
console.log(Object.prototype.polluted) // undefined
// only c was reparented
console.log(Object.getPrototypeOf(c) === Object.prototype) // false
Nothing global happened. The __proto__ key tripped the prototype setter, so only c got a new prototype; Object.prototype and every other object stay untouched. Where the write lands is what makes it a vulnerability, and this one landed on a single throwaway object.
That does not clear the caller. If untrusted input you forwarded reaches Object.prototype, the threat model is clear about whose bug that is:
If a developer intentionally merges user input into global objects or fails to isolate data structures, that is a misuse of Lodash's documented API, not a Lodash defect.
So check your own code first: where the data came from, and whether you isolated it.
The mirror image is the real library bug: the same untrusted input reaching a shared prototype through nothing but documented usage. That is what is worth reporting.
What "valid" actually looks like
Let's explore a valid one, CVE-2019-10744. On Lodash 4.17.0, defaultsDeep walked a constructor.prototype path straight into the shared prototype:
// Lodash 4.17.0 (CVE-2019-10744)
const _ = require('lodash')
const payload = '{"constructor":{"prototype":{"polluted":true}}}'
_.defaultsDeep({}, JSON.parse(payload))
const newObject = {}
console.log(newObject.polluted) // true, every object in the process
console.log(Object.prototype.polluted) // true, the global prototype really changed
These are the reports we act on: Lodash breaking its own documented contract, with no misuse from the calling application to explain it. A few others, out of every Lodash CVE on record, are worth exploring:
- Deletion.
_.unsetand_.omitdeleting keys off shared prototypes on a normal object with a plain string path (CVE-2025-13465; an array-wrapped path later bypassed the fix in CVE-2026-2950). I trace the root cause and fix in this deep dive. - Code injection.
_.templatecompiling untrusted input into executable code, straight into aFunction()sink (CVE-2026-4800; CVE-2021-23337 was the earlier one). We consider it insecure and advise against its use, and plan to remove it in Lodash 5.
Before you file, ask the right question
Telling your own finding apart from the ones above comes down to a single question:
Can you state, in one sentence, what Lodash did wrong on input the application was right to hand it?
If you can, you probably have a real report. If you cannot, it will most likely close out of scope. These four checks are how you get to that sentence:
- Where does the attacker control the input? If the application forwarded it, the bug is the application's.
- Is the API documented as unsafe with untrusted input? If so, it is already known, not new.
- Would plain JavaScript exhibit the same behavior? If so, it is not a Lodash bug.
- Does it need a second bug to be exploitable? If so, report that other bug instead.
I learn a lot from the reports you send, valid or not, and each one has helped us draw this line more clearly. I hope this article helps you see where it falls. If you can write that one sentence, open a report. The door is open, and thank you for sending them.