🔭

Astroでreact2shellのような脆弱性が起きない理由、そして脆弱にしないために

に公開
5

2025年12月3日に公開されたサーバー側での任意コード実行が可能となるReactにおける脆弱性CVE-2025-55182。通称react2shell。この脆弱性は単なる実装バグとしては片付けられない、フロントエンド界隈全体に及ぶ議論を巻き起こしました。

結論

  • RSFで起こった脆弱性はAstro Actionsでは起きないように設計されている。
  • それ以外にもAstro Actionsはサイトを脆弱にしないための仕組みを持っている。
  • 安全なサイトにするための手段は一つではないし、無数にある。

react2shellはなぜ起こるの?

RSCが有効になっているサーバーに細工されたリクエストを送ることで、サーバー側で攻撃者の指定する任意のコードを実行することができます。具体的にはReactのFlightプロトコルに関数の参照やプロトタイプ汚染などを仕組むことで、認証の有無に関係なく攻撃が可能となります。

さて、一体何が原因だったのでしょうか。

React Flight プロトコルの実装上のバグ

今回の脆弱性の直接の原因です。任意のオブジェクト.constructor.constructorでeval、Thenableの悪用、prototype汚染など、JavaScriptという言語自体の穴を悪用できる状態になっていました。本来このような脆弱性が起こらないように慎重に実装するべきでしたが、対応は不十分でした。

ユーザーからの入力の信頼しすぎ

今回の脆弱性の非常に間接的な原因です。(あくまでマインドセットの問題です。) RSFは「クライアントからサーバー関数を直接呼ぶ」「Promiseの内部状態まで表現できる」というような体験を提供するため、入力値のデシリアライズをフレームワーク側で高度に自動化しています。

この高い自由度を提供するために厳密な検証を十分に行えず、結果として開発者の意図しない隙が生まれてしまいました。


今までの話をまとめると、RSFは大まかに以下の要素が特徴として挙げられます。

  1. 独自プロトコルを採用したことによる実装の難易度(今回の脆弱性の原因
  2. ユーザーからのリクエストの検証の責任が開発者に委ねられる(サイト自体の脆弱性の原因となり得る

種々の問題を解決する手段としては革新的ではあるものの、いざ使うとなると複雑さが垣間見えてしまいます。


さて、所変わってSSGのAstroの話。

Astroの場合

同じ課題(より簡単なフォーム送信やデータ処理)を解決するための手段としてAstro Actionsというものがあります。ここで、Astroがどのようにこの課題にアプローチしているかを見てみましょう。
https://docs.astro.build/en/guides/actions/

Astro Actionsとは?

Astroコンポーネントからサーバー側で行う処理を簡単に実装できる機能です。

大まかにAstro Actionsの流れを解説します。

src/actions/index.tsというファイルにサーバー側で実行するactionを定義します。入力を受け取る場合、その入力のスキーマを指定しておく必要があります。

src/actions/index.ts
import { defineAction } from "astro:actions";
import { z } from "astro/zod";

export const server = {
  // defineActionという専用の関数を使用
  getGreeting: defineAction({
    // スキーマ定義が強制される
    input: z.object({
      name: z.string(),
    }),
    handler: async (input) => {
      return `Hello, ${input.name}!`
    }
  })
}

そして、任意のコンポーネント内で処理を呼び出します。actions.アクション名という形式で呼び出すことができます。

src/pages/index.astro
---
---
<button>Get greeting</button>

<script>
import { actions } from 'astro:actions';

const button = document.querySelector('button');
button?.addEventListener('click', async () => {
  // ここでの`actions`は src/actions/index.tsからexportされた`server`オブジェクトを参照している
  const { data, error } = await actions.getGreeting({ name: "Houston" });
  if (!error) alert(data);
})
</script>

[1]

actionsを利用する際、エディタの補完機能が働きます。

そして、ボタンを押したとき、以下のようなHTTPリクエストが実行されます。

Request
POST /_actions/getGreeting/ HTTP/1.1
Host: localhost:4321
Content-Length: 19
Content-Type: application/json

{
    "name": "Houston"
}
Response
HTTP/1.1 200 OK
Content-Type: application/json+devalue
Vary: Origin

[
    "Hello, Houston!"
]

以上がAstro Server Actionsの概要です。

RSFと比較

独自プロトコルを採用したことによる実装の難易度

Astro Actionsではユーザーからの入力にはプリミティブなJSON(application/json)を使っています。そのため、内部実装はRSFに比べてより簡素なものとなります。

これにより、チャンク参照によるガジェットチェーン(オブジェクトの内部状態や参照を連結させ、メソッドの呼び出し順序を操作することで、攻撃者が任意のコードを実行できてしまう脆弱性)は起こりづらくなります。デシリアライズする時点で、意図しないオブジェクトをサーバー側で復元され、プロトタイプ汚染が起こるリスクも大幅に低減されます。これが「Astroでreact2shellのような脆弱性が起きない理由」です。

ユーザーからのリクエストの検証の責任

Astro ActionsではZodによる検証がサーバー上で必ず実行されます。入力の定義には「関数の引数」ではなく「Zodスキーマ」の形式を使います。

Zodを「定義されたスキーマを通さない限り、フレームワークの処理を開始することができない防壁」として使用しています。また、Zodはパース時に意図しないプロパティを削ぎ落すので、入力の信頼性を高めることができます。

余談:つまりはRPC

Astro ActionsもRSFも、フロントエンドとバックエンドの間で関数呼び出しのような形で通信を行うRPC(Remote Procedure Call)の一種です。しかし、Astro ActionsはRPCの実装において、セキュリティと可読性を重視した設計となっています。というのも、Astro Server ActionsはRSFに比べてより後発の技術であり、RSFの設計や実装を先行事例として踏まえた上で設計されているからです。[2]

RSFがREST APIを直接のサーバー関数呼び出しとして隠蔽する一方、Astro Actionsはactions.アクション名というREST APIエンドポイントに一対一対応した形式として隠蔽します。

Astro Actionsは一々APIを作ってエンドポイントを立てるための、ボイラーテンプレートを省略するために設計されています。

このようなAPIはHono RPCのような他のフレームワークでも採用されている設計ですね。

まとめ

Astro ActionsはJSONというシンプルなフォーマット、スキーマ検証の強制、サーバーコードの分離といった手段を使いながら、RSFと同じ課題を別の方法で解決しています。

AstroはReactを置き換える存在ではありません。まったく別のニーズから生まれたにもかかわらず、同じ課題に直面してしまいました。そこで、Astroチームは(競合ではない)別のフレームワークのやり方を観察して良い方法を模索しました。そして生まれたものがAstro Actionsだったのです。

Special Thanks

このポストがきっかけとなって本記事ができました。Thanks!!

https://zenn.dev/connect0459/scraps/daff522bc625a5

またこちらの記事も一部参考にさせていただきました。

脚注
  1. Houstonとは、Astro公式マスコット(?)の名前です。kawaii ↩︎

  2. https://github.com/withastro/roadmap/issues/898#:~:text=So far%2C Astro,actions%2C and more. ↩︎

Discussion

odanodan

こちらの記事について Twitter での言及当初から公開されることを楽しみにしていて、たいへん興味深く読ませていただきました。

1点質問があります。

自分はこの記事を「Astro は Server Actions の呼び出しに JSON を使っていて zod で validation しているから安全」という意味で読み取りました。
また自分は React2Shell は Flight プロトコルを実行するときに RCE に繋がるバグがあったと理解しています。
zod による validation の話はユーザーの入力値の検証 (フレームワークの使い方) であるのに対して、React2Shell は Flight プロトコルの実装の考慮漏れ (フレームワーク内部の実装ミス) が原因なので、議論のレイヤーが噛み合っていないと感じています。

具体的には Server Actions の input を zod で validation する話は、RSC だと Server Function の引数を validation する話に対応していると思います。
今回の脆弱性は引数の validation が不足していたのが原因ではないので、議論が噛み合っていないと感じた次第です。
RCE に繋がる Flight プロトコルの payload は、 Flight プロトコルとしては valid なものであったはずなので、zod の validation があっても防げないと考えています。

こちらの解釈だとタイトルと記事の内容が一致しないと思うのですが、なにか解釈が誤っているところがあれば教えていただきたいです。

Hideki IkemotoHideki Ikemoto

実装でなく設計の違いですね。入力が信頼できないしPromiseとか渡す必要性がないからAstroはdevalueをサーバ→クライアントにしか使っていない。一方でRSC(RSF)は両方にFlightプロトコルを使っている(ですよね?)。そしてそれが設計思想。

FlightReplyServer are for client->server and ReactFlightClient is for server->client. They're not 100% symmetrical.
(中略)
This PR brings those changes to synchronize the two approaches.
https://github.com/facebook/react/pull/35277

これが根本的な違いです。間違った思想で設計しているからこんな20年前の脆弱性が起きます。実装の問題ではありません。

れやかれやか

@odan
とても参考になるご指摘ありがとうございます。

自分はこの記事を「Astro は Server Actions の呼び出しに JSON を使っていて zod で validation しているから安全」という意味で読み取りました。

この記事は以下の内容を想定して書いたものです。

  1. Astro Actionsではreact2shellを引き起こした仕組みが存在しない。
  2. そのほかにもサイトに脆弱性が生まれないようにする仕組みがある。

Astro ActionsのZodの下りは 2. を想定して書いたものです。タイトルが 1. の内容にしか触れていない上、記事の中でこの二つをうまく切り分けられていませんでした。

こちらの解釈だとタイトルと記事の内容が一致しないと思うのですが、

こちらのご指摘はおっしゃる通りです。

よって、タイトルおよび本文の一部を変更しましたので、ご再読いただけると幸いです。

odanodan

なるほど、ご説明ありがとうございます
たしかに後者の説明なら違和感がないと思いました

修正後の内容も再度読みました
重ねてありがとうございましたmm

れやかれやか

@ikemo
コメントありがとうございます。

実装でなく設計の違いですね。入力が信頼できないしPromiseとか渡す必要性がないからAstroはdevalueをサーバ→クライアントにしか使っていない。一方でRSC(RSF)は両方にFlightプロトコルを使っている(ですよね?)。

はい、そのように理解しています。RSFは入出力の自由度を得るためにFlightプロトコルを使って双方向にやり取りしています。

これが根本的な違いです。間違った思想で設計しているからこんな20年前の脆弱性が起きます。実装の問題ではありません。

RSFの設計思想は間違いとは言えません。言い訳がましく聞こえるかもしれませんが、この記事は「ReactよりAstroのほうが優れている」というような対立を煽ることを意図していません。結局は入力の自由さと入力の検証についてはトレードオフであったと考えています。