lodash: prototype pollution in _.unset/_.omit deletes global prototype methods
What's the vulnerability?
lodash's _.unset(object, path) and _.omit(object, paths) accept a string
path, parse it into segments (dot / bracket notation), and walk those
segments on the target object. Through 4.17.22 they do not reject
dangerous segments such as __proto__, constructor, or prototype.
Because someObject.__proto__ is the live Object.prototype, a crafted path
string lets an attacker steer _.unset out of the intended object and into a
built-in prototype, then delete a method from it. The deletion affects the
prototype globally for the rest of the Node.js process, so every object of
that type loses the method — a denial-of-service / integrity issue
(prototype pollution by deletion). It permits deleting members but not
overwriting their behavior.
Any code that forwards attacker-influenced key names into _.unset / _.omit
(for example a property path taken from JSON request input) is exploitable.
Root Cause Analysis
# RCA Report: CVE-2025-13465
## Summary
CVE-2025-13465 is a prototype-pollution-by-deletion vulnerability in lodash's `_.unset` and `_.omit` functions. When a user-controlled string path containing `__proto__` is passed to these functions, lodash traverses into `Object.prototype` and deletes the targeted property globally. This affects every object in the Node.js process, causing denial-of-service or integrity degradation. The issue is fixed in lodash 4.17.23 by adding a guard in `baseUnset` that rejects dangerous string path segments (`__proto__`, `constructor.prototype`).
## Impact
- **Package**: `lodash` (npm)
- **Affected versions**: 4.0.0 through 4.17.22 (inclusive)
- **Fixed version**: 4.17.23
- **Risk level**: Moderate (CVSS 3.1 base 5.3)
- **Consequences**: Any application that forwards attacker-controlled key names into `_.unset` or `_.omit` (e.g., from JSON request input) can have built-in prototype methods deleted globally, leading to denial-of-service or unexpected behavior across the entire process.
## Root Cause
The root cause lies in the `baseUnset` function (shared by `_.unset` and `_.omit`). Before the fix, `baseUnset` parsed the path string into segments and walked each segment on the target object without validating whether a segment was a dangerous prototype accessor such as `__proto__`, `constructor`, or `prototype`.
Because `someObject.__proto__` returns the live `Object.prototype`, a crafted path like `__proto__.toString` causes the function to traverse out of the intended object and into the global prototype, then delete the specified key from it.
**Fix commit**: [`edadd452146f7e4bad4ea684e955708931d84d81`](https://github.com/lodash/lodash/commit/edadd452146f7e4bad4ea684e955708931d84d81)
The fix adds a loop in `baseUnset` that iterates over path segments and:
1. Skips non-string keys.
2. Blocks `__proto__` anywhere in the path if it is not an own property of the current object.
3. Blocks `constructor.prototype` chains, with a narrow exception for primitive roots (e.g., `_.unset(0, 'constructor.prototype.a')`).
## Reproduction Steps
The reproduction script is `repro/reproduction_steps.sh`.
What the script does:
1. Installs the last published vulnerable version (`lodash@4.17.21`) in a temporary directory.
2. Runs a Node.js harness (`test_app.js`) that simulates an application receiving untrusted user input (`__proto__.toString`) and passing it to `_.unset({}, untrustedInput)`.
3. Captures the `typeof Object.prototype.toString` before and after the call.
4. Repeats steps 1–3 with the fixed version (`lodash@4.17.23`).
5. Also runs a secondary check with `_.omit({}, '__proto__.toString')` on the vulnerable build.
6. Writes a JSON runtime manifest (`repro/runtime_manifest.json`) with versions, exit codes, log paths, and explicit confirmation markers.
**Expected evidence of reproduction:**
- **Vulnerable build**: `typeof Object.prototype.toString` changes from `"function"` to `"undefined"`, and the harness exits with code `2` emitting `VULN_CONFIRMED`.
- **Fixed build**: `typeof Object.prototype.toString` stays `"function"`, and the harness exits with code `0` emitting `FIX_CONFIRMED`.
## Evidence
Log files captured by the script:
- `logs/vuln_unset.log` — Vulnerable `_.unset` test output.
- `logs/fixed_unset.log` — Fixed `_.unset` test output.
- `logs/vuln_omit.log` — Vulnerable `_.omit` test output.
- `repro/runtime_manifest.json` — Structured manifest linking all evidence.
Key excerpts from `logs/vuln_unset.log`:
```
lodash version: 4.17.21
untrusted path input: __proto__.toString
typeof Object.prototype.toString BEFORE: function
typeof Object.prototype.toString AFTER: undefined
VULN_CONFIRMED: prototype pollution via _.unset in lodash 4.17.21
```
Key excerpts from `logs/fixed_unset.log`:
```
lodash version: 4.17.23
untrusted path input: __proto__.toString
typeof Object.prototype.toString BEFORE: function
typeof Object.prototype.toString AFTER: function
FIX_CONFIRMED: prototype intact in lodash 4.17.23
```
Key excerpts from `logs/vuln_omit.log`:
```
lodash version: 4.17.21
typeof Object.prototype.toString BEFORE: function
typeof Object.prototype.toString AFTER: undefined
VULN_CONFIRMED_OMIT: _.omit pollutes prototype
```
Environment:
- Node.js v22.22.2
- npm 10.9.7
## Recommendations / Next Steps
- **Upgrade guidance**: Projects using lodash 4.17.21 or earlier should upgrade to 4.17.23 or later immediately. Note that a separate array-path bypass (CVE-2026-2950) exists and is fixed in 4.18.0, so upgrading to 4.18.0+ is recommended for full protection.
- **Code review**: Audit all calls to `_.unset` and `_.omit` that accept user-controlled path strings. If upgrading is not immediately possible, sanitize path segments to reject `__proto__`, `constructor`, and `prototype` before passing them to lodash.
- **Testing**: Add regression tests that exercise `__proto__` and `constructor.prototype` paths against `_.unset` and `_.omit` to prevent future regressions.
## Additional Notes
- **Idempotency**: `repro/reproduction_steps.sh` has been executed twice consecutively with identical results (exit 0, same log outputs).
- **Version caveat**: The ticket specifies 4.17.22 as the last vulnerable version, but this version was never published to npm or tagged in Git. The reproduction uses `4.17.21` as the vulnerable baseline because it is the last published version that exhibits the flaw. The fix commit `edadd452` is included in `4.17.23`, which is published and confirmed to block the attack.
- **Edge cases**: The fix specifically targets *string* path segments. A later bypass using array paths (`['__proto__', 'toString']`) was assigned a separate CVE (CVE-2026-2950) and is out of scope for this ticket.
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00134 pruva-verify GHSA-xxjr-mmjv-4gpg pruva-verify CVE-2025-13465 curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00134/artifacts/reproduction_steps.sh Make executable
chmod +x reproduction_steps.sh Run the script
./reproduction_steps.sh How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available