目次を表示する

Async Local Storage Deep Dive 2026 ─ 内部実装・落とし穴・性能を 9 章で読み解く

2026/1 の DoS 脆弱性 ─ なぜ React/Next.js は無事で APM は被弾したか

2026/1 の DoS 脆弱性 ─ なぜ React/Next.js は無事で APM は被弾したか

2026 年 1 月 13 日、Node.js は珍しい形のセキュリティ告知を出した。Mitigating Denial-of-Service Vulnerability from Unrecoverable Stack Space Exhaustion for React, Next.js, and APM Users。タイトルからして異例 ── 特定のフレームワークと APM ツールを名指ししている。

そして本文を読むと、もっと異例の事実が出てくる。

Node 24+ で React と Next.js は影響を受けない。だが APM 各社(Datadog / New Relic / Dynatrace / Elastic / OpenTelemetry)は同じ Node 24+ で影響を受けた。

なぜ同じ Node のバージョンで、影響範囲がこんなに違うのか。答えは第 5 章にある。

脆弱性の機構

問題のシナリオは以下:

import { createHook } from 'node:async_hooks';
createHook({ init() {} }).enable();

function recursive() {
  new Promise(() => {});
  return recursive();
}

try {
  recursive(); // ← これが try-catch で拾えずに即死する
} catch (err) {
  console.log('Never runs'); // ← 来ない
}

何が起きるか:

  1. recursive() が再帰的に深くなり、new Promise() を呼ぶ
  2. V8 PromiseHook が同じコールスタックで発火する
  3. スタックが exhausted する瞬間、active frame は hook callback の中
  4. V8 は「hook 内で fatal エラーが起きた」と判定する(TryCatchScope::kFatal
  5. プロセスが exit code 7 で即死try-catch を貫通する
sequenceDiagram
  participant U as user code
  participant V8 as V8
  participant Hook as async_hooks callback
  participant TC as TryCatchScope::kFatal

  U->>U: recursive 呼び出し(深い)
  U->>V8: new Promise()
  V8->>Hook: PromiseHook 発火(同スタック)
  Hook-->>V8: stack overflow
  V8->>TC: kFatal で扱う
  TC-->>U: ❌ try-catch 貫通
  TC-->>U: process.exit(7)

  Note over U: try-catch が機能しないため<br/>サービスが即死する

通常の JavaScript では RangeError: Maximum call stack size exceededtry-catch で拾える。だがhook 内のスタックオーバーフローはその例外を発火する余地がない ── スタックがすでに尽きているからだ。V8 は kFatal として扱うしかない。

影響範囲

プロダクト内部で createHook を使う?影響を受けたか
React Server Components使わない(AsyncLocalStorage のみ)Node 24+ で影響なし
Next.js使わない(AsyncLocalStorage のみ)Node 24+ で影響なし
Datadog APM直接使うcreateHook 経由でトレース ID を伝播)影響あり
New Relic直接使う影響あり
Dynatrace直接使う影響あり
Elastic APM直接使う影響あり
OpenTelemetry直接使う影響あり

5 章の伏線回収

なぜ React と Next.js は無事だったのか。第 5 章で見たとおり:

Node 24+ の AsyncLocalStorage は、もう async_hooks.createHook() を使っていない。

React / Next.js は ALS だけを使っている。Node 24 で ALS の実装が AsyncContextFrame に置き換わった結果、createHook を経由しない ようになった。よって PromiseHook が発火しない。よって kFatal の罠を踏まない。

第 5 章で「createHook と決別した」と書いた決断が、2 年後に結果的にセキュリティ脆弱性を回避したことになる。Node コアチームの設計判断が、フレームワーク側に「動いただけで安全」という余録をもたらした。

一方 APM 各社は、トレース ID をリクエスト境界を越えて伝播するために createHook を直接使い続けた。「migrate away from this API」という第 2 章の公式忠告を踏み続けた結果、被弾した。

パッチとフラグの罠

修正パッチは 2026-01-13 にリリース:

バージョン系修正済み
20.x LTS20.20.0+
22.x LTS22.22.0+
24.x LTS24.13.0+
25.x Current25.3.0+
8.x - 18.x(EOL)パッチなし

修正の中身は単純:TryCatchScope::kFatal でエラーを掴んだ際に StackOverflow 由来かを判定し、kFatal なら ReThrow に切り替える。これで try-catch が機能する。

// 概念コード
if (HasCaught() && mode_ == CatchMode::kFatal) {
  Local<Value> exception = Exception();
  if (IsStackOverflowError(env_->isolate(), exception)) {
    ReThrow();      // ← 追加: ユーザーコードに戻す
    Reset();
    return;
  }
  FatalException(/* ... */);
}

注意すべきフラグの罠:第 5 章で紹介した --no-async-context-frame。これを使うと Node 24 でも ALS が旧実装に戻る ── つまり createHook を使う実装に。そうすると React/Next.js も再び脆弱性の影響圏に入る。互換性確保のためのフラグだが、本番運用で使う場合はパッチ済み Node を必ず確保しておく必要がある。

CVE は付かなかった

意外なことに、この件には CVE 番号が割り当てられなかった。Node コアチームの判断は:

  • スタックオーバーフローからの recovery は ECMAScript の仕様上保証された挙動ではない
  • V8 のセキュリティ保証にも含まれない
  • 「仕様外動作への依存」が根本原因(CWE-758, CWE-674)

つまり「そもそも頼ってはいけない recovery に頼っていたエコシステム側の問題」と整理された。Node 側のパッチは「リスク緩和」として位置づけられている。

これは技術的には正しい判断だが、実運用上はパッチを当てなければサービスが落ちる実害がある。「言葉の整理」と「現場の実害」を分けて理解する必要がある。

この章の要点

  • 2026/1/13 に公開された async_hooks 起因の DoS:recursive code の Promise 生成でスタック枯渇 → kFatal → try-catch 貫通 → exit code 7
  • React Server Components / Next.js は Node 24+ で影響なし(ALS が createHook を使わない実装に進化していたため)
  • APM 各社(Datadog / NR / Dynatrace / Elastic / OTel)は createHook 直叩きで被弾
  • パッチは 20.20.0 / 22.22.0 / 24.13.0 / 25.3.0
  • --no-async-context-frame は旧実装に戻す ── パッチを当てない限り脆弱性圏に戻るフラグ
  • CVE 番号は割り当てられなかった(仕様外動作への依存と整理)

次章への問いかけ

ALS の進化は、結果として「createHook から離れる」方向に収斂した。これは ALS だけの話で終わるのか、JavaScript 全体の話になるのか?

最終章で TC39 AsyncContext proposal の動向と、ALS の今後を見る。