近期,Node.js 官方发布了一系列重要的安全更新,修复了 8 个安全漏洞。这次更新涉及 Node.js 20.x、22.x、24.x 和 25.x 等所有活跃版本,影响范围之广,几乎覆盖了所有使用 React Server Components、Next.js 或 APM 监控工具的生产环境应用。
问题到底出在哪里?
在 Node.js 的异步编程世界里,有一个叫async_hooks的底层 API。它的作用是追踪异步操作的生命周期。听起来很技术化,但你可能每天都在用它,只是不知道而已。
React Server Components 用它来追踪渲染上下文,Next.js 用它来追踪请求信息,Datadog、New Relic 等 APM 工具用它来追踪请求链路。可以说,async_hooks已经成为现代 Node.js 应用的基础设施。
但问题就出在这里。当你的代码出现深层递归,导致栈溢出时,正常情况下 Node.js 会抛出一个RangeError: Maximum call stack size exceeded错误,你的try-catch可以捕获它,你的错误处理器可以记录它,然后应用继续运行。
但如果启用了async_hooks,情况就完全不同了。当栈溢出发生时,Node.js 会直接以退出代码 7 终止进程,不经过try-catch,不触发uncaughtException处理器,就这样,应用死了。
为什么这么容易被触发?
需要明确的是,这个 BUG 的触发需要满足几个条件:
启用了
async_hooks(使用 AsyncLocalStorage、APM 工具等)代码中存在深度递归
递归过程中创建了 Promise(触发 async hooks)
递归深度达到栈溢出的程度
虽然听起来条件挺多,但在实际应用中,这些条件很容易同时满足。让我举个实际的例子。
假设你有一个 Next.js API 路由,用来处理用户上传的 JSON 数据:
export defaultasyncfunction handler(req, res) { try { const data = req.body; const result = processNestedData(data); res.json({ success: true, result }); } catch (err) { // 你以为这里能捕获错误?天真了 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); }一切看起来都很安全,对吧?有try-catch,有错误处理。
但如果有人发送一个嵌套了几千层的 JSON 数组,你的服务器会直接崩溃。不是返回 500 错误,而是整个进程退出,所有正在处理的其他请求都会中断。
这就是一个典型的 DoS(拒绝服务)攻击向量。
技术原因是什么?
问题的根源在于async_hooks的实现方式。当你创建一个 Promise 时,V8 引擎会同步调用 promise hook,这个 hook 会触发 Node.js 的async_hooks回调。
这意味着,每次new Promise()都会在当前调用栈上添加额外的栈帧。当你的代码递归创建 Promise 时,栈上既有用户代码的帧,也有async_hooks的帧。
当栈最终溢出时,抛出错误的那一刻,执行上下文正好在async_hooks的回调里。Node.js 为了避免在 hook 内部出现错误导致的不一致状态,会用一个特殊的错误处理器TryCatchScope::kFatal来包裹这些回调。
kFatal的意思是:如果这里出错了,状态已经不可恢复,直接退出进程。
虽然这个设计的初衷是为了保护应用,但在栈溢出这个场景下,它反而成了问题。因为错误本身来自用户代码,而不是 hook 本身。
Node.js 是怎么修复的?
这次的修复方案很巧妙。Node.js 在TryCatchScope的析构函数里增加了一个检测:如果捕获到的是栈溢出错误,就把它重新抛给用户代码,而不是当作致命错误处理。
TryCatchScope::~TryCatchScope() { if (HasCaught() && mode_ == CatchMode::kFatal) { Local<Value> exception = Exception(); // 检测到栈溢出?重新抛出而不是退出 if (IsStackOverflowError(env_->isolate(), exception)) { ReThrow(); Reset(); return; } // 其他致命错误:按原逻辑退出 FatalException(/* ... */); } }这样一来,栈溢出错误就能像正常情况下一样被try-catch捕获了。
为什么说这只是缓解措施?
Node.js 官方在博客中特别强调:这只是一个缓解措施(mitigation),而不是根本性的解决方案。
原因很简单:栈溢出的行为本身就不是 ECMAScript 规范的一部分。JavaScript 规范假设栈空间是无限的,没有规定引擎应该在栈溢出时做什么。
抛出可捕获的RangeError只是 V8 等引擎的「尽力而为」行为。依赖这种未定义的行为来保证服务可用性,本身就是有风险的。
正确的做法是:如果你的代码可能处理深度不确定的递归结构(比如用户上传的 JSON),应该主动限制递归深度,或者用迭代算法替代递归。
function processNestedData(data, maxDepth = 100) { function process(item, depth) { if (depth > maxDepth) { thrownewError('Nesting too deep'); } if (Array.isArray(item)) { return item.map(child => process(child, depth + 1)); } return transform(item); } return process(data, 0); }不要指望运行时帮你兜底。
还有哪些漏洞被修复?
除了这个栈溢出问题(CVE-2025-59466),这次更新还修复了其他几个重要漏洞:
CVE-2025-55131(高危):Buffer 分配时的竞态条件可能导致未初始化的内存泄露,从而暴露敏感信息如 token、密码等。
CVE-2025-55130(高危):通过精心构造的符号链接路径可以绕过文件系统权限模型,读写任意文件。
CVE-2025-59465(高危):发送畸形的 HTTP/2 HEADERS 帧可以让服务器崩溃。
CVE-2026-21636(中危):权限模型可以被 Unix Domain Socket 绕过,访问本地特权服务。
CVE-2026-21637(中危):TLS PSK/ALPN 回调中的异常可能导致进程崩溃或文件描述符泄露。
CVE-2025-59464(中危):处理 TLS 客户端证书时的内存泄漏。
CVE-2025-55132(低危):fs.futimes()可以绕过只读权限修改文件时间戳。
这些漏洞涵盖了内存安全、权限模型、网络协议等多个层面,影响范围确实很广。
哪些版本受影响?
好消息是,Node.js 团队已经为所有活跃版本发布了补丁:
Node.js 25.3.0(当前版本)
Node.js 24.13.0(LTS)
Node.js 22.22.0(LTS)
Node.js 20.20.0(LTS)
如果你还在使用更老的版本(18.x 及以下),这些版本已经停止维护,不会收到安全补丁。如果无法升级,可以考虑联系 OpenJS 基金会的商业支持。
特别需要注意的是,如果你在使用 Node.js 24 或更新版本,React 和 Next.js 应用不会受到栈溢出问题的影响,因为AsyncLocalStorage在这些版本中已经用 V8 的新 APIAsyncContextFrame重新实现,不再依赖async_hooks。
但是,如果你的 APM 工具直接使用了async_hooks.createHook(),所有版本仍然受影响。
如果你正在生产环境使用 Node.js,特别是:
使用了 React Server Components
使用了 Next.js
使用了任何 APM 工具(Datadog、New Relic、Dynatrace、Elastic APM、OpenTelemetry 等)
使用了
AsyncLocalStorage
你应该尽快升级到上述的补丁版本。
同时,检查你的代码中是否有处理不可信输入的递归逻辑,加上深度限制或改用迭代算法。
写在最后
这次漏洞揭示了一个有趣的现象:我们每天依赖的基础设施,可能建立在一些未被明确保证的行为之上。
async_hooks从一个调试 API 发展成了整个生态系统的关键依赖。React、Next.js、所有主流 APM 工具,都在用它。但它的某些边界情况,却从未被充分测试和规范化。
这不是某个框架或工具的问题,而是整个生态系统演化过程中的自然结果。当一个 API 变得足够流行,它的每一个实现细节都可能成为事实标准。
好在 Node.js 团队及时发现并修复了这个问题。但更重要的是,这提醒我们:不要假设运行时会永远按照你期望的方式工作,特别是在处理边界情况时。
防御性编程,永远不过时。
一个有趣的社区讨论
这次漏洞披露后,在开发者社区引发了一些有趣的讨论。
有开发者在推特上评论说:"Next.js 真是被诅咒了,拜托别再试图重新发明 PHP 了,天哪"(next.js is cursed tbh, just stop trying to reinvent php please omg)。
Node.js 核心维护者 Matteo Collina 则直接指出:"核心问题出在 React。"(The "core" problem is in React.)
这个对话很有意思。很多人把矛头指向 Next.js,但技术上说,问题的根源确实在 React Server Components 对AsyncLocalStorage的使用。Next.js 只是在此基础上构建的框架。
这也提醒我们,在讨论技术问题时,准确定位问题的层次很重要。表面上看起来是某个框架的问题,实际上可能是更底层的设计决策带来的影响。
当然,这并不是说 React 或 Next.js 做错了什么。它们使用AsyncLocalStorage是合理的选择,只是碰上了 Node.js 实现中的一个边界情况。技术栈的复杂性就在于此:每一层都在合理地使用下一层的 API,但层与层之间的交互可能产生意想不到的问题。
参考资料:
Node.js 官方博客 - Mitigating Denial-of-Service Vulnerability
Node.js 2026 年 1 月安全发布公告