T-CREATOR

<div />

Next.js・React Server Componentsが危険?async_hooksの脆弱性CVE-2025-59466を徹底解説

Next.js・React Server Componentsが危険?async_hooksの脆弱性CVE-2025-59466を徹底解説

Next.js や React Server Components を使用している環境で、try-catch が効かずサーバーがクラッシュする深刻な脆弱性(CVE-2025-59466)が報告されました。本記事では、この脆弱性の仕組み・影響範囲・防御策を、Next.js 開発者の視点から徹底解説します。

「async_hooks の脆弱性」「Next.js のセキュリティ対策」「AsyncLocalStorage の安全性」を知りたい方に向けた記事です。

async_hooks 脆弱性の影響比較表

#環境async_hooks 有効try-catchスタックオーバーフロー時の動作緊急度
1Next.js App Router自動で有効効かない即座にクラッシュ(終了コード7)最優先
2Next.js Pages Router自動で有効効かない即座にクラッシュ(終了コード7)最優先
3Express + APMAPM経由で有効効かない即座にクラッシュ(終了コード7)最優先
4Express(APMなし)無効効くRangeError をキャッチ可能
5素の Node.js無効効くRangeError をキャッチ可能

この表は即答用です。各環境での詳細な影響と対策は後段で解説します。

検証環境

  • OS: macOS Sonoma 14.7 / Ubuntu 22.04 LTS
  • Node.js: 22.21.0(脆弱性確認)→ 22.22.0(修正確認)
  • Next.js: 15.1.0
  • React: 19.0.0
  • 主要パッケージ:
    • @opentelemetry/sdk-node: 0.57.0
    • dd-trace: 5.30.0
  • 検証日: 2026年1月14日

背景:なぜ Next.js 環境で async_hooks が危険なのか

この章でわかること

  • async_hooks とは何か(初学者向け)
  • Next.js が内部で async_hooks を使用する理由
  • 脆弱性が発生するメカニズム

async_hooks の基本(初学者向け)

async_hooks は、Node.js が提供する非同期処理の追跡機能です。Promise や setTimeout などの非同期処理がいつ開始・終了したかを監視できます。

javascriptimport { createHook } from "node:async_hooks";

// 非同期処理の開始・終了を追跡
const hook = createHook({
  init(asyncId, type) {
    console.log(`非同期処理 ${type} が開始: ID=${asyncId}`);
  },
  destroy(asyncId) {
    console.log(`非同期処理が終了: ID=${asyncId}`);
  },
});

hook.enable();

この機能を使うと、「どのリクエストがどの処理を実行しているか」を追跡できるため、APM ツールやフレームワークが活用しています。

Next.js が async_hooks を使用する理由

Next.js の App Router や React Server Components は、AsyncLocalStorage という API を内部で使用しています。AsyncLocalStorage は async_hooks をベースに構築されており、リクエストごとのコンテキスト(認証情報、ロケールなど)を保持するために使われます。

以下の図は、Next.js における async_hooks の利用構造を示しています。

mermaidflowchart TD
  subgraph nextjs["Next.js App Router"]
    rsc["React Server<br/>Components"]
    middleware["Middleware"]
    api["API Routes"]
  end

  subgraph internal["内部実装"]
    als["AsyncLocalStorage"]
    hooks["async_hooks"]
  end

  subgraph context["保持されるコンテキスト"]
    auth["認証情報"]
    locale["ロケール"]
    headers["リクエストヘッダー"]
  end

  rsc --> als
  middleware --> als
  api --> als
  als --> hooks
  als --> context

図の補足:Next.js の主要機能は AsyncLocalStorage を経由して async_hooks を使用しており、開発者が意識しなくても自動的に有効化されています。

脆弱性が発生するメカニズム

問題は、async_hooks が有効な状態でスタックオーバーフローが発生した場合です。通常、スタックオーバーフローは RangeError: Maximum call stack size exceeded として try-catch でキャッチできます。

しかし、async_hooks が有効な場合、Node.js は内部で TryCatchScope::kFatal というモードでフック処理をラップしています。このモードでは、エラーが発生すると無条件にプロセスを終了させます。

mermaidflowchart LR
  subgraph normal["async_hooks 無効時"]
    A1["再帰処理"] --> A2["スタック枯渇"]
    A2 --> A3["RangeError"]
    A3 --> A4["try-catch で<br/>キャッチ可能"]
  end

  subgraph vuln["async_hooks 有効時"]
    B1["再帰処理"] --> B2["スタック枯渇"]
    B2 --> B3["フック内で<br/>RangeError"]
    B3 --> B4["kFatal モード<br/>により即終了"]
    B4 --> B5["try-catch<br/>無効"]
  end

図の補足:async_hooks が有効な場合、スタックオーバーフローエラーは「致命的フックエラー」として扱われ、通常のエラーハンドリングをバイパスしてプロセスが終了します。

つまずきポイント

  • 「自分のコードで async_hooks を使っていない」と思っても、Next.js や APM ツールが自動的に有効化しています
  • AsyncLocalStorage は async_hooks の上に構築されているため、new AsyncLocalStorage() を使うだけで影響を受けます

課題:CVE-2025-59466 の詳細と攻撃シナリオ

この章でわかること

  • 脆弱性の技術的な詳細
  • 攻撃が成立する条件
  • 実際の攻撃コード例

脆弱性の技術的詳細

項目内容
CVE番号CVE-2025-59466
深刻度MEDIUM(ただし影響範囲が広いため実質 HIGH)
影響バージョンNode.js 20.x〜25.x(修正前)
修正バージョン20.20.0, 22.22.0, 24.13.0, 25.3.0
攻撃種別リモート DoS(サービス拒否)
認証要否不要

攻撃が成立する条件

以下のすべての条件が揃うと、攻撃が成立します。

  1. async_hooks が有効(Next.js、APM ツール、AsyncLocalStorage 使用時)
  2. 再帰的な処理が存在(ネストしたデータの処理など)
  3. 入力データのネスト深度が制限されていない

Next.js アプリケーションでは、条件1は自動的に満たされるため、条件2と3が揃えば攻撃可能です。

攻撃コード例:Next.js API Route

以下は、脆弱な Next.js API Route の例です。

javascript// app/api/process/route.js(脆弱なコード)
export async function POST(request) {
  try {
    const data = await request.json();
    const result = processNestedData(data);
    return Response.json({ success: true, result });
  } catch (err) {
    // このキャッチブロックは実行されない
    return Response.json({ error: "Processing failed" }, { status: 500 });
  }
}

function processNestedData(data) {
  if (Array.isArray(data)) {
    return data.map((item) => processNestedData(item));
  }
  if (typeof data === "object" && data !== null) {
    return Object.fromEntries(
      Object.entries(data).map(([k, v]) => [k, processNestedData(v)]),
    );
  }
  return data;
}

攻撃ペイロード

攻撃者は、以下のような深くネストした JSON を送信します。

javascript// 攻撃ペイロード生成(50,000レベルのネスト)
function generateDeepPayload(depth) {
  let payload = { value: "end" };
  for (let i = 0; i < depth; i++) {
    payload = { nested: payload };
  }
  return payload;
}

// 攻撃リクエスト
fetch("/api/process", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(generateDeepPayload(50000)),
});

このリクエストを受信すると、サーバーは終了コード7で即座にクラッシュし、try-catch は実行されません。

影響を受けるフレームワーク・ツール一覧

カテゴリ製品・ライブラリasync_hooks 使用
フレームワークNext.js(App Router / Pages Router)AsyncLocalStorage 経由
フレームワークRemixAsyncLocalStorage 経由
フレームワークSvelteKitAsyncLocalStorage 経由
APMDatadog(dd-trace)createHook 直接使用
APMNew ReliccreateHook 直接使用
APMDynatracecreateHook 直接使用
APMElastic APMcreateHook 直接使用
ObservabilityOpenTelemetrycreateHook 直接使用
ロギングPino(async context)AsyncLocalStorage 経由
ロギングWinston(async context)AsyncLocalStorage 経由

つまずきポイント

  • 「API Route を公開していないから大丈夫」と思っても、Server Actions や Server Components でも同じ問題が発生します
  • JSON.parse() 自体は深いネストを処理できますが、その後の再帰処理でクラッシュします

解決策と判断:バージョンアップと防御策

この章でわかること

  • 推奨される対策の優先順位
  • Node.js のアップデート方法
  • アップデートできない場合の暫定対策

対策の優先順位

優先度対策効果実装コスト
1(最優先)Node.js アップデート根本解決低〜中
2入力データのネスト深度制限攻撃緩和
3再帰処理の深度制限攻撃緩和
4WAF でのペイロードサイズ制限攻撃緩和

Node.js アップデート(根本解決)

修正済みバージョンにアップデートすることで、根本的に解決できます。

bash# nvm を使用している場合
nvm install 22.22.0
nvm use 22.22.0

# volta を使用している場合
volta install node@22.22.0

# package.json で指定する場合
{
  "engines": {
    "node": ">=22.22.0"
  }
}

Next.js プロジェクトでの確認方法

bash# 現在の Node.js バージョンを確認
node -v

# 脆弱性が修正されているか確認(22.22.0 以降なら OK)
# 20.20.0, 22.22.0, 24.13.0, 25.3.0 以降が修正済み

暫定対策1:入力データのネスト深度制限

Node.js をすぐにアップデートできない場合の暫定対策です。

javascript// lib/validation.js
export function validateJsonDepth(obj, maxDepth = 50, currentDepth = 0) {
  if (currentDepth > maxDepth) {
    throw new Error(`JSON nesting depth exceeds limit: ${maxDepth}`);
  }

  if (obj === null || typeof obj !== "object") {
    return true;
  }

  if (Array.isArray(obj)) {
    for (const item of obj) {
      validateJsonDepth(item, maxDepth, currentDepth + 1);
    }
  } else {
    for (const value of Object.values(obj)) {
      validateJsonDepth(value, maxDepth, currentDepth + 1);
    }
  }

  return true;
}
javascript// app/api/process/route.js(修正後)
import { validateJsonDepth } from "@/lib/validation";

export async function POST(request) {
  try {
    const data = await request.json();

    // ネスト深度を検証(再帰処理の前に実行)
    validateJsonDepth(data, 50);

    const result = processNestedData(data);
    return Response.json({ success: true, result });
  } catch (err) {
    return Response.json({ error: err.message }, { status: 400 });
  }
}

暫定対策2:再帰処理の深度制限

再帰関数自体に深度制限を追加する方法です。

javascript// lib/process.js
export function processNestedData(data, depth = 0, maxDepth = 100) {
  if (depth > maxDepth) {
    throw new Error("Maximum recursion depth exceeded");
  }

  if (Array.isArray(data)) {
    return data.map((item) => processNestedData(item, depth + 1, maxDepth));
  }

  if (typeof data === "object" && data !== null) {
    return Object.fromEntries(
      Object.entries(data).map(([k, v]) => [
        k,
        processNestedData(v, depth + 1, maxDepth),
      ]),
    );
  }

  return data;
}

暫定対策3:Next.js Middleware でのグローバル検証

すべての API Route に適用したい場合は、Middleware を使用します。

javascript// middleware.js
import { NextResponse } from "next/server";

const MAX_CONTENT_LENGTH = 1024 * 1024; // 1MB
const MAX_NESTING_DEPTH = 50;

function checkDepth(obj, depth = 0) {
  if (depth > MAX_NESTING_DEPTH) return false;
  if (obj === null || typeof obj !== "object") return true;

  const values = Array.isArray(obj) ? obj : Object.values(obj);
  return values.every((v) => checkDepth(v, depth + 1));
}

export async function middleware(request) {
  // API Route のみ検証
  if (!request.nextUrl.pathname.startsWith("/api/")) {
    return NextResponse.next();
  }

  // Content-Length チェック
  const contentLength = request.headers.get("content-length");
  if (contentLength && parseInt(contentLength) > MAX_CONTENT_LENGTH) {
    return NextResponse.json({ error: "Payload too large" }, { status: 413 });
  }

  // JSON ボディのネスト深度チェック
  if (request.method === "POST" || request.method === "PUT") {
    try {
      const clonedRequest = request.clone();
      const body = await clonedRequest.json();

      if (!checkDepth(body)) {
        return NextResponse.json(
          { error: "JSON nesting too deep" },
          { status: 400 },
        );
      }
    } catch {
      // JSON パースエラーは後続の処理に任せる
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: "/api/:path*",
};

つまずきポイント

  • 暫定対策の validateJsonDepth 関数自体も再帰処理なので、50階層程度に制限しないと自身がクラッシュの原因になります
  • Middleware でのチェックは request.clone() を使わないと、後続の処理でボディを読み取れなくなります

具体例:実務での対応パターン

この章でわかること

  • 実際のプロジェクトでの対応手順
  • CI/CD での自動チェック方法
  • 検証・テスト方法

対応手順のフローチャート

mermaidflowchart TD
  start["脆弱性の認知"] --> check1{"Node.js バージョン<br/>確認"}
  check1 -->|"修正済み"| done["対応完了"]
  check1 -->|"未修正"| check2{"即時アップデート<br/>可能?"}
  check2 -->|"可能"| update["Node.js<br/>アップデート"]
  update --> test["動作確認"]
  test --> done
  check2 -->|"不可"| mitigation["暫定対策<br/>実装"]
  mitigation --> schedule["アップデート<br/>計画策定"]
  schedule --> update

図の補足:Node.js のアップデートが根本解決ですが、即時対応が難しい場合は暫定対策を実装した上で計画的にアップデートします。

実務での対応例:EC サイトの場合

実際に業務で対応した EC サイト(Next.js 15 + App Router)での事例です。

影響調査

bash# 1. Node.js バージョン確認
node -v
# v22.20.0(脆弱性あり)

# 2. 再帰処理を含むコードの検索
grep -r "function.*(" --include="*.js" --include="*.ts" | \
  xargs grep -l "return.*\1"

# 3. API Route の一覧取得
find app -name "route.js" -o -name "route.ts"

発見された脆弱なエンドポイント

  • ​/​api​/​products​/​import - 商品データの一括インポート
  • ​/​api​/​categories​/​tree - カテゴリツリーの取得
  • ​/​api​/​orders​/​export - 注文データのエクスポート

対応内容

  1. 緊急対応(当日): Middleware でネスト深度制限を追加
  2. 短期対応(1週間以内): Node.js 22.22.0 へアップデート
  3. 中期対応(1ヶ月以内): 再帰処理を含む全関数に深度制限を追加

CI/CD での自動チェック

GitHub Actions で Node.js バージョンをチェックする例です。

yaml# .github/workflows/security-check.yml
name: Security Check

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  check-node-version:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Check Node.js version for CVE-2025-59466
        run: |
          # package.json の engines.node を取得
          REQUIRED_VERSION=$(node -pe "require('./package.json').engines?.node || ''")

          # 最小安全バージョン
          SAFE_VERSIONS=("20.20.0" "22.22.0" "24.13.0" "25.3.0")

          echo "Required Node.js version: $REQUIRED_VERSION"
          echo "Safe versions: ${SAFE_VERSIONS[*]}"

          # バージョンチェックロジック(簡易版)
          if [[ -z "$REQUIRED_VERSION" ]]; then
            echo "::warning::No Node.js version specified in package.json"
          fi

脆弱性の検証方法

ローカル環境で脆弱性を確認する方法です。本番環境では絶対に実行しないでください

javascript// scripts/test-vulnerability.js(検証用)
import { createHook } from "node:async_hooks";

// async_hooks を有効化(Next.js と同じ状態を再現)
createHook({ init() {} }).enable();

function recursive(depth = 0) {
  new Promise(() => {}); // 非同期コンテキスト作成
  if (depth > 100000) return depth;
  return recursive(depth + 1);
}

try {
  console.log("Testing vulnerability...");
  recursive();
  console.log("No crash - vulnerability may be fixed");
} catch (err) {
  console.log("Caught error:", err.message);
  console.log("This means the vulnerability is fixed");
}

// 脆弱な場合:終了コード 7 でクラッシュ
// 修正済みの場合:RangeError をキャッチ
bash# 検証実行
node scripts/test-vulnerability.js
echo "Exit code: $?"
# 脆弱な場合: Exit code: 7
# 修正済みの場合: Exit code: 0

つまずきポイント

  • 検証スクリプトを本番環境で実行すると、サーバーがクラッシュします
  • new Promise(() => {}) の行が重要で、これがないと async_hooks が発火せず脆弱性を再現できません

async_hooks 利用パターン比較(実務判断用・詳細)

この章でわかること

  • 各利用パターンでの影響度
  • 代替手段の検討
  • 長期的なアーキテクチャ判断

詳細比較表

利用パターンasync_hooks 依存脆弱性影響代替手段移行コスト
Next.js App Router必須(内部使用)ありなし(アップデートのみ)-
Next.js Pages Router必須(内部使用)ありなし(アップデートのみ)-
AsyncLocalStorage 直接使用必須ありスレッドローカル変数(制限あり)
Datadog APM必須あり無効化可能だがトレース喪失
OpenTelemetry必須あり無効化可能だがトレース喪失
カスタム createHook必須あり削除または無効化低〜中

Next.js における判断フロー

mermaidflowchart TD
  start["Next.js プロジェクト"] --> q1{"Node.js<br/>22.22.0 以上?"}
  q1 -->|"はい"| safe["対応完了"]
  q1 -->|"いいえ"| q2{"即時アップデート<br/>可能?"}
  q2 -->|"はい"| update["アップデート実施"]
  update --> safe
  q2 -->|"いいえ"| q3{"API Route<br/>あり?"}
  q3 -->|"はい"| mitigation["Middleware で<br/>ネスト制限追加"]
  q3 -->|"いいえ"| q4{"Server Actions<br/>で外部入力処理?"}
  q4 -->|"はい"| mitigation
  q4 -->|"いいえ"| lowrisk["リスク低<br/>(計画的対応)"]
  mitigation --> schedule["アップデート<br/>計画策定"]
  lowrisk --> schedule
  schedule --> update

図の補足:Next.js では async_hooks を無効化できないため、Node.js のアップデートが唯一の根本解決策です。

APM ツール利用時の判断

APM ツールを一時的に無効化することで、脆弱性の影響を回避できます。ただし、トレーシング機能が失われるトレードオフがあります。

javascript// Datadog APM を条件付きで無効化
// dd-trace を import しない = async_hooks が有効化されない

// 環境変数で制御
if (process.env.ENABLE_APM === "true") {
  require("dd-trace").init();
}
bash# 本番環境では有効、脆弱性対応中は無効
ENABLE_APM=false node server.js

トレードオフの判断

選択肢メリットデメリット
APM 有効のまま監視継続DoS 攻撃リスク
APM 一時無効DoS 回避監視喪失
Node.js アップデート根本解決テスト工数

業務では、「APM を一時無効化 → Node.js アップデート → APM 再有効化」の順で対応しました。

つまずきポイント

  • Next.js では async_hooks を無効化する手段がないため、暫定対策は入力バリデーションに限られます
  • APM を無効化すると、その間のエラーやパフォーマンス問題を検知できなくなります

まとめ

CVE-2025-59466 は、Next.js や React Server Components を使用するほぼすべての環境に影響する深刻な脆弱性です。

  1. async_hooks が有効な場合、try-catch が効かない:スタックオーバーフローが発生すると、エラーハンドリングをバイパスしてプロセスが即座にクラッシュします。

  2. Next.js では回避不可:App Router も Pages Router も内部で AsyncLocalStorage を使用しており、async_hooks を無効化する手段がありません。

  3. Node.js のアップデートが唯一の根本解決:20.20.0、22.22.0、24.13.0、25.3.0 以降で修正されています。

  4. 暫定対策は入力バリデーション:アップデートまでの間、Middleware や各エンドポイントでネスト深度を制限することで攻撃を緩和できます。ただし、これは根本解決ではありません。

  5. APM ツール利用環境も影響:Datadog、New Relic、OpenTelemetry などを導入している環境は、Next.js を使用していなくても同様の影響を受けます。

本脆弱性は「自分には関係ない」と思い込みやすいですが、モダンな Node.js 環境のほとんどが影響を受けます。まずは自身の環境の Node.js バージョンを確認し、必要に応じて早急なアップデートを検討してください。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;