How a Malicious JSON Crashes Node.js Servers via Async Hooks and the New Fix
The recent Node.js security release patches eight vulnerabilities, most notably a stack‑overflow bug triggered by deep recursive promises when async_hooks is enabled, which allows a crafted JSON payload to terminate the process, and the fix modifies TryCatchScope to re‑throw stack‑overflow errors instead of exiting.
Node.js recently published a security release that fixes eight vulnerabilities across active versions 20.x, 22.x, 24.x and 25.x, affecting applications that use React Server Components, Next.js, or APM monitoring tools.
Where the problem originates
In Node.js, the async_hooks API tracks the lifecycle of asynchronous operations. It is used by React Server Components, Next.js, and APM tools such as Datadog and New Relic, making it a core infrastructure component.
When code performs deep recursion that overflows the call stack, Node.js normally throws a RangeError: Maximum call stack size exceeded which can be caught by a try‑catch block, allowing the process to continue. However, if async_hooks is enabled, the process exits with code 7 without invoking try‑catch or uncaughtException, causing the application to die.
Why it is easy to trigger
The bug requires four conditions:
Async hooks are enabled (e.g., via AsyncLocalStorage or an APM tool).
The code contains deep recursion.
Recursion creates a Promise, thereby triggering async hooks.
The recursion depth reaches the point of stack overflow.
These conditions often co‑occur in real‑world code. For example, a Next.js API route that processes user‑uploaded JSON might look like this:
export default async function handler(req, res) {
try {
const data = req.body;
const result = processNestedData(data);
res.json({ success: true, result });
} catch (err) {
// You think this catches the error? Naïve.
console.error('Processing failed:', err);
res.status(500).json({ error: 'Processing failed' });
}
}
function processNestedData(data) {
if (Array.isArray(data)) {
return data.map(item => processNestedData(item));
}
return transform(data);
}If an attacker sends a JSON array nested thousands of levels deep, the server does not return a 500 error; instead, the entire process exits, aborting all concurrent requests – a classic denial‑of‑service vector.
Technical cause
The root cause lies in how async_hooks handles new Promise(). V8 synchronously calls a promise hook, which triggers a Node.js async‑hooks callback, adding an extra stack frame for each promise. When recursive promises are created, both user code frames and async‑hooks frames accumulate.
When the stack finally overflows, the error is thrown while the async‑hooks callback is active. Node.js wraps these callbacks in a special error handler TryCatchScope::kFatal, which treats any error as unrecoverable and exits the process.
How Node.js fixed it
The fix adds a check in the destructor of TryCatchScope. If a caught exception is identified as a stack‑overflow error, it is re‑thrown to user code instead of being treated as fatal.
TryCatchScope::~TryCatchScope() {
if (HasCaught() && mode_ == CatchMode::kFatal) {
Local<Value> exception = Exception();
// Detect stack overflow? Re‑throw instead of exiting
if (IsStackOverflowError(env_->isolate(), exception)) {
ReThrow();
Reset();
return;
}
// Other fatal errors: exit as before
FatalException(/* ... */);
}
}With this change, a stack‑overflow error can be caught by normal try‑catch logic.
Why this is only a mitigation
The Node.js blog notes that the fix is a mitigation, not a fundamental solution, because stack overflow behavior is not defined by the ECMAScript specification. Relying on engines to throw a catchable RangeError is therefore risky.
Best practice: limit recursion depth or replace recursive algorithms with iterative ones when handling untrusted input.
function processNestedData(data, maxDepth = 100) {
function process(item, depth) {
if (depth > maxDepth) {
throw new Error('Nesting too deep');
}
if (Array.isArray(item)) {
return item.map(child => process(child, depth + 1));
}
return transform(item);
}
return process(data, 0);
}Other CVEs fixed in the release
CVE-2025-55131 (high) : Buffer allocation race condition leading to uninitialized memory leaks.
CVE-2025-55130 (high) : Crafted symlink paths bypass filesystem permissions.
CVE-2025-59465 (high) : Malformed HTTP/2 HEADERS frames can crash the server.
CVE-2026-21636 (medium) : Unix Domain Socket can bypass permission model.
CVE-2026-21637 (medium) : TLS PSK/ALPN callback exceptions may crash the process or leak file descriptors.
CVE-2025-59464 (medium) : Memory leak when handling TLS client certificates.
CVE-2025-55132 (low) : fs.futimes() can bypass read‑only permissions to modify timestamps.
Affected versions
Patches are available for all active releases:
Node.js 25.3.0 (current)
Node.js 24.13.0 (LTS)
Node.js 22.22.0 (LTS)
Node.js 20.20.0 (LTS)
Older, unmaintained versions (≤ 18.x) will not receive fixes. Users of newer versions (24 and above) are not affected by the stack‑overflow issue because AsyncLocalStorage now uses the V8 AsyncContextFrame API instead of async_hooks. However, any APM tool that still calls async_hooks.createHook() remains vulnerable.
Recommendations
Upgrade to the patched versions as soon as possible.
Audit code for deep, untrusted recursion and add depth limits or iterative alternatives.
Verify that APM integrations do not rely on the legacy async_hooks.createHook() API.
Community reaction
Developers on Twitter joked that "Next.js is cursed" while Node.js core maintainer Matteo Collina blamed "React" for the root cause, pointing to the use of AsyncLocalStorage in React Server Components. The discussion highlights the importance of correctly attributing issues to the underlying layer rather than the framework built on top of it.
Final thoughts
This incident shows that widely‑used infrastructure can rely on undocumented engine behavior. While Node.js acted quickly to mitigate the problem, developers should adopt defensive programming practices and avoid assuming that runtime edge cases are always handled gracefully.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
