lodash (npm)
Registry: npm Weekly Downloads: ~25,000,000 (as of 2026-04-12) Repository: https://github.com/lodash/lodash Security Contact: none listed Disclosure Policy: none listed Current Status: audit-ingested
Audit History
| Date | Auditor | Scope | Methodology | Findings | Source | |------|---------|-------|-------------|----------|--------| | 2026-04-12 | @travis-burmaster | full-source (all path-walking functions) | hybrid (manual review + automated) | 3 new findings + all CVE patches verified | oss-security-kb |
Audit scope: lodash 4.18.1 (latest), single-file build lodash.js (17,259 lines, 817 functions). Systematic review of all 120+ path-walking functions for prototype pollution and related injection vectors. Commit cb0b9b9 (lodash/lodash main branch, 2026-04-12).
Findings
Critical
C1: Template Code Injection via _.template()
Severity: Critical (CWE-94 -- Code Injection) Status: By design, but frequently misused
_.template() compiles template strings using the Function() constructor (line 14992) with no sanitization of template content. The <% %> evaluate delimiter injects directly into the function body:
// RCE if attacker controls template string
_.template('<%= constructor.constructor("return process")() %>')({})
The variable option has reForbiddenIdentifierChars guards (line 181), but the template string itself is unrestricted. Any attacker-controlled string reaching _.template() is a direct RCE vector.
Code: lodash.js:14882-14992
Impact: Remote code execution in any application passing user input to _.template(). Common in server-side rendering, email templating, and configuration processing.
Medium
M1: Prototype Method Invocation via _.invoke()
Severity: Medium (CWE-470 -- Use of Externally-Controlled Input to Select Classes or Code)
_.invoke() follows any path including __proto__ and constructor.prototype, then calls the resolved function via apply() with no path sanitization:
// Successfully calls Object.prototype.hasOwnProperty with Object.prototype as this
_.invoke({}, '__proto__.hasOwnProperty', 'x')
Code: lodash.js:3260
Impact: Attacker can invoke arbitrary prototype methods with controlled arguments. Not directly exploitable for property writes but could be a chain in a larger attack.
M2: Prototype Method Invocation via _.result()
Severity: Medium (CWE-470)
Same vector as invoke -- traverses any path, calls functions with parent object as this, no sanitization:
_.result({}, 'constructor.prototype.toString')
Code: lodash.js:13722
CVE Patch Verification (All Patched Correctly)
| CVE | Patched Function | Defense Mechanism | Verified |
|-----|------------------|-------------------|----------|
| CVE-2019-10744 | defaultsDeep | safeGet returns undefined for constructor (forces new object creation instead of traversing to Object.prototype) | Yes |
| CVE-2020-8203 | set, setWith, update, updateWith, zipObjectDeep | baseSet line 4040: blocks __proto__, constructor, prototype on every path segment | Yes |
| GHSA-xxjr-mmjv-4gpg | unset, omit | baseUnset line 4393/4399: blocks __proto__ and non-terminal constructor/prototype | Yes |
Defense-in-Depth Architecture (Verified)
Lodash 4.18.1 uses three layers of prototype pollution defense:
-
baseSet guard (line 4040) -- Checks every path segment against
__proto__,constructor,prototype. Returns early on match. Protects: set, setWith, update, updateWith, pick, pickBy, zipObjectDeep. -
safeGet (line 6704) -- Returns
undefinedfor__proto__(unconditionally) andconstructor(when value is a function). Protects: merge, mergeWith, defaultsDeep, baseMergeDeep. -
baseAssignValue (line 2597) -- Uses
Object.definePropertyfor__proto__writes, creating an own data property rather than traversing the prototype chain. Protects: clone, cloneDeep, assignValue, zipObject.
Bypass Vectors Tested (All Blocked)
| Vector | Result |
|--------|--------|
| Unicode escapes (\u005f\u005fproto\u005f\u005f) | Blocked -- resolved at parse time, string check matches |
| Bracket notation in paths (["__proto__"]) | Blocked -- stringToPath extracts literal string |
| Custom toString() returning "proto" | Blocked -- toKey runs before checks |
| constructor.prototype via _.set | Blocked at line 4040 |
| constructor.prototype via _.merge | Blocked by safeGet returning undefined |
| constructor.prototype via _.defaultsDeep | Blocked -- same merge internals |
| Array paths bypassing string checks | Blocked -- checks operate on individual keys after toKey |
| Proxy/Symbol keys | Blocked -- cannot equal string "proto" |
| Null byte in path strings | Blocked -- no null-byte handling gaps in stringToPath |
Confirmed Safe Functions
| Function Group | Status |
|----------------|--------|
| set, setWith, update, updateWith | Protected by baseSet guard |
| merge, mergeWith, defaultsDeep | Protected by safeGet + baseAssignValue |
| clone, cloneDeep, cloneDeepWith | Uses defineProperty for proto (own property) |
| pick, pickBy | Protected by baseSet guard |
| zipObject, zipObjectDeep | baseAssignValue + baseSet guards |
| at, get, baseGet | Read-only (minor info leak via prototype chain traversal) |
| has, hasIn, hasPath | Read-only (info leak via in operator) |
| unset, omit | proto/constructor checks in baseUnset |
| customDefaultsMerge | safeGet + baseAssignValue + keysIn non-enumeration |
Known Vulnerabilities
| CVE / Issue | Severity | Description | Fixed in | Source |
|-------------|----------|-------------|----------|--------|
| CVE-2019-10744 | High | Prototype pollution in defaultsDeep | 4.17.12 | snyk |
| CVE-2020-8203 | High | Prototype pollution via zipObjectDeep, set, setWith, update, updateWith | 4.17.19 | GHSA-p6mc-m468-83gw |
| GHSA-xxjr-mmjv-4gpg | High | Prototype pollution in _.unset and _.omit | 4.17.23+ | GHSA |
Security Posture Notes
- All three known prototype pollution CVEs are correctly patched in 4.18.1 with defense-in-depth (3 independent layers).
- No new prototype pollution vectors found in any of the 120+ path-walking functions.
- The
_.template()code injection (C1) is the primary remaining risk -- it is architectural (usesFunction()constructor) and frequently misused. _.invoke()and_.result()can invoke prototype methods with attacker-controlled arguments but cannot write properties.- Lodash is a model for effective post-CVE remediation: the fixes are comprehensive, layered, and bypass-resistant.
Recommendations for Developers
- Never pass user input to
_.template()-- treat it aseval(). Use a sandboxed template engine (Handlebars, Mustache, Nunjucks with autoescaping) for user-controlled templates. - Never pass user-controlled paths to
_.invoke()or_.result()-- validate paths against an allowlist. - Ensure you're on lodash >= 4.17.21 -- earlier versions have unpatched prototype pollution.
- Consider lodash-es or per-method imports to reduce attack surface.
Open Questions (Resolved)
- ~~Has any full-source modern audit been published post-2020 fixes?~~ Resolved: This is the first systematic audit of all path-walking functions. All CVE patches verified effective.
- ~~Are there remaining dangerous deep-object mutation patterns not covered by prior fixes?~~ Resolved: No new prototype pollution vectors found. Template code injection and method invocation via invoke/result are the remaining risks.
- ~~Which downstream packages still expose vulnerable versions indirectly?~~ Open: Transitive dependency audit of vulnerable lodash versions remains unperformed.
Related Pages
- [[npm/index]]
- [[npm/minimist]]
- [[npm/express]]
Last updated: 2026-04-12 | Sources: 6 (upstream repository, npm registry, source code audit of lodash 4.18.1, CVE databases, Snyk, GitHub Security Advisories) Auditor contact: @travis-burmaster