{"id":"VND-nodejs-january-2026-dos-mitigation-async-hooks","kind":"vulnerability","aliases":[],"related":["CVE-2025-15284"],"title":"Node.js: Mitigating Denial-of-Service Vulnerability from Unrecoverable Stack Space Exhaustion for React, Next.js, and APM Users","description":"Commercial support for versions past the Maintenance LTS phase is available through our OpenJS Ecosystem Sustainability Program partners Mitigating Denial-of-Service Vulnerability from Unrecoverable Stack Space Exhaustion for React, Next.js, and APM Users MC JC Matteo Collina, Joyee Cheung Mitigating Denial-of-Service Vulnerability from Unrecoverable Stack Space Exhaustion for React, Next.js, and APM Users TL;DR\nNode.js/V8 makes a best-effort attempt to recover from stack space exhaustion with a catchable error, which frameworks have come to rely on for service availability. An edge case that reproduces only when async_hooks are enabled breaks this recovery path: when recursion in user code exhausts stack space, Node.js exits immediately with exit code 7 instead of throwing a recoverable error. This can be reproduced in countless applications because:\nReact Server Components use AsyncLocalStorage\nNext.js uses AsyncLocalStorage for request context tracking\nOther frameworks may also use AsyncLocalStorage for request context tracking\nMost APM tools (Datadog, New Relic, Dynatrace, Elastic APM, OpenTelemetry) use AsyncLocalStorage or async_hooks.createHook to trace requests\nThe weakness ultimately lies in the ecosystem&#x27;s reliance on an unspecified behavior in the language - recovery from stack space exhaustion - for service availability ( CWE-758 ). Given the widespread use of async_hooks by popular frameworks and APM tools, the aforementioned edge case can expose this weakness more frequently and can present a Denial‑of‑Service vector for many applications. Node.js shipped a mitigation in the January 2026 security release to make this unspecified behavior more consistent, reducing the chance of reproduction. However, the weakness remains in the ecosystem until applications and frameworks move away from relying on unspecified behavior for availability.\nFor users of these frameworks/tools and server hosting providers : Update as soon as possible.\nFor libraries and frameworks : apply more robust defenses against stack space exhaustion to ensure service availability (e.g., limit recursion depth or avoid recursion if the depth can be controlled by an attacker). A recoverable RangeError: Maximum call stack size exceeded is only an unspecified behavior maintained with best-effort, and cannot be depended on for security guarantees.\nThe Reproduction\nWhen a stack overflow occurs in user code while async_hooks is enabled, Node.js immediately exits with code 7 instead of allowing try-catch blocks to catch the error. This is a special condition in Node.js that skips the process.on(&#x27;uncaughtException&#x27;) handlers, making the exception uncatchable.\nimport { createHook } from &#x27; node:async_hooks &#x27; ;\n// This simulates what APM tools do\ncreateHook ( { init () {} } ) . enable () ;\nfunction recursive () {\nnew Promise ( () =&gt; {} ) ; // Creates async context\nreturn recursive () ;\n}\ntry {\nrecursive () ;\n} catch ( err ) {\nconsole . log ( &#x27; This never runs &#x27; , err ) ;\n} JavaScript Copy to clipboard\nExpected : try-catch catches the RangeError\nActual : Immediate crash with exit code 7\nWhy This Affects React and Next.js\nReact Server Components\nReact 18+ uses AsyncLocalStorage (which is built on async_hooks ) to track the rendering context for Server Components:\n// Inside React&#x27;s internals\nimport { AsyncLocalStorage } from &#x27; node:async_hooks &#x27; ;\nconst asyncLocalStorage = new AsyncLocalStorage () ;\n// Every server component render creates async context\nasync function renderServerComponent ( Component , props ) {\nreturn asyncLocalStorage . run ( { request : currentRequest }, async () =&gt; {\nreturn &lt; Component {... props } /&gt;;\n} ) ;\n} JavaScript Copy to clipboard\nNext.js Request Context\nNext.js uses AsyncLocalStorage to track request context, cookies, headers, and more:\n// Simplified from Next.js internals\nimport { AsyncLocalStorage } from &#x27; node:async_hooks &#x27; ;\nexport const requestAsyncStorage = new AsyncLocalStorage () ;\n// Every request creates async context\nexport function handleRequest ( req , res ) {\nreturn requestAsyncStorage . run ( { req , res }, async () =&gt; {\n// Your page/API handler runs here\n} ) ;\n} JavaScript Copy to clipboard\nThe Real-World Scenario\nConsider a Next.js API route that processes user-submitted JSON:\n// pages/api/process.js\nexport default async function handler ( req , res ) {\ntry {\nconst data = req . body ;\nconst result = processNestedData ( data ) ; // Deeply nested = stack overflow\nres . json ( { success : true , result } ) ;\n} catch ( err ) {\n// THIS CATCH BLOCK NEVER RUNS\nconsole . error ( &#x27; Processing failed: &#x27; , err ) ;\nres . status ( 500 ) . json ( { error : &#x27; Processing failed &#x27; } ) ;\n}\n}\nfunction processNestedData ( data ) {\nif ( Array . isArray ( data )) {\nreturn data . map ( item =&gt; processNestedData ( item )) ;\n}\nreturn transform ( data ) ;\n} JavaScript Copy to clipboard\nA user sending deeply nested JSON can crash your entire server:\n[[[[[[[[[[[[[[[[[[[[[[[[ /* 50,000 levels deep */ ]]]]]]]]]]]]]]]]]]]]]]]]\nWithout async_hooks : try-catch catches the RangeError , returns 500, server continues\nWith async_hooks (React/Next.js) : Server crashes immediately with exit code 7\nWhy the Use of APM Tools Makes It Easier to Reproduce\nApplication 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.\nTo 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.\nMost 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(&#x27;dd-trace&#x27;) , require(&#x27;newrelic&#x27;) , or initialize OpenTelemetry, your application has async_hooks enabled.\nThe 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.\nWhy This Is Only a Mitigation, and The Vulnerability Lies Elsewhere\nWhile 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:\nStack Space Exhaustion Behavior Is Not Specified\nThe &quot;Maximum call stack size exceeded&quot; error is not part of the ECMAScript specification. The specification does not impose any limit and assumes infinite stack space . Imposing a limit and throwing a recoverable error are only behaviors that JavaScript engines implement with best-effort. Applications and frameworks are already at risk when they build a security model on top of these unspecified behaviors that are not guaranteed to reproduce consistently, see:\nCWE-758: Reliance on Undefined, Unspecified, or Implementation-Defined Behavior\nCWE-674: Uncontrolled Recursion\nIt&#x27;s worth noting that even when ECMAScript specifies that proper tail calls should reuse stack frames , this is not implemented by most JavaScript engines today, including V8. And in the few JavaScript engines that do implement it, proper tail calls (as used in the reproduction above ) can block an application with infinite recursion instead of hitting the stack size limit at some point and stopping with an error, which is another Denial-of-Service vector. This reinforces that stack overflow behavior cannot be relied upon for defending against Denial-of-Service attacks.\nThis Behavior Is Not Part of The Security Guarantees of V8\nIn Node.js, the stack space usage from JavaScript function calls is primarily implemented by V8. JavaScript engines developed for browsers have a different security model, and they do not triage crashes like this as security vulnerabilities. This means similar behavior inconsistencies reported in the upstream (like this ) are not guaranteed to go through vulnerability disclosure procedures, making any security classification by Node.js alone ineffective.\nuncaughtException Limitations\nThe 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 &quot;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.&quot;\nTrying 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.\nWhy We Put It In a Security Release\nAlthough it is a patch for an unspecified behavior, we chose to include it in the security release because of its widespread impact on the ecosystem. The prevalence of async_hooks usage by React Server Components, Next.js, and APM tools makes it a practical Denial-of-Service vector for many applications.\nMaking the unspecified behavior more consistent in Node.js improves developer experience and makes error handling more predictable.\nHowever, it&#x27;s important to note that we were fortunate to be able to fix this particular case. There&#x27;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 .\nIt&#x27;s worth noting that large array allocations can suffer from similar issues, like the recent qs vulnerability CVE-2025-15284 showed. It&#x27;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.\nTechnical Deep Dive\nHow async_hooks Works\nWhen you create a Promise, async_hooks fires callbacks to track the async context:\nnew Promise()\n→ V8 promise hook triggered\n→ async_hooks init callback runs\n→ Your hook code executes (e.g., APM span creation)\nThe Fatal TryCatchScope\nNode.js wraps async_hooks callbacks in a special error handler called TryCatchScope with CatchMode::kFatal :\n// From Node.js internals\nvoid EmitAsyncInit ( /* ... */ ) {\nTryCatchScope try_catch ( env , TryCatchScope :: CatchMode :: kFatal ) ;\n// Run async_hooks callback\n} C++ Copy to clipboard\nkFatal means: &quot;If any error occurs here, it&#x27;s unrecoverable. Exit immediately.&quot;\nThis behavior is documented and intentional. From the async_hooks documentation :\nIf any AsyncHook callbacks throw, the application will print the stack trace and exit. The exit path does follow that of an uncaught exception, but all &#x27;uncaughtException&#x27; listeners are removed, thus forcing the process to exit.\nThe reason for this error handling behavior is that these callbacks are running at potentially volatile points in an object&#x27;s lifetime, for example during class construction and destruction. Because of this, it is deemed necessary to bring down the process quickly in order to prevent an unintentional abort in the future.\nThis design makes sense: if your APM tool&#x27;s init callback throws an error, the application is in an undefined state. The hook might have partially executed, resources might be leaked, and continuing could cause d","summary":"When user code recurses until the stack is exhausted while async_hooks is enabled, Node.js exits immediately with code 7 instead of throwing a catchable RangeError. An attacker who can send deeply nested input to an application that uses AsyncLocalStorage or APM hooks can crash the whole server.","severity":"critical","cvss":null,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"node","fixed":[],"affected":"","ecosystem":"node"},{"name":"node","fixed":[],"affected":"8.x","ecosystem":"node"},{"name":"node","fixed":[],"affected":"18.x","ecosystem":"node"},{"name":"node","fixed":[],"affected":"20.x","ecosystem":"node"},{"name":"node","fixed":[],"affected":"22.x","ecosystem":"node"},{"name":"node","fixed":[],"affected":"24.x","ecosystem":"node"},{"name":"node","fixed":[],"affected":"25.x","ecosystem":"node"}],"ecosystems":["node"],"tags":["apm","next.js","node","react","runtime","self-hosted"],"not_affected_if":["no-async-hooks","no-untrusted-recursion"],"how_to_check":"- Run `node --version` and compare against the fixed releases listed in the Node.js January 2026 security advisory.\n- Check whether the app loads `dd-trace`, `newrelic`, OpenTelemetry, or uses `AsyncLocalStorage` or `async_hooks.createHook` (for example via React Server Components or Next.js).\n- Check whether user-controlled JSON or other input is processed by unbounded recursion.","fix":"- Node.js: upgrade to the patched release from the January 2026 security release (exact versions unknown from the sources).\n- Limit recursion depth or cap nesting depth of user-supplied input, and reject overly deep payloads.\n- Apply framework updates for React, Next.js, and APM tools as their maintainers release them.","status":"open","published_at":"2026-01-13T17:00:00+00:00","first_seen_at":"2026-10-23T19:13:00+00:00","triaged_at":"2026-10-25T05:37:31+00:00","updated_at":"2026-10-25T05:37:31+00:00","category":"runtime","confidence":0.6,"triage":{"at":"2026-10-25T05:37:31Z","model":"claude-haiku-5-5","output":{"fix":"- Node.js: upgrade to the patched release from the January 2026 security release (exact versions unknown from the sources).\n- Limit recursion depth or cap nesting depth of user-supplied input, and reject overly deep payloads.\n- Apply framework updates for React, Next.js, and APM tools as their maintainers release them.","reach":"internet","title":"Node.js: deep recursion with async_hooks enabled crashes the process","impact":"medium","summary":"When user code recurses until the stack is exhausted while async_hooks is enabled, Node.js exits immediately with code 7 instead of throwing a catchable RangeError. An attacker who can send deeply nested input to an application that uses AsyncLocalStorage or APM hooks can crash the whole server.","category":"runtime","nicknames":[],"who_cares":["node","next.js","react","apm","self-hosted"],"confidence":0.6,"needs_auth":"unknown","worst_case":"Server process crashes on a single crafted request, causing denial of service.","analyst_note":"Fixed Node.js version numbers and per-branch patch versions are not in the extracted text; check https://nodejs.org/en/blog/vulnerability/january-2026-dos-mitigation-async-hooks for exact fixed versions and whether the mitigation is complete.","how_to_check":"- Run `node --version` and compare against the fixed releases listed in the Node.js January 2026 security advisory.\n- Check whether the app loads `dd-trace`, `newrelic`, OpenTelemetry, or uses `AsyncLocalStorage` or `async_hooks.createHook` (for example via React Server Components or Next.js).\n- Check whether user-controlled JSON or other input is processed by unbounded recursion.","needs_analyst":true,"not_affected_if":["no-untrusted-recursion","no-async-hooks"],"who_is_affected":"Applications where attacker-controlled input drives deep recursion and async_hooks is enabled, which the advisory says is common in React Server Components, Next.js, and most APM setups. Servers that do not use async_hooks or AsyncLocalStorage, or that never recurse on untrusted input, are less exposed.","user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.001151,"input_tokens":7607,"output_tokens":780,"severity_before":"critical"},"analyst_status":null,"analyst_note":null,"search":"'-758':289C '/v8':154C '18':549C '2026':23A,331C '7':63B,210C,458C,538C 'actual':532C 'affect':541C 'aforement':304C 'allow':461C 'also':240C 'apm':17A,36B,85B,125C,148C,248C,255C,301C,496C 'appli':384C 'applic':80B,223C,323C,354C 'async':27A,54B,188C,295C,449C,491C,507C,556C,574C,585C,587C,597C,627C 'async_hooks.createhook':260C 'asynclocalstorag':83B,229C,232C,242C,258C,551C,571C,577C,579C,593C,610C,624C 'attack':71B,411C 'attempt':160C 'avail':102C,180C,287C,365C,395C 'avoid':401C 'away':358C 'behavior':276C,338C,363C,424C 'best':158C,428C 'best-effort':157C,427C 'block':465C 'break':192C 'built':554C 'call':416C 'cannot':431C 'case':183C,306C 'catch':464C,467C,513C,528C,529C 'catchabl':68B,169C 'chanc':343C 'cheung':132C 'clipboard':524C,604C 'code':46B,62B,200C,209C,447C,457C,537C 'collina':130C 'come':174C 'commerci':92C 'compon':227C,547C,565C,582C,590C,599C 'condit':474C 'consist':340C 'consol':515C 'const':576C,630C 'context':235C,245C,508C,562C,586C,607C,614C 'control':408C 'cooki':615C 'copi':522C,602C 'countless':222C 'crash':88B,534C 'creat':506C,584C 'createhook':488C,499C 'currentrequest':596C 'cwe':288C 'datadog':250C 'deepli':75B 'defens':387C 'denial':4A,112C,135C,317C 'denial-of-servic':3A,111C,134C 'depend':433C 'depth':399C,405C 'dos':25A 'dos-mitigation-async-hooks':24A 'dr':151C 'dynatrac':253C 'e.g':396C 'ecosystem':106C,270C,352C 'edg':182C,305C 'effort':159C,429C 'elast':254C 'enabl':57B,191C,452C,501C 'ensur':393C 'err':514C,520C 'error':170C,216C,469C 'everi':580C 'exceed':419C 'except':485C 'exhaust':12A,52B,120C,143C,166C,201C,284C,391C 'exit':59B,205C,208C,455C,536C 'expect':525C 'export':629C 'expos':308C 'framework':172C,238C,299C,356C,383C 'frameworks/tools':370C 'frequent':312C 'function':502C,588C 'given':290C 'guarante':437C 'handler':482C 'header':616C 'hook':55B,86B,189C,296C,450C,492C,557C,575C,628C 'hooks':28A 'host':43B,373C 'howev':346C 'immedi':60B,206C,454C,533C 'import':487C,570C,623C 'init':500C 'input':77B 'insid':566C 'instead':64B,211C,459C 'intern':569C,622C 'januari':330C 'january':22A 'javascript':521C,601C 'jc':128C 'joye':131C 'languag':279C 'librari':381C 'lie':267C 'limit':397C 'log':516C 'lts':99C 'maintain':425C 'mainten':98C 'make':155C,335C,483C 'mani':322C 'matteo':129C 'maximum':415C 'may':239C 'mc':127C 'mitig':2A,110C,133C,327C 'mitigation':26A 'move':357C 'nest':76B 'never':518C 'new':251C,504C,578C 'next.js':15A,37B,123C,146C,230C,544C,605C,608C,621C 'node':29A,30A,31A,32A,33A,34A,35A,38B,490C,573C,626C 'node.js':1A,58B,153C,204C,324C,453C,476C 'node.js/v8':152C 'nodejs':21A 'occur':444C 'openj':105C 'opentelemetri':256C 'overflow':443C 'partner':109C 'past':96C 'path':195C 'phase':100C 'popular':298C 'possibl':379C 'present':315C 'process.on':480C 'program':108C 'promis':505C 'prop':591C,600C 'provid':374C 'rangeerror':69B,414C,531C 'react':14A,39B,122C,145C,225C,542C,545C,548C,567C 'recov':162C 'recover':215C,413C 'recoveri':194C,280C 'recurs':47B,197C,398C,402C,503C,510C,512C 'reduc':341C 'releas':333C 'reli':176C,360C 'relianc':272C 'relic':252C 'remain':349C 'render':561C,583C 'renderservercompon':589C 'reproduc':185C,220C 'reproduct':345C,439C 'request':234C,244C,263C,595C,606C,613C 'requestasyncstorag':631C 'return':509C,592C,598C 'robust':386C 'run':519C,594C 'runtim':40B 'secur':332C,436C 'self':42B 'self-host':41B 'send':74B 'server':91B,226C,372C,546C,564C,581C 'servic':6A,114C,137C,179C,286C,319C,394C 'ship':325C 'simplifi':619C 'simul':494C 'size':418C 'skip':478C 'soon':377C 'space':11A,119C,142C,165C,203C,283C,390C 'special':473C 'stack':10A,50B,118C,141C,164C,202C,282C,389C,417C,442C 'support':93C 'sustain':107C 'throw':66B,213C 'tl':150C 'tool':249C,302C,497C 'trace':262C 'track':236C,246C,559C,612C 'tri':463C,511C,527C 'try-catch':462C,526C 'ultim':266C 'uncatch':486C 'uncaughtexcept':481C 'unrecover':9A,117C,140C 'unspecifi':275C,337C,362C,423C 'updat':375C 'use':82B,228C,231C,241C,257C,293C,550C,609C 'user':18A,45B,126C,149C,199C,367C,446C 'vector':320C 'version':95C 'vnd':20A 'vnd-nodejs-january':19A 'vulner':7A,115C,138C 'weak':265C,310C,348C 'whole':90B 'widespread':292C","nicknames":[],"mitigations":[],"who_affected":"Applications where attacker-controlled input drives deep recursion and async_hooks is enabled, which the advisory says is common in React Server Components, Next.js, and most APM setups. Servers that do not use async_hooks or AsyncLocalStorage, or that never recurse on untrusted input, are less exposed.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-01-13T17:00:00+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"nodejs","external_id":"january-2026-dos-mitigation-async-hooks","url":"https://nodejs.org/en/blog/vulnerability/january-2026-dos-mitigation-async-hooks","severity":"critical","published_at":"2026-01-13T17:00:00+00:00","modified_at":null,"first_seen_at":"2026-10-23T19:13:00+00:00","fetched_at":"2026-10-25T20:36:45+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}