Skip to content

Commit 66b49c1

Browse files
mcollinajoyeecheung
andcommitted
Update apps/site/pages/en/blog/vulnerability/january-2026-dos-mitigation-async-hooks.md
Co-authored-by: Joyee Cheung <joyeec9h3@gmail.com> Signed-off-by: Matteo Collina <matteo.collina@gmail.com>
1 parent e095ef6 commit 66b49c1

File tree

1 file changed

+57
-57
lines changed

1 file changed

+57
-57
lines changed

apps/site/pages/en/blog/vulnerability/january-2026-dos-mitigation-async-hooks.md

Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
date: 2026-01-08T17:00:00.000Z
2+
date: 2026-01-13T17:00:00.000Z
33
category: vulnerability
44
title: Mitigating Denial-of-Service Vulnerability from Unrecoverable Stack Space Exhaustion for React, Next.js, and APM Users
55
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
2020

2121
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.
2222

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 ecosystems 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.
2424

2525
**For users of these frameworks/tools and server hosting providers**: Update as soon as possible.
2626

@@ -174,31 +174,44 @@ A user sending deeply nested JSON can crash your entire server:
174174
**Without `async_hooks`**: `try-catch` catches the `RangeError`, returns 500, server continues
175175
**With `async_hooks` (React/Next.js)**: Server crashes immediately with exit code 7
176176

177-
## A Brief History: From async_hooks to AsyncContextFrame
177+
## Why This Affects Every APM User
178178

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.
180180

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.
182182

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.
184184

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.
186186

187-
### AsyncLocalStorage
187+
## Why This Is Only a Mitigation, and The Vulnerability Lies Elsewhere
188188

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:
190190

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
192192

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.
194194

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.
196196

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
198198

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.
200200

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.
202215

203216
## Technical Deep Dive
204217

@@ -292,45 +305,6 @@ The complete sequence when stack overflow occurs:
292305
293306
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.
294307
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-
334308
## The Fix
335309
336310
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:
361335
- Applications can handle the error gracefully
362336
- Behavior is more consistent with and without `async_hooks` enabled
363337

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+
364364
## Affected Versions
365365

366366
**Patched releases available for:**
@@ -394,7 +394,7 @@ The impact on React Server Components and Next.js varies by Node.js version:
394394

395395
## Mitigation
396396

397-
**Recommended**: Upgrade to the patched versions released on January 8th, 2026.
397+
**Recommended**: Upgrade to the patched versions released on January 13th, 2026.
398398

399399
If you cannot upgrade immediately, consider altering your application to avoid deep recursion, particularly when allocating promises within recursive functions.
400400

@@ -409,15 +409,15 @@ If you cannot upgrade immediately, consider altering your application to avoid d
409409
- **December 12, 2025**: Anna Henningsen identifies a blocker for this strategy. The Node.js team starts brainstorming on alternative solutions.
410410
- **December 16, 2025**: Joyee Cheung communicates that Node.js cannot treat this as a vulnerability for the reasons listed in this blog post.
411411
- **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
413413

414414
## Conclusion
415415

416416
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`.
417417

418418
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.
419419

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.**
421421

422422
## Acknowledgments
423423

0 commit comments

Comments
 (0)