Transcript
00:00:00また起きてしまいました。サーバーコンポーネントのCVEに関する動画は今年で3本目ですが、
00:00:05まだすべてを網羅できていない気がします。今回はReactとNext.js全体で13個、
00:00:11そう、13個ものCVEが見つかりました。うち6個は深刻度「高」で、DoS攻撃や
00:00:15ミドルウェアのバイパス、XSSなどが含まれます。サーバーコンポーネントは間違いだったのかもしれません。
00:00:20こちらがNext.jsのセキュリティリリースです。今月だけで、いくつかの重大な問題が
00:00:28修正されています。下の方を確認すると、当然ながら解決策は
00:00:32すべてのNext.jsバージョンをアップグレードすることです。これが影響を受けるバージョンです。
00:00:36特筆すべきは、TanStackはこれに影響されないことです。偏見かもしれませんが、これも
00:00:41私がTanStackを使う理由の一つです。すべてを説明すると時間がかかるので、
00:00:44また、すべてに有効なエクスプロイトが見つかったわけではありませんが、
00:00:48各カテゴリから一つずつ紹介します。まずは「ミドルウェアとプロキシのバイパス」から。
00:00:52再現できたのは、このPages Routerのものです。i18nを使用している場合、
00:00:56Pages Routerでミドルウェア・プロキシのバイパスが発生します。これは深刻度10点中7.5のCVEです。
00:01:02これが脆弱なアプリケーションの例です。Next.jsの設定でi18nを有効にし、
00:01:06英語とフランス語の2つのロケールを設定しました。次にミドルウェアファイルですが、
00:01:12後のバージョンのNext.jsでは、これから示す混乱を避けるために名前が「proxy」に変更されています。
00:01:16基本的に、ミドルウェアによって受信リクエストを変更できるはずです。
00:01:19リダイレクト、書き換え、ヘッダーの追加など何でも可能です。私の例では、
00:01:24「/secret」ページにアクセスしようとすると、セッションCookieがあるか、つまり
00:01:28ログインしているかを確認し、していなければログインページにリダイレクトさせます。
00:01:32理論上、認証済みユーザーだけが秘密のページを見られるはずです。
00:01:37下部にはマッチャーがあり、その秘密のページに適用されるミドルウェアが
00:01:41ロケールのバリエーションにも一致するようにしています。現在2つのロケールがあるため、
00:01:45技術的にはこのURLには3つのバージョンが存在します。秘密のページ自体には、
00:01:50server-side propsがあります。これらはレンダリング時にサーバーから取得されます。
00:01:54ミドルウェアを設定したので、理論的にはログインしたユーザーだけが
00:01:58これらの値を見ることができるはずです。これらは後でページ上で、メールアドレス、フラグ、
00:02:03見出しとして使用されます。繰り返しますが、認証済みユーザーのみが閲覧可能です。では、
00:02:07実際に試してみましょう。まずその秘密のページにアクセスしようとすると、
00:02:11ログインしていないためログインページにリダイレクトされます。ミドルウェアは機能しています。
00:02:16しかし、もし私たちが天才ハッカーだったらどうでしょう? まずは「要素を検証」します。
00:02:20まさにハッカーらしい作業ですね。そして、ここにある `next-data` スクリプトの中から、
00:02:24ビルドIDを探します。私の場合はこれですね。これをコピーします。
00:02:28次に、URLを入力します。`_next/data/` の後にコピーしたビルドIDを続け、
00:02:32アクセスしたいページ名に `.json` を付けます。すると、ご覧の通り、
00:02:37ミドルウェアで保護されているはずのPropsが返ってきました。
00:02:40フラグ、メール、見出し、そして「開発ニュースやチュートリアルのために
00:02:44登録してね」というメッセージまで見えます。ぜひ登録してくださいね。私のハッカースキルに
00:02:48驚かれたかもしれませんが、なぜこれが起きるのでしょうか? 実行したのと同様、
00:02:52説明も簡単です。秘密のページと server-side props がある場合、
00:02:56Next.js では server-side props はこのような形式のURLから提供されますが、
00:03:00ミドルウェアはこのルートを保護しているはずでした。問題は、i18nを使用していたため、
00:03:05英語とフランス語という他の2つのURLも存在していたことです。server-side props も
00:03:09英語とフランス語のバリエーションを持ちますが、Next.js のコードに不備があり、
00:03:13i18nが有効な場合、ベースケースが保護されていませんでした。つまり、これが
00:03:18マッチャーに含まれていなかったのです。英語とフランス語版は保護されていましたが、
00:03:22`/secret` というベースのケースは無防備でした。URLを英語版に変更してみると、
00:03:26すぐにログインページにリダイレクトされるのが分かります。非常に単純な脆弱性ですが、
00:03:31正直に言うと、これらのミドルウェア・バイパスは、聞いた感じほど悪くはありません。
00:03:35良くはないですが、ミドルウェアだけで重要なものを保護すべきではないからです。
00:03:40Next.js 自体もそれを推奨していません。もし server-side props に
00:03:44機密データがあるのにサーバー認証ロジックがないとしたら、
00:03:48問題の一部はあなた側にもあると感じます。では、より深刻な「サービス拒否(DoS)」に移りましょう。
00:03:533つありましたが、確実に再現できたのは一つだけでした。それは、
00:03:56サーバーコンポーネントにおけるDoSです。これは Next.js だけでなく、
00:04:01`react-server-dom` パッケージを使用しているものすべてに影響します。実質的に Next.js と、
00:04:05Vinxi などそれを模倣した他のフレームワークです。TanStack Start は
00:04:10これを使用していないため脆弱ではありません。これも深刻度は 7.5/10 です。
00:04:14これには非常にシンプルな Next.js アプリが必要です。その中で、
00:04:18サーバーアクションを使っている必要がありますが、単純なもので構いません。これが
00:04:22実行中のサイトです。ページをリフレッシュすると、
00:04:25読み込みはほぼ一瞬です。具体的な数値で見ると、リクエストを送ったとき、
00:04:290.02秒で解決しています。しかし、ここでエクスプロイトを実行してから
00:04:34もう一度リクエストを送ると、今回は6秒かかりました。一回実行しただけでこれですから、
00:04:39これを連鎖させたらどうなるか想像してみてください。このエクスプロイトを理解するには、
00:04:42React Flight プロトコルについて知る必要があります。これは React が、サーバーと
00:04:46クライアント間でコンポーネントツリーやデータをシリアライズするために使う形式です。
00:04:50おそらく見たことがあるでしょう。このページにはサーバーアクションを持つフォームがあります。
00:04:54ネットワークタブを開いて送信をクリックすると、ペイロードが
00:04:58このように、一見デタラメなデータとして送られているのが分かります。レスポンスも同様です。
00:05:02このペイロードをコピーして、サーバーに送られたときに何が起きるか説明しましょう。
00:05:05最初のステップはデシリアライズです。チャンク 0 から始まり、そこには
00:05:10この `$k1` があります。この `$k1` はポインタで、ここから1つのアンダースコアで始まる
00:05:16フォームデータが来ることを示しています。ペイロードの一部として送られた
00:05:20他のすべてのキーをスキャンし、1つのアンダースコアで始まる文字列を
00:05:24探します。それを見つけると、それがキーであり、その先が値であると認識します。
00:05:28それが完了すると、名前、メール、メッセージなどのデータを下にあるような
00:05:32オブジェクトに変換できます。非常にシンプルですね。しかし、この手法の問題は、
00:05:36規模が大きくなったときに起こります。例えば、もう一つポインタを追加して、
00:05:41今度は `$k2`(2つのアンダースコアで始まるキー)を探すとしましょう。
00:05:44`$k1` の処理中、1つのアンダースコアで始まるものを探すために
00:05:48これら6つすべてをスキャンします。そして `$k2` に行くと、
00:05:52全く同じことを2つのアンダースコアに対して行います。合計12個のキーを
00:05:56スキャンすることになります。これだけなら悪くありませんが、極端な例を考えましょう。
00:06:03もし 199,999 個のランダムなキーをペイロードに追加し、チャンク 0 の
00:06:07配列を `$k1`, `$k2` から `$k1000` まで増やすと、アンダースコアが1つから
00:06:12アンダースコア1、2、3から1000までを 20万個の全キーの中から探すことになります
00:06:17つまり 合計で2億回の文字列比較が行われる ということなのです
00:06:21想像通り これでは数秒間スレッドが ブロックされてしまいます
00:06:25これが その問題を修正したと思われる コミットです ご覧の通り
00:06:28このコミットには多くの変更が含まれており 正直少し複雑ですが
00:06:33できる限り分かりやすく説明します 基本的には カーソルベースの仕組みが導入されました
00:06:36送信された20万個のキーをリストに読み込み 最初の位置から
00:06:41k1の参照を探し始めます リストを下っていく際 カーソルは逆戻りしません
00:06:45j1を確認して アンダースコア1と一致しないので 次のj2へ進みます
00:06:50j2も一致しないため そのままキーリストを ずっと下っていき
00:06:54最後のj199,999まで到達します ここまで来て
00:07:01k1に一致するものがないと判断し 次のk2へ移動します
00:07:06k2はアンダースコア2を探しますが 問題は カーソルベースであり
00:07:09逆戻りできないため リストの末尾ですぐに 探索が終了してしまうことです
00:07:14結果 undefinedとなり それがk1000まで 繰り返されます
00:07:18今回は 20万個のキーを1回走査しただけです 実質的にこの修正で
00:07:23演算回数は k*n(kは参照数 nはキー数)から n+kまで削減されました
00:07:27今回の場合 2億回の演算が わずか20万1000回まで減ったのです
00:07:33全キーと `$k` 参照を確認する必要は依然としてありますからね。この
00:07:37Primeのツイートが現状をよく表しています。「シリアライズを伴う独自の
00:07:41プロトコルを作るのは非常に難易度が高い」ので、これほど多くの問題が出るのも不思議ではありません。
00:07:46個人的には、Claude Mythos に React と Next.js のコードを一度精査してもらうべきだと思います。
00:07:50次は、今回の中で最も深刻度の高い、Next.js アプリにおける
00:07:54「サーバーサイド・リクエスト・フォージェリ(SSRF)」です。ランクは 8.6/10 ですが、
00:07:59Vercel ホストのデプロイには影響せず、セルフホストや他のプロバイダーのみが対象です。
00:08:04このエクスプロイトも悪用は非常に簡単です。まず Next.js サーバーを起動します。
00:08:09これも標準的な Next.js アプリでよく、何の修正も必要ありません。
00:08:14次に、内部サーバーを用意します。例えば、外部からはアクセスできず、
00:08:18Next.js サーバーからしかアクセスできないクラウド上のサーバーだとします。
00:08:23そして、非常にシンプルな curl リクエストを Next.js アプリケーションに
00:08:26送ります。ポート 3002 で動いているアプリに対し、リクエストのターゲットとして
00:08:31アクセスしたい localhost の URL を指定します。これで実行すると、
00:08:36何が返ってくるか見てください。Python サーバーの HTML ページ、
00:08:40基本的なディレクトリ一覧が表示されました。Python サーバー側を確認すると、
00:08:45確かに Next.js アプリケーションからリクエストが来ていることが分かります。
00:08:49図で説明すると、この点線内が Next.js のデプロイ環境だとします。
00:08:53Next.js サーバーがあり、Redis やデータベースなどの内部サービスがあります。
00:08:57これらには一般公開したくないので、誰も curl リクエストを
00:09:02送ることはできず、ファイアウォールで遮断され Next.js からしかアクセスできません。
00:09:06Next.jsサーバーからしかアクセスできません。私たちが行ったのは、単に
00:09:10内部サービスにリクエストを送ってくれ」と頼んだのです。その結果が返ってきたため、
00:09:15内部アクセス権を持つ Next.js を経由してファイアウォールをバイパスしたことになります。
00:09:19根本的な原因も単純です。curl リクエストで `upgrade: websocket`
00:09:23ヘッダーを送ると、Next.js 内でこのコードが実行されます。
00:09:28これは URL 上のルートを解決しますが、ここで得られるパースされた URL は
00:09:32実は curl で指定したリクエストターゲット、つまり Next.js アプリではなく
00:09:37到達しようとしている内部サーバーのターゲットになってしまいます。
00:09:40パースされた URL は下のチェックを通り、「プロトコルがあるか」を確認されます。
00:09:45HTTP を使っているのでプロトコルは「あり」となり、そのまま
00:09:49リクエストがプロキシされてしまいます。この修正では、`resolveRoutes` 関数に
00:09:552つの新しいガードを追加しました。完了を示す boolean とステータスコードを受け取ります。
00:09:58この関数は URL を受け取り、Next.js の書き換えやミドルウェアに基づいて
00:10:02正当なプロキシリクエストか処理します。そうでなければ `finished` は false になり、
00:10:06下のチェックで false と判定され、プロキシリクエストは実行されません。
00:10:11前述の curl リクエストの場合、まさにこれが起こり `finished` は false になります。
00:10:15もし万が一 `finished` が true になったとしても、次のチェックはステータスコードです。
00:10:20HTTP リクエストであれば 200 や 404 などのコードが返るため、
00:10:24ステータスコードがある場合は有効な WebSocket プロキシリクエストではないと判断し、
00:10:27この行は無視されます。この動画ではこれらの問題を深く掘り下げてきましたが、
00:10:32ここまで見てくれた方は、コメント欄に適当な言葉(例えば「bar」とか)を書いて
00:10:36HTTPリクエストなら 200や404などの ステータスコードが返ってきます
00:10:41カテゴリは手短に説明します。まずは「キャッシュポイズニング」から。
00:10:45深刻度「中」のこの問題を再現しました。React サーバーコンポーネントにおける
00:10:49キャッシュポイズニングで、ランクは 5.4/10 です。再現のために、
00:10:53Next.js アプリと、本番環境を模した疑似 CDN を用意しました。
00:10:58CDN の URL でサイトを訪れ、「製品を見る」をクリックして戻り、またクリックします。
00:11:01初回はキャッシュミス、2回目はキャッシュヒットになるはずです。ログを見ると、
00:11:04最初はクエリ付きの `/products` でミス、次はヒットしています。次に、
00:11:09キャッシュの期限切れなどを想定してキャッシュをクリアし、この curl リクエストを送ります。
00:11:14その後にアプリで「製品を見る」をクリックすると、文字化けしたデータが返ってきます。
00:11:18キャッシュポイズニング攻撃に成功したわけです。何が起きているかというと、
00:11:23`react-server-component: 1` というヘッダーで curl を送ると、
00:11:27その URL は HTML ではなくサーバーコンポーネントのデータを返します。
00:11:31次にそれがキャッシュされる際、サーバーコンポーネントのデータかどうかをチェックする
00:11:35関数を通ります。データであればそのように、そうでなければ HTML として保存されます。
00:11:39理論上、ユーザーがページを普通に開いて HTML 版を取得しようとするときに、
00:11:44サーバーコンポーネントのデータが返ることはありません。しかし今回のケースでは、
00:11:47curl リクエストの末尾にクエリ文字列があったため、サーバーコンポーネントかどうかの
00:11:52要件を満たしませんでした。単純に末尾が `.rsc` かどうかをチェックしていたからです。
00:11:58末尾がクエリ文字列だったので、システムは HTML だと思い込んでキャッシュに保存し、
00:12:02次にユーザーがアクセスした際に、HTML としてサーバーコンポーネントのデータを返したのです。
00:12:07修正は非常に簡単で、`.rsc` で終わるかチェックする際に、
00:12:11クエリ文字列を無視するように変更されました。いよいよ最後のカテゴリ、
00:12:16「クロスサイトスクリプティング(XSS)」の CVE です。再現できたのはこれです。
00:12:20深刻度は 6.1/10 で、信頼できない入力を含む `beforeInteractive` スクリプトでの XSS です。
00:12:24簡単に言うと、Next.js で `strategy="beforeInteractive"` を持つスクリプトタグがあり、
00:12:29検索パラメータ(Search Params)などの信頼できない内容を含む属性がある場合、
00:12:33XSS が可能になります。例えば、検索パラメータに大量の内容を埋め込んだ
00:12:37このようなリンクをユーザーにクリックさせたとします。すると、
00:12:41ユーザーにはログイン画面の再入力を求めるような表示が出ますが、実際には
00:12:46検索パラメータ経由で注入された偽のログインフォームです。これにより攻撃者は
00:12:50被害者のマシンで JavaScript を実行できます。より現実的な例としては、
00:12:55Chrome のセッション Cookie を盗んで、アクセス権のあるすべてにログインすることでしょう。
00:12:59これは不適切なエスケープの典型例で、簡略化したバージョンで説明します。
00:13:03検索パラメータで、まずスクリプトタグを閉じ、次に新しいタグを開いて
00:13:08実行したい内容を書きます。これで実行すると、ご覧の通り「pwned」と
00:13:12アラートが出ました。仕組みとしては、`beforeInteractive` の Next.js
00:13:16スクリプトタグがあり、クエリパラメータなどの信頼できないソースから
00:13:19データを受け取る属性がある場合、Next.js 内では `dangerouslySetInnerHTML` を
00:13:22使う形式に変換されます。名前からして危険そうですが、ここで `JSON.stringify` も
00:13:26被害者のマシンでJavaScriptを実行できるのです。より現実的な例を挙げるとすれば
00:13:31HTML 文字をエスケープしないことです。実際には Next.js のスクリプトタグから
00:13:34ソース(src)を探し、残りの Props(データトラッキングIDやクエリパラメータの値)が
00:13:39この `JSON.stringify` に入ります。ページ上にレンダリングされると、
00:13:43データトラッキングIDがあり、その後にクエリパラメータで挿入した文字列が続きます。
00:13:47これを展開すると、スクリプトタグがあり、データトラッキングIDがあり、
00:13:51その直後に閉じスクリプトタグが出現します。これで Next.js が意図した
00:13:55スクリプトが終了し、その後に任意のスクリプトを実行できてしまいます。
00:13:59最後にあるこの部分は、これがないと「analytics」という文字が
00:14:04ただのテキストとして表示されてしまうのを防ぐためです。不審な動きを
00:14:08隠蔽しているわけです。以上、今週見つかった Next.js と React の
00:14:1213個の CVE と、そのいくつかの仕組みでした。正直、どう考えればいいのか
00:14:17分かりません。嫌な気分です。2年ほど前はすべてのプロジェクトに Next.js を使い、
00:14:21それが最高で未来だと思っていましたが、ハードルの連続のように感じます。
00:14:24色々なことを急ぎすぎて、後から修正に追われている印象です。個人的には今、
00:14:29完全に TanStack や、コンテンツ系なら Astro に傾倒しています。
00:14:33それらの方がずっとシンプルに見えます。また最近の Cloudflare の動きも
00:14:37気に入っていて、徐々にプロジェクトを移しています。ただ、Vercel にもまだ
00:14:4120ほどプロジェクトがあるので、アップデートしに行かないといけません。
00:14:45皆さんはどう思いますか? サーバーコンポーネントはいつか役立つのか、
00:14:49それとも試みは失敗だったのでしょうか? コメントで教えてください。登録も忘れずに。
00:14:53それでは、また次回の動画でお会いしましょう。
00:14:58この部分がないと、アナリティクスがページ上に
00:15:01テキストとして表示されてしまいます。HTMLがそう認識するからです。これはそれを覆い隠し、
00:15:05ページで何か不具合が起きている形跡を消してくれます。というわけで、
00:15:09今週のNext.jsとReactに関する13個の脆弱性と、その仕組みの解説でした。
00:15:14正直、これについてはどう考えればいいか分かりません。2年ほど前は
00:15:18すべてのプロジェクトでNext.jsを使い、それが最高で未来だと思っていましたが、
00:15:22今はハードルの連続のように感じます。急いで導入して、後から修正している印象です。
00:15:26個人的には、今は完全にTanStack派ですし、コンテンツ重視のサイトなら
00:15:31Astroを使います。その方がずっとシンプルに感じられるからです。また、
00:15:35最近のCloudflareの動きも非常に気に入っているので、徐々に
00:15:39プロジェクトを移行しています。ただVercelにもまだ20ほど残っているので、
00:15:43更新しに行かないといけません。サーバーコンポーネントは本当に有用になると思いますか?
00:15:48ぜひコメント欄で教えてください。高評価とチャンネル登録もお願いします。
00:15:51それでは、また次回。