Recently reported by a security researcher, @daniel-msft, via email. I quote the exchange below. (I have made slight formatting tweaks to make it readable on GitHub, but not reviewed carefully. Anything that looks garbled in Daniel's messages may be my fault, and I haven't taken any care to preserve italics etc.)
Hello Kevin & Mark,
Thanks for the head's up (#685) on checking package.json for a better way to get a hold of you folks, apologies for not spotting it sooner. I'm reporting a denial-of-service vulnerability in jsdiff's diffJson() function, specifically in the canonicalize() helper.
SUMMARY
The canonicalize() function in src/diff/json.ts recursively traverses input objects with no depth limit. An attacker who can supply a deeply nested JSON object (~5,000 levels, approximately 50 KB) to any server-side code that calls diffJson() can crash the Node.js process with an uncaught RangeError (Maximum call stack size exceeded). This is a hard crash, not a slowdown, and can be repeated to sustain a denial of service.
AFFECTED FUNCTION
src/diff/json.ts: canonicalize() (lines 66-115)
Called unconditionally by castInput() whenever diffJson() receives an object argument. The function recurses once per nesting level of the input, with no depth guard, no iterative fallback, and no try/catch.
AFFECTED VERSIONS
Confirmed on diff@9.0.0 (latest at time of report). The vulnerability has been present since canonicalize() was introduced and is inherent to the recursive implementation.
REPRODUCTION
function makeNested(depth) {
let obj = {};
let cur = obj;
for (let i = 0; i < depth; i++) {
cur.child = {};
cur = cur.child;
}
return obj;
}
// This crashes the process:
diffJson(makeNested(5000), {});
// RangeError: Maximum call stack size exceeded
Tested on:
- Windows, Node.js v24.14.0 (win32 x64): crash threshold ~4,789 levels
- Linux (WSL2 Ubuntu 24.04), Node.js v24.15.0 (x64): crash threshold ~4,476 levels
IMPACT
- CVSS 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
- CWE-674: Uncontrolled Recursion
- Any server-side application that passes attacker-controlled JSON to diffJson() without a try/catch wrapper is directly vulnerable.
- The payload (~50 KB) is well within default HTTP body size limits.
- 397M monthly npm downloads, 4,153 dependent packages.
SUGGESTED FIX
Option A (minimal): Add a depth parameter with a safe default:
obj, stack, replacementStack, replacer, key, depth = 0
) {
if (depth > 1000) {
throw new Error('diffJson: input exceeds maximum nesting depth');
}
// ... existing logic, pass depth + 1 on recursive calls
}
Option B (robust): Convert to an iterative implementation using an explicit stack. The existing stack/replacementStack arrays used for circular reference detection already provide scaffolding for this.
I'm happy to provide detail, if needed. Thank you for maintaining this widely-used library.
Best regards,
Daniel Cervera
Microsoft Security Engineering
Thanks for the report.
I don't really see how option A is a fix. Currently, too much recursion causes a RangeError to be thrown implicitly. After, an Error would be thrown explicitly. Both are catchable. How would the latter be any better?
I'm also not inclined to consider this as a vulnerability. You envisage a situation where diffJson is called in a webserver application with the payload of a HTTP request and hits the recursion limit, triggering a RangeError. But that won't crash the application; request handlers throwing exceptions when something goes wrong is a possibility that webserver frameworks expect and handle. (Typically, the default behaviour is something like printing the exception details to stderr and emitting a response with HTTP status code 500.)
I guess option B would be a genuine improvement on the status quo, but for the reason above I'm not convinced that the security angle you're seeing makes any sense. This is just a case of a library function that throws an exception on some inputs and doesn't warn of this possibility in the docs. Not ideal but neither especially unusual nor something I'd normally expect to be viewed as a vuln. There's plenty of library functions in the world that can sometimes throw an exception due to a bug in the library, and it surely can't make sense to consider them all denial-of-service vulnerabilities!
(In due course I intend to migrate this convo to the public issue tracker but will hold off in case you think I'm missing something and want to object.)
Cheers,
Mark
Hey Mark,
Thanks for taking the time with me on this response! I've raised this with my colleagues. You're right on both points, and we're adapting internally, responsive to your inputs here.
Option B is the right behavior change. Iterative or depth-capped, the function shouldn't be a stack-depth amplifier on adversarial input. If you ship that as a public quality fix, no objection from me.
Please go ahead and migrate the conversation to the public issue tracker whenever it's convenient. Thanks for engaging this seriously.
Best,
Daniel Cervera
Microsoft Security Engineering
Recently reported by a security researcher, @daniel-msft, via email. I quote the exchange below. (I have made slight formatting tweaks to make it readable on GitHub, but not reviewed carefully. Anything that looks garbled in Daniel's messages may be my fault, and I haven't taken any care to preserve italics etc.)