Zenn
🔒

Next.jsで認証バイパスが可能に!? CVSS 9.1の致命的脆弱性から学ぶNext.jsの設計思想と教訓

2025/03/30に公開

1. はじめに

1.1 この記事を書こうと思った理由

先日、Next.jsコミュニティは「CVE-2025-29927」という、極めて深刻な脆弱性の公表によって揺れました。
https://github.com/advisories/GHSA-f82v-jwr5-mffw

CVSSスコア9.1 (Critical) と評価されたこの問題は、以下のようなヘッダーを追加するだけで、Next.jsのミドルウェア機能をバイパスし、認証やアクセス制御をスキップすることが可能というものです。

x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware

影響を受けるバージョンも11系の一部(11.1.4以降)から15系の一部(15.2.3未満)と広範囲に渡ります。

この話題を受けて、ちょうど筆者が現在支援しているNext.jsの導入プロジェクトでも話題に上がり、「本当にNext.jsはセキュリティー的に大丈夫なのか?」といったご質問を受けることがありました。

結論を先に言うと、基本的な多層防御の原則に従ってバックエンドでの認証・認可が適切に実装されていれば、この脆弱性単体で即座に致命的な被害につながるケースはないでしょう。

なぜかと言うと、この脆弱性の影響を直接的に受けるのは、認証・認可のロジックをミドルウェア層だけに全面的に依存している、という公式も推奨しない設計のアプリケーションに限られるからです。

弊社でもNext.jsを使ってWEBアプリケーションを構築する機会は多いですが、ミドルウェアだけに頼ることはなく、複数のレイヤーでも認証・認可のチェックを行っています。

もしミドルウェアがバイパスされたとしても、これらの後続のチェック機構が機能していれば、不正なアクセスは防げるはずです。

ただしこの脆弱性を大したことがないと言いたいわけではありません。

安易な楽観論(うちは大丈夫だったから問題ない)も、過剰な悲観論(Next.jsはもう使えない)も、どちらも技術的な本質から目を背けた姿勢で、重要なのは、このインシデントを冷静に分析し、フレームワークの特性、設計思想のトレードオフ、そして我々開発者自身が負うべきセキュリティ責務を正確に理解することです。

上記の本質的な問いが、「ミドルウェア」「認証」「Critical (9.1)」というセンセーショナルなワードの裏に隠れてしまっているような気がして、この記事を書こうと思いました。

1.2 記事の構成

この記事では、この「CVE-2025-29927」騒動を多角的に、そして技術的に可能な限り深く掘り下げていきます。

  • 脆弱性の技術的メカニズム: 問題のコードはどこにあり、なぜそれが悪用可能だったのか
  • 修正内容の詳細: Next.jsチームは具体的にどのようにコードを修正したのか
  • Next.jsアーキテクチャの功罪: ミドルウェアとEdge Runtimeの設計思想、その利点と、今回の脆弱性を生んだトレードオフ
  • 普遍的な教訓: なぜミドルウェアだけでは不十分なのか。セキュアなアプリケーション設計の原則とは何か
  • 実践的な対策: 我々開発者は具体的に何をすべきか

構成や内容に関しては以下のTheo氏の解説を参考にしているところが大きいです。
本脆弱性を発見された方の記事と一緒に見てみることをお勧めします。

https://www.youtube.com/watch?v=0EVB5LAtlDQ&t=1775s
https://zhero-web-sec.github.io/research-and-things/nextjs-and-the-corrupt-middleware

2. 脆弱性の概要

まず、「CVE-2025-29927」がどのような脆弱性であり、どのような影響をもたらしたのか、その全体像を把握しましょう。

2.1 脆弱性の核心:内部制御ヘッダー x-middleware-subrequest の悪用

問題の核心は、Next.jsが内部的に使用していたx-middleware-subrequestというHTTPヘッダーの不適切な扱いにありました。

このヘッダーは、Next.jsアプリケーション内部で発生するサブリクエスト(例えば、サーバーコンポーネントがレンダリング中に自身のAPIルートをfetchで呼び出すケースなど)において、ミドルウェアが意図せず再帰的に呼び出され、無限ループに陥ることを防ぐための内部的な制御メカニズムの一部でした。

具体的には、ミドルウェア関数が自身(または他のミドルウェア関数)を呼び出すたびに、その呼び出し履歴(どのミドルウェアが呼ばれたか)をこのヘッダーの値(コロン区切りの文字列)に追加していく、という仕組みになっていました。

そして、ミドルウェアが実行される際、このヘッダー値をチェックし、特定のミドルウェアの呼び出し回数が事前に定義された上限(例: 5回)に達していた場合、それは無限ループの兆候であると判断し、安全のためミドルウェアの実行をスキップする、というフェイルセーフ機構が組み込まれていたのです。

ここまでの意図は理解できます。

問題は、この内部制御用のヘッダーが、外部のクライアントからのHTTPリクエストによっても任意の値に設定可能であったという、設計上の重大な欠陥です。

これにより、攻撃者はHTTPリクエストを送信する際に、意図的に細工したヘッダー、例えば

x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware

(ここで middleware は対象のミドルウェア名)を含めることで、Next.js内部の再帰深度チェックロジックを騙し、ミドルウェアの実行そのものを完全にバイパスさせることが可能になってしまいました。

ミドルウェアに実装されていた認証チェック、アクセス権限制御、リダイレクト処理など、あらゆるロジックが実行されることなく、リクエストは後続のアプリケーション本体(ページやAPIルート)へと素通りしてしまう。これが、この脆弱性の恐ろしさでした。

2.2 問題になった実際のコードを見てみる

以下は脆弱性があったバージョンから、該当箇所のコードを抜き出して私がコメントを加えたものです。

https://github.com/vercel/next.js/blob/f4552826e1ed15fbeb951be552d67c5a08ad0672/packages/next/src/server/web/sandbox/sandbox.ts#L94C1-L114C4

export const run = withTaggedErrors(async function runWithTaggedErrors(params) {
  const runtime = await getRuntimeContext(params)
  // 1. 外部からも設定可能なヘッダーを取得
  const subreq = params.request.headers[`x-middleware-subrequest`]
  const subrequests = typeof subreq === 'string' ? subreq.split(':') : []

  const MAX_RECURSION_DEPTH = 5
  // 2. 現在実行しようとしているミドルウェア名 (params.name) が、ヘッダー値の配列内に何回含まれているかをカウント
  const depth = subrequests.reduce(
    (acc, curr) => (curr === params.name ? acc + 1 : acc),
    0
  )
  // 3. カウント結果が上限値以上か判定
  if (depth >= MAX_RECURSION_DEPTH) {
    // 4. 上限に達した場合、後続処理に進むためのシグナルを含むレスポンスを生成して返す
    //   (このレスポンスを受け取ったNext.js内部の処理が、ミドルウェアをスキップして次に進む)
    return {
      waitUntil: Promise.resolve(),
      response: new runtime.context.Response(null, {
        headers: {
          'x-middleware-next': '1',
        },
      }),
    }
  }

問題の核心は、ミドルウェアを実行する関数(run 関数)に存在した再帰深度チェックロジックでした。
このコードのステップを追うと、問題点が明確になります。

  1. 信頼できない入力の無検証受け入れ: params.request.headers.get('x-middleware-subrequest') で外部からのヘッダー値を無条件に読み込んでいます。これが全ての元凶です。

  2. 制御ロジックへの直接利用: 読み込んだ値を単純な文字列処理 (split(':'), reduce) で解析し、その結果をミドルウェアをスキップするか否かの制御フロー判断 (if (depth >= MAX_RECURSION_DEPTH)) に直結させています。

  3. 内部状態との不一致: ヘッダー値は実際の内部的な再帰呼び出し状態を全く反映しておらず、完全に外部から注入された偽情報である可能性があるにもかかわらず、それを真実として扱っています。

2.3 修正PRの内容

この脆弱性は当初以下のPRで解決を試みました。

https://github.com/vercel/next.js/pull/77201

主な修正ポイントは以下の通りです。

  1. ヘッダー名の変更: まず、既存の攻撃を無効化するために、ヘッダー名を x-middleware-subrequest から x-middleware-subrequest-id に変更しました。
  2. 暗号学的に安全なランダムIDの導入: これが最も重要な修正です。内部サブリクエストを識別するために、固定的なミドルウェア名ではなく、8バイトのランダムな値を生成し、それを16進数の文字列に変換してmiddlewareSubrequestId変数に保存するようにしました。

攻撃者は、内部で動的に生成される予測不能なIDを知ることができないため、ヘッダーを偽装してミドルウェアをバイパスすることは極めて困難になりました。

ただし最終的にはシンプルに以下のPRで、x-middleware-subrequestを一切使わない方法で修正したようです。

https://github.com/vercel/next.js/pull/77474

何れにせよ、「なぜ最初の設計でこのリスクが見過ごされたのか?」と言う疑問は残ります。
その答えは、Next.js、特にミドルウェアとEdge Runtimeの設計思想、そしてVercelが推進する開発体験への強い志向性にあると思っています。

3. Next.jsミドルウェアとEdge Runtimeの設計思想

今回の脆弱性の本質を理解するためには、

  • Next.jsのミドルウェアがどのような思想に基づいて設計され
  • なぜEdge Runtimeという特殊な環境で動作することを前提としているのか
  • その背景とトレードオフを掘り下げることが不可欠です

3.1 Next.jsでの「ミドルウェア」の概念

Middleware allows you to run code before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.

Middleware defaults to using the Edge runtime.

Next.jsの公式ドキュメントによれば、ミドルウェアはリクエストがページやAPIルートに到達する前に実行される関数です。
また、ミドルウェアはアプリケーション本体と同じ実行環境ではなく、Edge Runtimeという軽量な実行環境で動作することを想定して設計されています。
https://nextjs.org/docs/app/building-your-application/routing/middleware

The Node.js Runtime is used for rendering your application.
The Edge Runtime is used for Middleware (routing rules like redirects, rewrites, and setting headers).

Node.js Runtimeはアプリケーションのレンダリングに使用され、Edge Runtimeはミドルウェア(リダイレクト、リライト、ヘッダーの設定などのルーティングルール)に使用されるということを示しています。
https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes

推奨されているミドルウェアの主な用途は以下の通りで、用途としては限定的です。

  • リダイレクトの実行:特定の条件に基づいてユーザーを別のページにリダイレクトする
  • リクエストの書き換え:URLの書き換えやパスの変更
  • ヘッダーの設定・変更:全てのページまたは特定のページのヘッダー情報を変更
  • A/Bテスト:ユーザーごとに異なるページを表示する実験的なテスト

逆に適さないケースとしては以下が挙げられています。

  • 複雑なデータフェッチと操作
  • セッション管理

3.2 Edge Runtime:速度と制約のトレードオフ

Edge Runtimeは、エッジ(ユーザーに近い場所)のサーバーでコードを実行する仕組みで、これにより、従来の中央集権型サーバーに比べて、低遅延かつ高速なレスポンスが実現できるとされています。

Vercel Edge Functionsなどで利用されるこのランタイムは、Node.jsとは異なり、Web標準API(fetch, Request, Response, URLなど)に準拠した、より軽量でセキュアなサンドボックス環境を提供します。

これにより、世界中のエッジロケーションでコードを高速に実行することが可能になります。
しかし、その速度と引き換えに、Edge Runtimeには無視できない制約が存在します。

  • 制限されたNode.js API: ファイルシステムへのアクセス、多くのNode.jsネイティブモジュール(fs, path, net, osなど)、データベースドライバの多くは利用できません。
  • ステートレス: リクエスト間でメモリ上の状態を保持することは基本的にできません。
  • CPU/メモリ/時間制限: 実行時間や使用可能なリソースに厳しい制限があります。

https://nextjs.org/docs/app/api-reference/edge#unsupported-apis

これらの制約は、ミドルウェアで「できること」と「すべきこと」を大きく規定します。
Edge Runtimeでのミドルウェア処理は、

軽量なリクエスト/レスポンス変換処理(リダイレクト、URL書き換え、ヘッダー操作、簡単な認証トークン検証など)に最適化されており、データベースへのアクセスや複雑なビジネスロジックの実行には本質的に向いていません。

3.3「ミドルウェア」という命名がもたらすミスリード

Theo氏は、そもそもNext.jsチームがこの機能を「ミドルウェア」と名付けたことが、多くの混乱を生んだ原因だと指摘します。

ExpressRuby on RailsLaravelといった他のフレームワークに慣れ親しんだ開発者にとって、「ミドルウェア」という言葉は、「ミドルウェア」はアプリケーション本体と同じランタイムで動作し、比較的自由度の高い処理(認証、認可、ロギング、リクエスト解析、DBアクセスなど)を行う層を想起させます。

しかし今まで見てきたように、Next.jsのミドルウェア(特にEdge Runtime)は、その実行環境と目的が大きく異なります。

実態は「Edgeで動作する、制約付きのリクエスト前処理フック」と呼ぶ方が正確かもしれません。

主なユースケースはルーティング制御やヘッダー操作であり、認証・認可のような重い処理を全面的に担わせる設計は、Edge Runtimeの制約と脆弱性のリスクの両面から推奨されません。

Vercelが目指したのは、CDNレベルでの高速な処理によるTTFB改善やオリジン負荷軽減だったのでしょう。

しかし、「ミドルウェア」という一般的な用語を採用したことで、開発者のメンタルモデルとの間にギャップが生じ、不適切な利用(例: DBアクセスを伴う複雑な認可ロジックの実装試行)や、今回のようなセキュリティリスクへの配慮不足(Edgeの制約下で安易なヘッダー制御に頼る)を招いた側面は否定できません。

3.4 安易な最適化が招いた今回の脆弱性

このような制約の多いEdge Runtime上で、Next.jsは内部サブリクエストにおけるミドルウェアの再帰実行を防ぐという課題に直面したのだと思います。
その結果、実装の容易さや、既存のHTTPプロトコル上で完結するというシンプルさを重視し、HTTPヘッダー(x-middleware-subrequest)を用いた制御を採用したのだと推察します。

ただし、信頼できないクライアント入力を内部制御ロジックの根幹に使用するという判断は、セキュリティ意識があれば避けられたはずで、開発体験やパフォーマンスへの強い志向が、基本的なセキュリティプラクティスを曇らせた典型例と言えるのではないでしょうか。

4. Next.jsはフルスタックか?誤解されやすい責任分界線

今回のインシデントをきっかけに改めて浮かび上がってきたのは、Next.jsというフレームワークの「責任の範囲」がどこまでなのか分かりにくいという問題です。

4.1 「フルスタック」の実態

Next.jsは、公式サイトでも「フルスタック・フレームワーク」と自称しています。

Next.js is a React framework for building full-stack web applications. You use React Components to build user interfaces, and Next.js for additional features and optimizations.

https://nextjs.org/docs#what-is-nextjs

そして実際、以下のような豊富な機能を提供しており、開発体験は非常に洗練されています。

  • App Router(ファイルベースのルーティング)
  • Server Components / Server Actions
  • Edge Middleware
  • API Routes / Route Handlers
  • ISR / SSG / SSRのハイブリッドプリレンダリング
  • Image最適化、フォント読み込み、metadata管理
  • サーバーとクライアントのコード統合

このように見ると、たしかに 「全部入り」感があります。

しかし、DjangoやLaravelのような伝統的なフルスタックフレームワークと比較すると、決定的に欠けているものあります。

それは、認証、認可、セッション管理、高度なセキュリティ機能(CSRF保護の詳細設定、RBAC基盤など)、ORMといった、アプリケーションの根幹をなすセキュリティおよびデータ永続化レイヤーの標準実装部分です。

4.2 Next.jsと他のフレームワークの機能比較

機能 Next.js (App Router) Django Laravel
UI React (Server/Client Comp.) Templates / (React etc.) Blade / (Vue/React etc.)
Routing File-based Routing URLconf Route Files
API Route Handlers DRF / Ninja API Routes / Controllers
Rendering SSR, SSG, ISR, Client Server-side Server-side
認証/認可 外部ライブラリ/自作 django.contrib.auth Auth Scaffolding/Policy
セッション管理 外部ライブラリ/自作 Session Middleware Session/Sanctum/Passport
ORM 外部ライブラリ (Prisma等) Django ORM Eloquent ORM
基本セキュリティ △ (一部機能) ◎ (CSRF, XSS protect etc.) ◎ (CSRF, Middleware etc.)
設計思想 柔軟性/選択肢重視 Batteries Included Progressive Framework
開発者の責任 構成選択・実装責任大 フレームワーク依存度高 フレームワーク依存度中〜高

Next.jsは、これらの重要コンポーネントを「開発者の選択」に委ねています。
これは柔軟性というメリットである一方、開発者は自らセキュリティアーキテクチャを設計・実装・維持する責任を負うことを意味します。

4.3 「統合された体験」 vs. 「統合された実装」

ではなぜ「フルスタック」と呼ばれているのか?

この疑問に対する一つの答えは、「開発体験の統合性」にあります。
Next.jsは、ReactベースのUI、サーバー側ロジック、API、プリレンダリングなどを同一のアプリケーション構造内で完結できるという意味で、確かにフルスタック的です。

RailsやDjangoは、「すべての機能が揃っていて、Rails Way / Django Way に沿って作ればセキュアで拡張性がある」という完成された思想があります。

一方、Next.jsは「フロントもサーバーも同じ構造内で書けるが、何をどう使うかはあなた次第」という極めて柔軟な思想です。

これを一言で言えば、Next.jsは:

「統合された体験」を提供するが、「統合された(特にセキュリティ面の)実装」は提供していない

ということになります。

この違いを理解しないまま「フルスタック」という言葉を鵜呑みにすると、「Next.jsを使っていれば基本的なセキュリティは大丈夫だろう」という危険な誤解を生みます。

4.4 実務でNext.jsを使う開発者が認識すべきこと

Next.jsは素晴らしい柔軟性とスピードを提供してくれるフレームワークです。
しかし、認証やセッションといったアプリケーションの安全性の根幹については、「Next.jsがやってくれる」と思っていると、痛い目を見ることになります。

  • Next.jsは、認証・認可・セッション管理の基盤を提供しない。これらは next-auth, Clerk, Lucia Auth などのライブラリを導入するか、自作する必要がある
  • セキュリティ戦略(認証方式、セッション管理方式、アクセス制御の場所)は、プロジェクト開始時に開発者自身が定義しなければならない
  • DjangoやLaravelのような「推奨される安全な道筋 (The Rails Way的な)」は、Next.jsには(少なくともセキュリティアーキテクチャに関しては)明確には存在しないss

その意味で、Next.jsは「Jamstack時代のヘッドレス構成を自由に組める基盤」と見るべきで、「安全性と統一性をフレームワークに頼りたい人」には向いていない側面もあります。

この「責任分界」の意識こそが、今回の脆弱性の本質的な教訓の一つだと思います。

5. 最大の教訓:多層防御なくして真の安全なし

さて、技術的な詳細、設計思想、プラットフォームの課題を踏まえた上で、我々開発者がこの経験から得るべき、最も重要かつ実践的な教訓を改めて強調したいと思います。それは、多層防御 (Defense in Depth) の原則を、特に認証・認可処理において徹底することです。

5.1 なぜミドルウェア「だけ」では絶対にダメなのか?

今回の脆弱性は、ミドルウェア層がいかに「脆い」防御線となりうるかを明確に示しました。
しかし、仮に今回の脆弱性がなかったとしても、認証・認可をミドルウェアだけに依存する設計は、原理的に危険です。

  • クライアントからの入力は信用できない: HTTPリクエストは、ヘッダー、Cookie、クエリパラメータ、リクエストボディなど、あらゆる要素がクライアント側で操作可能です。ミドルウェアがこれらの情報を基に認証判断を行っている場合、その判断自体が偽装された情報に基づいている可能性があります。
  • 責務の不一致: 前述の通り、Next.jsのミドルウェア(特にEdge Runtime)は、本来、重厚な認証処理を行うようには設計されていません。データベースアクセスや複雑な権限チェックが必要な場合、実行環境の制約からも、ミドルウェアは不適切な場所です。認証・認可というセキュリティの核心的責務は、それを行うのに適した場所、すなわちバックエンド層が担うべきです。
  • 最終防衛ラインの必要性: 本当に守るべきデータ(データベース内の情報、機密ファイルなど)や、実行されると重大な影響がある処理(決済、データ削除など)は、バックエンドに存在します。その直前で、「誰が」「何を」しようとしているのかを最終確認するプロセスがなければ、安全は確保できません。

5.2 多層防御の実践:バックエンドでの厳格な検証

では、具体的にどのように多層防御を実践すべきか? 認証・認可のチェックは、以下のバックエンド層で必ず実施しましょう。

  • APIルート / Route Handlers:

    • リクエストヘッダー(Authorization: Bearer <token>など)やCookieから認証情報(セッショントークン、JWTなど)を取得します。
    • 取得した認証情報を、データベースや外部認証プロバイダ(Auth0, Clerkなど)と照合し、有効性を検証します。
    • 検証されたユーザーIDに基づき、要求された操作(データの取得、作成、更新、削除)を実行する権限があるかをチェックします。
    • これらの検証を処理の冒頭で行い、失敗した場合は適切なエラーレスポンス(401 Unauthorized, 403 Forbidden)を返します。
  • tRPC Procedures:

    • protectedProcedure のようなカスタムプロシージャを作成し、その中でコンテキスト(ctx)からユーザー情報を取得・検証するミドルウェア(tRPCのミドルウェア)を適用します。
    • 認証が必要なすべてのプロシージャに protectedProcedure を使用することで、検証漏れを防ぎます。
    • 必要に応じて、入力データ(input)とユーザー情報(ctx.user)に基づいて、より詳細な認可チェック(例: 特定のリソースへのアクセス権限)をプロシージャ内部で実装します。
  • サーバーアクション:

    • アクション関数の冒頭で、認証ライブラリが提供する関数(例: auth() from @clerk/nextjs/server)を呼び出し、認証状態とユーザーIDを取得します。
    • 認証されていない場合や、必要な権限がない場合は、エラーをスローするか、redirect や新しい forbidden() ヘルパーなどを使って処理を中断します。
  • サーバーコンポーネント / データ取得関数:

    • ページやコンポーネントのレンダリングに必要なデータを取得する関数(例: lib/data.ts 内)の中で、あるいはその関数を呼び出すサーバーコンポーネントの冒頭で、認証状態を確認します。
    • 認証済みユーザーIDに基づいて、アクセス可能なデータのみを取得・フィルタリングするようにします。
    • server-only パッケージを利用して、これらのサーバーサイド専用関数がクライアントにバンドルされないことを保証します。

6. 結論

「CVE-2025-29927」という一件は、Next.jsエコシステム、そして広くWeb開発コミュニティ全体に対して、多くの重要な示唆を与えてくれました。

Next.jsのような高度に抽象化されたフレームワークは、複雑な内部実装を隠蔽し、開発者にシンプルで直感的なインターフェースを提供することで、驚異的な生産性を実現します。

しかし、その「Magic」の裏側で何が起こっているのかを理解しないまま利便性だけを享受していると、今回のような予期せぬ落とし穴にはまるリスクもまた高まります。

特に、セキュリティという、失敗が許されない領域においては、「知らないで使う」ことの代償は計り知れません。

この経験から私たちが学ぶべき最も重要なことは、常にセキュリティの基本原則に立ち返ることの重要性です。
そして、認証・認可というアプリケーションの根幹においては、多層防御の考え方を徹底し、単一の防御策に決して依存しないという設計思想を貫くことです。

ミドルウェアは、あくまでエコシステムの一部であり、便利なツールの一つに過ぎません。
最終的なセキュリティの砦は、常にバックエンドに儲けるべきです。

この記事が、Next.jsを用いた開発に携わる皆さんにとって、より安全で信頼性の高いアプリケーションを構築するための一助となり、また、フレームワークとの健全な向き合い方を考えるきっかけとなれば、幸いです。

codeciaoテックブログ

Discussion

ログインするとコメントできます