シングルページ アプリケーションの脆弱性: 修正方法
Mandiant
※この投稿は米国時間 2025 年 1 月 16 日に、Google Cloud blog に投稿されたものの抄訳です。
概要
-
シングルページ アプリケーション(SPA)は通常、クライアントサイドの性質上、アクセス制御に関する複数の脆弱性を抱えている
-
アプリケーションを支える API に堅牢なアクセス制御ポリシーを実装することで、クライアントサイド レンダリングに関連するリスクを大幅に軽減できる
-
SPA 内でサーバーサイド レンダリングを使用すると、権限のないユーザーがページやデータを変更したり、表示したりするのを防ぐことができる
はじめに
シングルページ アプリケーション(SPA)は、動的でユーザー フレンドリーなインターフェースを備えているため幅広く利用されていますが、セキュリティ リスクをもたらす可能性もあります。SPA によく実装されるクライアントサイド レンダリングにより、不正アクセスやデータ操作に対して SPA が脆弱になることがあります。このブログ投稿では、ルーティングの操作、非表示の要素の表示、JavaScript のデバッグなど、SPA 特有の脆弱性について考え、それらのリスクを軽減するための推奨事項を示します。
シングルページ アプリケーション
SPA はウェブ アプリケーション デザイン フレームワークで、アプリケーションが JavaScript によってコンテンツの非表示 / 表示、または変更が行われる単一のドキュメントを返します。これは、従来 PHP または HTML サイトだけに実装されていたフラット ファイル アプリケーション フレームワークや、データ、ビュー、サーバー制御がアプリケーションのさまざまな部分で処理されるモデル ビュー コントローラ(MVC)アーキテクチャとは異なります。SPA の動的データは API 呼び出しを通じて更新されるため、ページの更新や別の URL へのナビゲーションが不要になります。このアプローチにより、SPA はネイティブ アプリケーションのようになり、シームレスなユーザー エクスペリエンスが実現します。SPA の実装によく使用される JavaScript フレームワークには、React、Angular、Vue などがあります。
クライアントサイド レンダリング
クライアントサイド レンダリングを使用する SPA では、サーバーは CSS、メタデータ、JavaScript のみ含まれる HTML ドキュメントでリクエストに応答します。最初に返される HTML ドキュメントにはコンテンツが含まれませんが、JavaScript ファイルがブラウザで実行されると、実行時にアプリケーションのフロントエンド ユーザー インターフェース(UI)とコンテンツが HTML ドキュメントに読み込まれます。アプリケーションがルーティングを使用するよう設計されている場合、JavaScript は URL を取得し、ユーザーがリクエストしたページの生成を試みます。これが行われている間、アプリケーションはデータの読み込みと、現在のユーザーがデータにアクセスする権限を持っているかどうかの確認のために、API エンドポイントにリクエストを送信します。ユーザーがまだ認証されていない場合、アプリケーションはログインページをレンダリングするか、ユーザーを別のシングル サインオン(SSO)アプリケーションにリダイレクトして認証を行います。
これらすべてが行われている間、アプリケーション ダッシュボードまたはログインページがブラウザに読み込まれる前に、ユーザーには空白のページが一時的に表示されることがあります。この空白の時間に、アプリケーションは自身のユーザー エクスペリエンス全体を構築する、数十万行の圧縮 JavaScript を読み込む可能性があります。SPA は、Netflix、Hulu、Uber、DoorDash など、世界中の何百万ものアプリケーションで使用されています。
クライアントサイド レンダリングの問題
SPA はコンテンツをレンダリング(API データを使用)する際に完全にクライアントのブラウザに依存するため、ユーザーはアプリケーションのかなりの部分を制御することができます。このため、ユーザーがアプリケーションを自由に操って、ユーザーまたはロールを簡単に偽装できてしまいます。
ルーティング
SPA が実装される JavaScript フレームワークの基本的な側面の一つは、ルートの概念です。これらのフレームワークはルートを使用して、アプリケーション内のさまざまなページを表示します。この場合のルートは、ダッシュボードやユーザー プロファイルなど、ユーザーから見えるさまざまなビューを指します。すべての JavaScript はクライアントのブラウザによって処理されるため、クライアントはアプリケーション ソースに含まれる JavaScript ファイルでこれらのルートを表示することができます。ユーザーがこれらのルートを識別できる場合、そのいずれかにアクセスを試みることができます。JavaScript の実装方法によっては、ユーザーが特定のルートにアクセスできるかどうかを確認するためのチェックが行われる場合があります。以下に示すのは、ビューの作成に関する情報、さらに重要なパス属性を含む React ルーティングの例です。
In = function () {
return (0, _.jsx)(d.rs, {
children: (0, _.jsxs)(ki, {
children: [
(0, _.jsx)(d.AW, {
path: "/dashboard",
children: (0, _.jsx)(Ii, {}),
}),
(0, _.jsx)(d.AW, {
path: "/users",
children: (0, _.jsx)(wi, {}),
}),
(0, _.jsx)(d.AW, {
path: "/profile",
children: (0, _.jsx)(Ti, {}),
}),
],
}),
});
};
非表示の要素
SPA がアクセス制御を処理する方法の一つとして、非表示ページ要素の使用があります。つまり、ページが読み込まれると、アプリケーションはローカル / セッション ストレージ、Cookie 値、またはサーバー レスポンスを通じてユーザーのロールを確認します。アプリケーションは、ユーザーのロールを確認した後、そのロールに基づいて要素を表示または非表示にします。ユーザーがアクセスできる要素のみをレンダリングする場合もあります。また、すべての要素をレンダリングしつつ、それぞれの要素の CSS プロパティを制御することによって「非表示」にすることもあります。非表示の要素はブラウザのデベロッパー ツールを通じて見えるようにできるため、ユーザーはこれらの要素を強制的に表示できます。このような非表示の要素には、フォーム フィールドのほか、他のページへのリンクもあります。
JavaScript のデバッグ
最新のブラウザでは、ユーザーがブレークポイントを使用して JavaScript をリアルタイムでデバッグできます。最新のウェブブラウザでは、JavaScript ファイルにブレークポイントを設定できるため、変数を変更したり、関数をまとめて書き換えたりすることができます。コア関数をデバッグすると、ユーザーがアクセス制御をバイパスし、不正なページアクセスを取得できる可能性があります。以下の JavaScript について考えましょう。
function isAuth() {
var user;
var cookies = document.cookies;
var userData = btoa(cookies).split(‘:’);
if (userData.length == 3) {
user.name = userData[0];
user.role = userData[1];
user.isAuthed = userData[2];
} else {
user.name = “”;
user.role = “”;
user.isAuthed = false;
}
return user;
}
上で定義した関数は、ユーザーの Cookie を読み取って、値を Base64 デコードし、区切り文字として : を使用してテキストを分割し、値が一致する場合はユーザーが認証済みであるとみなします。攻撃者は、これらのコア関数を識別すると、クライアントサイド アプリケーションによって処理されているすべての承認およびアクセス制御を回避できるようになります。
悪用
JavaScript フレームワークの問題を手動で悪用するには時間と練習が必要ですが、それを簡単に行う手法がいくつかあります。一般的な手法としては、JavaScript ファイルを分析してアプリケーション ルートを識別する方法が挙げられます。ルートを識別すると、UI を介さずにアプリケーション ページを「強制参照」して直接アクセス可能になります。この手法は単独でも機能しますが、アプリケーション内のロール チェックの識別が必要となる場合もあります。これらのチェックには、JavaScript Debugger を通じてアクセスでき、実行中に変数を変更して承認または認証チェックをバイパスできます。もう一つの便利な手法は、Burp Suite Professional などの HTTP プロキシでユーザー情報のリクエストに対するサーバー レスポンスをキャプチャし、ユーザー オブジェクトを手動で変更する方法です。これらの悪用手法は効果的ですが、強力な予防措置によって軽減できます。この投稿でいくつか詳しく説明します。
推奨事項
アクセス制御の問題は、クライアントサイドでレンダリングされる JavaScript フレームワーク全体の問題です。ユーザーがアプリケーションをブラウザに読み込んでしまうと、そのユーザーが不正な方法でコンテンツを操作するのを防ぐ効果的な軽減策はほとんどありません。一方、API において堅牢なサーバーサイド アクセス制御チェックを実装することにより、攻撃者がもたらす可能性のある影響が大幅に軽減されます。攻撃者は、管理者のコンテキストでページがどのように表示されるかを確認したり、特権リクエストの構造を確認したりすることはできるかもしれませんが、制限されたデータを取得したり変更したりすることはできません。
API リクエストをロギングおよびモニタリングし、権限のないユーザーが保護されたデータにアクセスしようとしたかどうかや、アクセスに成功しているかどうかを識別する必要があります。加えて、セキュリティのギャップを特定するために、ウェブ アプリケーションと API の存続期間全体にわたって、定期的にペネトレーション テストを実施することをおすすめします。ペネトレーション テストでは、アクセス制御の実装が部分的または不完全になっている API が明らかになるため、攻撃者に悪用される前に欠陥を修正する機会が得られます。
API アクセス制御
堅牢な API アクセス制御の実装は、SPA を保護するために重要です。アクセス制御メカニズムでは、ユーザーによるセッション トークンの変更または偽造を防ぐため、JSON ウェブトークン(JWT)または他の一意の不変のセッション識別子を使用する必要があります。API エンドポイントはセッション トークンを検証し、すべてのやり取りに対してロールベースのアクセスを適用する必要があります。API は、ユーザーが認証済みかどうかをチェックするよう構成されていることがよくありますが、エンドポイントへのユーザーロール アクセスを包括的にチェックするわけではありません。場合によっては、正しく構成されていないエンドポイントが 1 つあるだけで、アプリケーションが危険にさらされる可能性があります。たとえば、新規ユーザーを作成する管理エンドポイントを除く、すべてのアプリケーション エンドポイントがユーザーのロールをチェックしている場合、攻撃者は管理者ユーザーを含む任意のロールレベルでユーザーを作成できます。
適切な API アクセス制御の例を図 1 に示します。


図 1: 適切な API アクセス制御の例
この図は、ユーザーがアプリケーションに対して認証を行い、JWT を受け取ってページをレンダリングする様子を示しています。ユーザーは SPA とやり取りし、ページをリクエストします。SPA はユーザーが認証されていないことを識別するため、JavaScript はログインページをレンダリングします。ユーザーがログイン リクエストを送信すると、SPA は API リクエストを通じてそれをサーバーに転送します。API は、ユーザーが認証されたことを知らせ、後続のリクエストで使用できる JWT を提供します。SPA は、サーバーからレスポンスを受け取ると、JWT を保存し、ユーザーが最初にリクエストしたダッシュボードをレンダリングします。
同時に、SPA は API からページのレンダリングに必要なデータをリクエストします。API は、データをアプリケーションに送り返し、そのデータがユーザーに表示されます。次に、ユーザーはクライアントサイドのアクセス制御をバイパスする方法を見つけ、アプリケーションのメイン管理ページをリクエストします。SPA は、管理ページのデータをレンダリングするための API リクエストを生成します。バックエンド サーバーはユーザーのロールレベルをチェックしますが、ユーザーは管理者ユーザーではないため、サーバーはデータへのアクセスがユーザーに許可されていないことを示す 403 エラーを返します。
図 1 の例は、API アクセス制御によって、ユーザーによる API データへのアクセスを防ぐ方法を示しています。例に示すように、ユーザーは SPA 内のページにアクセスできましたが、API アクセス制御によって、ページの完全なレンダリングに必要なデータにアクセスすることはできません。C# または Java で開発された API の場合、アクセス制御の実装を簡素化するためのアノテーションがフレームワークに用意されていることがよくあります。
サーバーサイド レンダリング
API アクセス制御以外にこの問題を軽減するもう一つの方法として、Svelte-Kit、Next.js、Nuxt.js、Gatsby など、サーバーサイド レンダリング機能を備えた JavaScript フレームワークを使用できます。サーバーサイド レンダリングは、MVC アーキテクチャと SPA アーキテクチャを組み合わせたものです。サーバーは、すべてのソース コンテンツを一度に配信するのではなく、リクエストされた SPA ページをレンダリングし、最終的な出力のみユーザーに送信します。クライアント ブラウザがルーティング、レンダリング、アクセス制御を行うことはなくなります。サーバーは、HTML をレンダリングする前にアクセス制御ルールを適用し、許可されたユーザーのみが特定のコンポーネントまたはデータを表示できるようにします。
サーバーサイド レンダリングの例を図 2 に示します。


図 2: サーバーサイド レンダリングの例
この図は、サーバーサイドでレンダリングされたアプリケーションにユーザーがアクセスする様子を示しています。アプリケーションで認証済みページがリクエストされると、サーバーはユーザーが認証されていて、そのページを表示する権限を持っているかどうかをチェックします。ユーザーはまだ認証されていないため、アプリケーションはログインページをレンダリングし、そのページをユーザーに表示します。次に、ユーザーが認証を行って、サーバーがセッションを構築し、必要な Cookie やトークンを設定して、ユーザーをアプリケーション ダッシュボードにリダイレクトします。リダイレクトされたユーザーはリクエストを生成し、サーバーが認証状態をチェックします。ユーザーにはページへのアクセス権があるため、サーバーは必要なデータを取得し、そのデータを使用してダッシュボードをレンダリングします。
次に、ユーザーは管理ページの URL を識別し、アクセスを試みます。このインスタンスで、アプリケーションは認証状態とユーザーのロールをチェックします。ユーザーは管理者ロールを持っていないため、ページの表示は許可されず、サーバーは 403 Forbidden
またはエラーページへのリダイレクトで応答します。
おわりに
まとめると、SPA は動的で魅力的なユーザー エクスペリエンスを提供しますが、クライアントサイド レンダリングを使用して実装すると、特有のセキュリティ上の課題も生じます。ルーティング操作、非表示の要素の表示、JavaScript のデバッグなど、SPA 特有の脆弱性を理解することで、開発者はリスクを軽減するための積極的な対策を講じることができます。堅牢なサーバーサイド アクセス制御、API セキュリティ対策、サーバーサイド レンダリングの実装は、SPA を不正アクセスやデータ侵害から保護するための優れた方法です。定期的なペネトレーション テストとセキュリティ評価を実施することで、アプリケーション内に存在するセキュリティ ギャップを特定し、悪用される前に開発者が修正できるようになるため、SPA の全体的なセキュリティ ポスチャーをさらに強化できます。開発者は、セキュリティのベスト プラクティスを優先させることにより、SPA においてシームレスなユーザー エクスペリエンスとセンシティブ データのための安全な環境の両方を確実に提供できます。
-Mandiant、執筆者: Steven Karschnia、Truman Brown、Jacob Paullus、Daniel McNamara