You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: apps/site/pages/en/blog/vulnerability/january-2026-dos-mitigation-async-hooks.md
+57-57Lines changed: 57 additions & 57 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,5 +1,5 @@
1
1
---
2
-
date: 2026-01-08T17:00:00.000Z
2
+
date: 2026-01-13T17:00:00.000Z
3
3
category: vulnerability
4
4
title: Mitigating Denial-of-Service Vulnerability from Unrecoverable Stack Space Exhaustion for React, Next.js, and APM Users
5
5
slug: january-2026-dos-mitigation-async-hooks
@@ -20,7 +20,7 @@ Node.js/V8 makes a best-effort attempt to recover from stack space exhaustion wi
20
20
21
21
Due to the prevalence of this usage pattern in frameworks, including but not limited to React/Next.js, a significant part of the ecosystem is expected to be affected.
22
22
23
-
The bug fix is included in a security release because of its widespread impact on the ecosystem. However, this is only a mitigation for the general risk that lies in the ecosystem’s dependence on recoverable stack space exhaustion for service availability.
23
+
The bug fix is included in a security release because of its widespread impact on the ecosystem. However, this is only a mitigation for the general risk that lies in the ecosystem's dependence on recoverable stack space exhaustion for service availability.
24
24
25
25
**For users of these frameworks/tools and server hosting providers**: Update as soon as possible.
26
26
@@ -174,31 +174,44 @@ A user sending deeply nested JSON can crash your entire server:
174
174
**Without `async_hooks`**: `try-catch` catches the `RangeError`, returns 500, server continues
175
175
**With `async_hooks` (React/Next.js)**: Server crashes immediately with exit code 7
176
176
177
-
## A Brief History: From async_hooks to AsyncContextFrame
177
+
## Why This Affects Every APM User
178
178
179
-
Understanding this bug requires knowing how Node.js evolved its async context tracking.
179
+
Application Performance Monitoring (APM) tools are essential infrastructure for production applications. They track request latency, identify bottlenecks, trace errors to their source, and alert teams when something goes wrong. Companies use APM tools like Datadog, New Relic, Dynatrace, Elastic APM, and OpenTelemetry to maintain visibility into their distributed systems.
180
180
181
-
### The async_hooks Era
181
+
To provide this functionality, APM tools need to follow a request as it flows through your application, even across async boundaries. When an HTTP request comes in, is processed by middleware, queries a database, calls an external API, and finally returns a response, the APM needs to correlate all of these operations into a single trace. This requires async context tracking.
182
182
183
-
[`async_hooks`](https://nodejs.org/api/async_hooks.html) was introduced in [Node.js 8 (2017)](https://nodejs.org/en/blog/release/v8.0.0) as a low-level API to track asynchronous resources. It provides callbacks (`init`, `before`, `after`, `destroy`) that fire at key points in an async resource's lifecycle. APM tools immediately adopted it to trace requests across async boundaries.
183
+
Most modern APM tools use `AsyncLocalStorage` (which is built on `async_hooks` in versions of Node.js before Node 24) to propagate trace context across async operations. The moment you `require('dd-trace')`, `require('newrelic')`, or initialize OpenTelemetry, your application has `async_hooks` enabled.
184
184
185
-
However, `async_hooks` has significant performance overhead. Every Promise creation, every timer, and every I/O operation triggers these callbacks. This cost is unavoidable when the hooks are enabled.
185
+
The irony is notable: the tools you install to monitor and debug crashes can make a category of crashes behave differently. This is not the fault of the APM tools; they are using Node.js APIs exactly as intended.
186
186
187
-
### AsyncLocalStorage
187
+
##Why This Is Only a Mitigation, and The Vulnerability Lies Elsewhere
188
188
189
-
[Node.js 12.17.0 (2020)](https://nodejs.org/en/blog/release/v12.17.0) introduced [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), a higher-level API built on top of `async_hooks`. It provides a cleaner interface for the most common use case: storing context that flows through async operations (like request IDs, user sessions, or tracing spans).
189
+
While this issue has significant practical impact, we want to be clear about why Node.js is treating this fix as a mere mitigation of security vulnerability risks at large:
190
190
191
-
React Server Components and Next.js adopted `AsyncLocalStorage` for request context tracking, unknowingly inheriting all of `async_hooks` behaviors, including this bug.
191
+
### Stack Space Exhaustion Is Not Specified Behavior
192
192
193
-
### The AsyncContextFrame Revolution (Node.js 24+)
193
+
The "Maximum call stack size exceeded" error is not part of the ECMAScript specification. [The specification does not impose any limit, assuming infinite stack space](https://tc39.es/ecma262/#execution-context-stack); imposing a limit and throwing an error is simply behavior that JavaScript engines implement on a best-effort basis. Building a security model on top of an undocumented, unspecified feature that isn't guaranteed to work consistently would be unreliable.
194
194
195
-
In [Node.js 24](https://nodejs.org/en/blog/release/v24.0.0), `AsyncLocalStorage` was reimplemented using a new V8 feature called [`AsyncContextFrame`](https://github.com/tc39/proposal-async-context). This approach integrates context tracking directly into V8's Promise implementation, eliminating the need for JavaScript callbacks on every async operation.
195
+
It's worth noting that even when ECMAScript specifies that [proper tail calls](https://tc39.es/ecma262/#sec-tail-position-calls)[should reuse stack frames](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model#tail_calls), this is not implemented by most JavaScript engines today, including V8. And in the few JavaScript engines that do implement it, proper tail calls can block an application with infinite recursion instead of hitting the stack size limit at some point and stopping with an error, which is also a Denial-of-Service factor. This reinforces that stack overflow behavior cannot be relied upon for defending against Denial-of-Service attacks.
196
196
197
-
The result is dramatically better performance. Importantly for this bug, `AsyncLocalStorage` no longer uses `async_hooks.createHook()` internally. This is why React and Next.js are not affected by this bug on Node.js 24+.
197
+
### V8 Doesn't Treat This as a Security Issue
198
198
199
-
Note: `AsyncLocalStorage`is still exported from the `async_hooks` module for backwards compatibility, even though it no longer uses the `async_hooks` machinery internally on Node.js 24+. It's also available from `node:async_hooks` and the newer `node:async_context` module.
199
+
The stack space handling in Node.js is primarily implemented by V8. JavaScript engines developed for browsers have a different security model, and they do not treat crashes like this as security vulnerabilities ([example](https://issues.chromium.org/issues/432385241)). This means similar bugs reported in the upstream will not go through vulnerability disclosure procedures, making any security classification by Node.js alone ineffective.
200
200
201
-
For more details on this evolution and its performance implications, see [The Hidden Cost of Context](https://blog.platformatic.dev/the-hidden-cost-of-context).
201
+
### uncaughtException Limitations
202
+
203
+
The `uncaughtException` handler is not designed to recover the process after it fires. The Node.js documentation explicitly warns against this pattern. Specifically, the documentation states that ["Exceptions thrown from within the event handler will not be caught. Instead, the process will exit with a non-zero exit code, and the stack trace will be printed. This is to avoid infinite recursion."](https://nodejs.org/api/process.html#warning-using-uncaughtexception-correctly)
204
+
205
+
Trying to invoke the handler after the call stack size is exceeded would itself throw. The fact that it works without promise hooks is largely coincidental rather than guaranteed behavior.
206
+
207
+
### Why We Put It In a Security Release
208
+
209
+
Although it is a bug fix for an unspecified behavior, we chose to include it in the security release because of its widespread impact on the ecosystem.
210
+
React Server Components, Next.js, and virtually every APM tool are affected. The fix improves developer experience and makes error handling more predictable.
211
+
212
+
However, it's important to note that we were fortunate to be able to fix this particular case. There's no guarantee that similar edge cases involving stack overflow and `async_hooks` can always be addressed. **For mission-critical paths that must defend against infinite recursion or stack overflow from recursion whose depth can be controlled by an attacker, always sanitize the input or impose a limit on the depth of recursion by other means**.
213
+
214
+
It's worth noting that large array allocations can suffer from similar issues, like the recent [`qs`](https://github.com/ljharb/qs) vulnerability [CVE-2025-15284](https://github.com/ljharb/qs/security/advisories/GHSA-6rw7-vpxm-498p) showed. It's paramount that developers validate and constrain resource usage that could be controlled by an attacker. The runtime cannot always recover reliably from resource exhaustion after-the-fact.
202
215
203
216
## Technical Deep Dive
204
217
@@ -292,45 +305,6 @@ The complete sequence when stack overflow occurs:
292
305
293
306
The error originated in **user code** (the recursive pattern), but because it manifests while the hook callback is the active frame, it's treated as a fatal hook error.
294
307
295
-
## Why This Is Only a Mitigation, and The Vulnerability Lies Elsewhere
296
-
297
-
While this issue has significant practical impact, we want to be clear about why Node.js is treating this fix as a mere mitigation of security vulnerability risks at large:
298
-
299
-
### Stack Space Exhaustion Is Not Specified Behavior
300
-
301
-
The "Maximum call stack size exceeded" error is not part of the ECMAScript specification. [The specification does not impose any limit, assuming infinite stack space](https://tc39.es/ecma262/#execution-context-stack); imposing a limit and throwing an error is simply behavior that JavaScript engines implement on a best-effort basis. Building a security model on top of an undocumented, unspecified feature that isn't guaranteed to work consistently would be unreliable.
302
-
303
-
It's worth noting that even when ECMAScript specifies that [proper tail calls](https://tc39.es/ecma262/#sec-tail-position-calls) [should reuse stack frames](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model#tail_calls), this is not implemented by most JavaScript engines today, including V8. And in the few JavaScript engines that do implement it, proper tail calls can block an application with infinite recursion instead of hitting the stack size limit at some point and stopping with an error, which is also a Denial-of-Service factor. This reinforces that stack overflow behavior cannot be relied upon for defending against Denial-of-Service attacks.
304
-
305
-
### V8 Doesn't Treat This as a Security Issue
306
-
307
-
The stack space handling in Node.js is primarily implemented by V8. JavaScript engines developed for browsers have a different security model, and they do not treat crashes like this as security vulnerabilities ([example](https://issues.chromium.org/issues/432385241)). This means similar bugs reported in the upstream will not go through vulnerability disclosure procedures, making any security classification by Node.js alone ineffective.
308
-
309
-
### uncaughtException Limitations
310
-
311
-
The `uncaughtException` handler is not designed to recover the process after it fires. The Node.js documentation explicitly warns against this pattern. Specifically, the documentation states that ["Exceptions thrown from within the event handler will not be caught. Instead, the process will exit with a non-zero exit code, and the stack trace will be printed. This is to avoid infinite recursion."](https://nodejs.org/api/process.html#warning-using-uncaughtexception-correctly)
312
-
313
-
Trying to invoke the handler after the call stack size is exceeded would itself throw. The fact that it works without promise hooks is largely coincidental rather than guaranteed behavior.
314
-
315
-
## Why This Affects Every APM User
316
-
317
-
Application Performance Monitoring (APM) tools are essential infrastructure for production applications. They track request latency, identify bottlenecks, trace errors to their source, and alert teams when something goes wrong. Companies use APM tools like Datadog, New Relic, Dynatrace, Elastic APM, and OpenTelemetry to maintain visibility into their distributed systems.
318
-
319
-
To provide this functionality, APM tools need to follow a request as it flows through your application, even across async boundaries. When an HTTP request comes in, is processed by middleware, queries a database, calls an external API, and finally returns a response, the APM needs to correlate all of these operations into a single trace. This requires async context tracking.
320
-
321
-
Most modern APM tools use `AsyncLocalStorage` (which is built on `async_hooks` in versions of Node.js before Node 24) to propagate trace context across async operations. The moment you `require(‘dd-trace’)`, `require(‘newrelic’)`, or initialize OpenTelemetry, your application has `async_hooks` enabled.
322
-
323
-
The irony is notable: the tools you install to monitor and debug crashes can make a category of crashes behave differently. This is not the fault of the APM tools; they are using Node.js APIs exactly as intended.
324
-
325
-
### Why We Put It In a Security Release
326
-
327
-
Although it is a bug fix for an unspecified behavior, we chose to include it in the security release because of its widespread impact on the ecosystem.
328
-
React Server Components, Next.js, and virtually every APM tool are affected. The fix improves developer experience and makes error handling more predictable.
329
-
330
-
However, it's important to note that we were fortunate to be able to fix this particular case. There's no guarantee that similar edge cases involving stack overflow and `async_hooks` can always be addressed. **For mission-critical paths that must defend against infinite recursion or stack overflow from recursion whose depth can be controlled by an attacker, always sanitize the input or impose a limit on the depth of recursion by other means**. The runtime cannot guarantee reliable recovery from stack space exhaustion with a catchable error.
331
-
332
-
It's worth noting that large array allocations can suffer from similar issues, like the recent [`qs`](https://github.com/ljharb/qs) vulnerability [CVE-2025-15284](https://github.com/ljharb/qs/security/advisories/GHSA-6rw7-vpxm-498p) showed. It's paramount that developers validate and constrain resource usage that could be controlled by an attacker. The runtime cannot always recover reliably from resource exhaustion after-the-fact.
333
-
334
308
## The Fix
335
309
336
310
The fix detects stack overflow errors and re-throws them to user code instead of treating them as fatal:
@@ -361,6 +335,32 @@ After this fix:
361
335
- Applications can handle the error gracefully
362
336
- Behavior is more consistent with and without `async_hooks` enabled
363
337
338
+
## A Brief History: From async_hooks to AsyncContextFrame
339
+
340
+
Understanding this bug requires knowing how Node.js evolved its async context tracking.
341
+
342
+
### The async_hooks Era
343
+
344
+
[`async_hooks`](https://nodejs.org/api/async_hooks.html) was introduced in [Node.js 8 (2017)](https://nodejs.org/en/blog/release/v8.0.0) as a low-level API to track asynchronous resources. It provides callbacks (`init`, `before`, `after`, `destroy`) that fire at key points in an async resource's lifecycle. APM tools immediately adopted it to trace requests across async boundaries.
345
+
346
+
However, `async_hooks` has significant performance overhead. Every Promise creation, every timer, and every I/O operation triggers these callbacks. This cost is unavoidable when the hooks are enabled.
347
+
348
+
### AsyncLocalStorage
349
+
350
+
[Node.js 12.17.0 (2020)](https://nodejs.org/en/blog/release/v12.17.0) introduced [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), a higher-level API built on top of `async_hooks`. It provides a cleaner interface for the most common use case: storing context that flows through async operations (like request IDs, user sessions, or tracing spans).
351
+
352
+
React Server Components and Next.js adopted `AsyncLocalStorage` for request context tracking, unknowingly inheriting all of `async_hooks` behaviors, including this bug.
353
+
354
+
### The AsyncContextFrame Revolution (Node.js 24+)
355
+
356
+
In [Node.js 24](https://nodejs.org/en/blog/release/v24.0.0), `AsyncLocalStorage` was reimplemented using a new V8 feature called [`AsyncContextFrame`](https://github.com/tc39/proposal-async-context). This approach integrates context tracking directly into V8's Promise implementation, eliminating the need for JavaScript callbacks on every async operation.
357
+
358
+
The result is dramatically better performance. Importantly for this bug, `AsyncLocalStorage` no longer uses `async_hooks.createHook()` internally. This is why React and Next.js are not affected by this bug on Node.js 24+.
359
+
360
+
Note: `AsyncLocalStorage` is still exported from the `async_hooks` module for backwards compatibility, even though it no longer uses the `async_hooks` machinery internally on Node.js 24+. It's also available from `node:async_hooks` and the newer `node:async_context` module.
361
+
362
+
For more details on this evolution and its performance implications, see [The Hidden Cost of Context](https://blog.platformatic.dev/the-hidden-cost-of-context).
363
+
364
364
## Affected Versions
365
365
366
366
**Patched releases available for:**
@@ -394,7 +394,7 @@ The impact on React Server Components and Next.js varies by Node.js version:
394
394
395
395
## Mitigation
396
396
397
-
**Recommended**: Upgrade to the patched versions released on January 8th, 2026.
397
+
**Recommended**: Upgrade to the patched versions released on January 13th, 2026.
398
398
399
399
If you cannot upgrade immediately, consider altering your application to avoid deep recursion, particularly when allocating promises within recursive functions.
400
400
@@ -409,15 +409,15 @@ If you cannot upgrade immediately, consider altering your application to avoid d
409
409
-**December 12, 2025**: Anna Henningsen identifies a blocker for this strategy. The Node.js team starts brainstorming on alternative solutions.
410
410
-**December 16, 2025**: Joyee Cheung communicates that Node.js cannot treat this as a vulnerability for the reasons listed in this blog post.
411
411
-**December 17, 2025**: Anna Henningsen fixes the blocking issue for the patch.
412
-
-**January 8, 2026**: Patched versions released and disclosure published
412
+
-**January 13, 2026**: Patched versions released and disclosure published
413
413
414
414
## Conclusion
415
415
416
416
This bug highlights how deeply `async_hooks` has become embedded in the Node.js ecosystem. What started as a low-level debugging API is now a critical dependency for React Server Components, Next.js, every major APM tool, and any code using `AsyncLocalStorage`.
417
417
418
418
The fix improves the consistency of stack size limit errors caused by deep recursions. While we were able to address this particular case, developers should be aware that stack overflow behavior is not specified by ECMAScript and should not be relied upon for service availability. If the depth of recursion can be controlled by an attacker, always sanitize the input or impose a limit by other means to restrict the depth, instead of counting on the JS runtime to impose a limit or recover from it with a catchable error.
419
419
420
-
**Users running React RSC, Next.js, or any other framework using `AsyncLocalStorage`, as well as any APM tool in production, should upgrade to the patched versions released on January 8th, 2026.**
420
+
**Users running React RSC, Next.js, or any other framework using `AsyncLocalStorage`, as well as any APM tool in production, should upgrade to the patched versions released on January 13th, 2026.**
0 commit comments