はじめに
こんにちは。株式会社Flatt Security セキュリティエンジニアの石川です。
本稿では、ログイン機能をもつWebアプリケーションにおける実装上の注意を、マイページ機能から派生する機能のセキュリティ観点から記載していきます。特に、XSS(Cross-Site Scripting)やSQLインジェクションのような典型的な脆弱性と比較して語られることの少ない「仕様の脆弱性」にフォーカスしていきます。
これから述べる実装上の注意点は、実際にはマイページ機能であるかどうかに関係なく注意するべきです。
しかし、開発者の視点に立つと「これこれの機能にはどのようなセキュリティ観点があるか」という形が読みやすく、また他の機能の仕様のセキュリティを考える上で想像力を働かせやすいものになるのではないでしょうか。
また、株式会社Flatt Securityではお客様のプロダクトに脆弱性がないか専門のセキュリティエンジニアが調査するセキュリティ診断サービスを提供しています。料金に関する資料を配布中ですので、ご興味のある方は是非ご覧ください。
前提
本稿では、一般的なログイン機能が存在するWebアプリケーションの簡単な例と、 実装上の注意を述べていきますが、最初に、前提となる簡易的なWebアプリケーションの仕様について説明します。
この簡易的なWebアプリケーションでは、ログイン機能が存在し、自分自身のプロフィールを表示する機能があるものとします。 本稿では、自分自身のプロフィールを表示する機能のことをマイページと定義します。
想定しているデータベース構造は、簡単に以下のようなフィールドを同一テーブル内に保持するものとします。
フィールド名 | 概要 |
---|---|
id | システムの内部で使用 |
user_id | 公開情報 |
name | 公開情報 |
description | 公開情報 |
password_hash | システムの内部で使用 |
address | 自分自身以外には非公開 |
このDBの上で動作するアプリケーションでは、ユーザーはログインした後に、マイページを表示したり、他人のプロフィールを閲覧できます。 ログイン方法は、JWT等を用いる認証ではなくCookieを用いるようなセッション認証とします。
2つのURL形式
マイページのURLは一般的に、形式として2種類存在します。
第一に、マイページを指す際に、ログイン中のユーザーの識別子をURL中に含むものです。 例としてTwitterが挙げられます。Twitterでは、マイページにアクセスすると https://twitter.com/flatt_security のような形式のURLに遷移します。
本稿ではこの形式を「絶対的なURL」と定義します。 絶対的なURLでは、ログイン中のユーザーと、Path Parameterで指定されたidとログイン中ユーザーのidが一致した場合に、プロフィールを編集する、というボタンが見えるものとします。
第二に、ユーザーの識別子をURL中に含まないものです。例として、/me
のような形式です。
この形式を、本稿では「相対的なURL」と定義します。今回、相対的なURLを採用する際には他ユーザーのプロフィールページは閲覧できない仕様であることを前提とします。
最初に絶対的なURLの場合、次に相対的なURLの場合、最後に、URLの形式を問わずに注意しなければならない点について述べます。
絶対的なURLの場合の観点と対策
マイページのURLが絶対的な形式である場合、実装上特に気をつけなければならない点について述べます。
観点1: 認可制御の不備
あるユーザーアカウントのマイページが GET /users/flatt_security
で表される場合、
ユーザーアカウントを更新するためのエンドポイントは、PUT /users/flatt_security
のようになることが多いです。
このときのURLの形式を見てみると、PUT /users/{id}
となりますが、このidの値をバックエンドで信頼しすぎるとどうなるでしょうか。
最も単純で不適切な実装例は以下のようなフローでしょう。
- usersテーブルに対して、リクエストで指定されたidを持つユーザーの情報を更新。
<?php public function update($request){ $user_id = $request->id; App\User::where('id', $user_id)->update($request); }
この実装では、リクエストに含まれるid
を書き換えることによって、どのアカウントでも好きなリソースに対して更新操作が行えてしまいます。
このような脆弱性をロジックの不備と呼ぶこともありますが、一般的にツールによる脆弱性スキャンでは見つけることが難しく、調査のためにはセキュリティエンジニアによる手動診断が推奨されます。
対策1: 送信元ユーザーと変更対象の整合性を確認する
HTTPリクエストで更新を試みている対象のリソースが「ログイン中のアカウントが、正当な権限をもっているか」を確認しなければなりません。
先程の単純なフローから、権限確認を含む以下のようなフローに実装を変更する必要が出てきます。
ログイン中のアカウントが、指定されたid自身であるかどうか確認。 usersテーブルに対して、リクエストで指定されたidを持つユーザーの情報を更新。
<?php public function update($request){ $auth_id = Auth::user()->id; if($request->id != $auth_id){ return; } App\User::where('id', $auth_id)->update($request); }
例示したものは、単純なものとなっていますが、実際のWebアプリケーションでは、リソースのリレーションが連なっている中で、 「実行しようとしているアカウントが、その操作を実行するための権限があるかどうか」をその都度確認することが実装上重要と言えます。
観点2: 不要・不適切なリソースへのアクセス権
次に、不要・不適切なリソースへのアクセス権についてです。これもロジックの不備に分類される脆弱性です。
ここでは、他人のプロフィールページにおいて、そのユーザーに関する情報をGETするエンドポイントについて考えます。
GET /users/{id}
といったエンドポイントの想定されるレスポンスを仮に以下のようにします。
例1
{ "user_id":"flatt-security", "name":"flatt-security", "description":"開発者のための次世代セキュリティサービスを届け世界中のプロダクト開発を加速する" }
上の例では、他人のプロフィールページマイページの表示に必要な情報が過不足なく取得できていると言えます。 実装上、ユーザーのリソースを取得した後に、必要なフィールドのみを抽出しレスポンスできていることがわかります。
では、下の例ではどうでしょうか。
例2
{ "id":523, "user_id":"flatt-security", "name":"flatt-security", "description":"開発者のための次世代セキュリティサービスを届け世界中のプロダクト開発を加速する", "password_hash": "839dbee0768ef7870d0ceabfec2b7c4d624b04cd2f70881499f3bde632611c87", "address":"〒113-0033 東京都文京区本郷3-43-16 コア本郷ビル2A", }
このレスポンスには、連番となっているidやパスワードのハッシュ等、外部から見られることを想定していないフィールドが情報として含まれています。
なお、復号できないはずのハッシュ化されたパスワードが外部に露出することは問題のあることなのか?と疑問に思う方もいらっしゃると思いますが、適切な対策が行われていないと効率的に元の値を解析するツールを用いて復号されてしまう場合も存在はするので、問題ありと見るべきでしょう。これ以上本記事では深入りはしませんが、想定していない要素の露出はさらなる想定外のリスクにつながり得るため、基本的には避けるべきです。
対策2: 必要最低限の情報に絞ったレスポンス
実際はUI上に表示しない情報であっても、レスポンスに不要なフィールドが含まれている場合、コンソール上から個別のレスポンスを直接見ることで、情報を読み取ることができてしまいます。今回の例では、他人のプロフィールページにアクセスする際にレスポンスに含まれる情報は、例1で羅列したフィールドに絞るべきです。
また、ログイン中のユーザーと一致している場合、すなわちマイページにアクセスした際は、下に示すようなフィールドに絞って、レスポンスに含めるべきと言えます。他人のプロフィールページの場合に比較して address
のフィールドが増えるので、これに引きずられて他人のプロフィールページでも address
をレスポンスしないよう、気をつけながら実装を進めましょう。
例3
{ "user_id":"flatt-security", "name":"flatt-security", "description":"開発者のための次世代セキュリティサービスを届け世界中のプロダクト開発を加速する", "address":"〒113-0033 東京都文京区本郷3-43-16 コア本郷ビル2A", }
このセクションの結論として、レスポンスで情報を返す場合は、その権限・要件に応じた必要最低限の情報のみに絞ることが必要であることが言えます。
観点3: URLで使用されている予約語をidとして使用できる
次にURLとして使用する予約語を、ユーザーが自由に設定できるIDとして指定することができることについてです。
本稿で前提としているURL設計では、ユーザーのプロフィールを表示する際に、/users/{id}
という形式を採用しているため、この問題は発生しないと言えますが、サービスによっては異なります。
例えば、Twitterでは、ユーザーページを表示する際に、/{id}
という形式を採用しています。また、他のページ、たとえばトップページでは、/home
というURLになっており、ダイレクトメッセージの一覧では、/messages
というURLになっています。
すなわち、ユーザーが自由に設定できるIDと、その他のルーティングが同じ階層に存在しています。この場合、ユーザーがIDを設定する際に、他のルーティングで使用している文字列を禁止しなかった場合、静的に割り当てられているURLが優先され、ユーザー情報の表示や更新などが正しくできなくなります。
また、ある時点では予約語ではなかった場合であっても、URLの更新により、このような状態になる可能性があります。
例として、Twitterでは、昔はトップページのURLが/
でしたが、ある時点から/home
になりました。
実際にTwitterに @home
さんは存在していますが、この方は、Web上からでは、URLがトップページに割り当てられてしまっているため、アクセスできず、スマートフォンアプリケーション経由でのアクセスのみ可能になっています。
こういった問題は、セキュリティ的なリスクで考えると、「ユーザーIDに予約を設定できるため、サービスの正常な利用ができなくなる」と言えます。 ある時点でのURL設計では弾けていた予約語も、サービス運用後の設計にこのような問題が発生することもあり得るため、注意が必要です。
対策3: 拒否リストによる管理やURL設計による対処
考えられる最初の対策としては、将来的に使うであろう文字列の拒否リスト方式で管理することです。
Twitterの例で考えると、messages
や notifications
など、サービスの特性上考えうる機能のためのパスを予め予約しておくことです。しかし、この方式ですと、上述したように、月日とともに起こりうる仕様変更に対応できない可能性があります。
そこで、次に考えられる対策として、専用の名前空間(パス)を持った設計にすることです。こちらも上述したように、/users/{id}
のように、ユーザーが設定できる文字列のprefixとして、名前空間を付与することです。あるいは、設計思想上、どうしても他のパスと同じ名前空間にユーザーIDを設定したい場合には、パスには利用しない記号や文字列などをユーザーIDに付与する、という仕様も考えられます。たとえば、@
を{user_id}
の前につけることで、同じ名前空間でありながら、ユーザーが設定した文字列であるかどうかをシステム側で認識することができます。
相対的なURLの場合の観点
次に、相対的なURLにおける実装上の注意について記述します。
相対的なURLでは、GET /me
のようなURLで、マイページを表示する際に、ユーザーの識別子をリクエスト中に含まないため、
ログイン時にアカウントと紐付けられたセッションや、トークンから対象リソースを割り出すことになります。
そのため、そもそも変更権限のないリソースを指定することが構造上難しいと言えます。 したがって、相対的なURLでは、絶対的なURLで発生しやすい、リソースの所有権確認の不備の脆弱性は、発生しにくいと言えます。 この形式のURLでは、別のレイヤーにおいて特に注意するべき事項として、以下の点が挙げられます。
不適切なキャッシュ・CDNの公開範囲
Webアプリケーションでは、サーバーやデータベースへの負担を減らすため、しばしば、CDN(コンテンツ・デリバリー・ネットワーク)および、さまざまなレイヤーにおけるキャッシュを用います。CDNは、地理的に近いサーバーにコンテンツをキャッシュすることで、オリジンサーバーへのアクセスを減らすとともに、レスポンスを高速化することが目的ですので、本稿ではキャッシュと同様に扱います。
これらの機構は、ログイン判定を含むサーバーのロジックの外で動作することもあるため、設定によっては認証後のユーザー自身のみが知るべき情報が乗り、権限外のユーザーに配信されてしまうこともあり得ます。
上述したように、キャッシュにはさまざまなレイヤーが存在し、それぞれ必要に応じた設定を行います。
ユーザーに近い部分のキャッシュで言うと、まずはブラウザのキャッシュが挙げられます。 スクリプトファイルやCSSファイルなど、HTMLソースの中から呼ばれる静的ファイルなどを、ページアクセスのたびに取得することは、ネットワークの帯域的にも、応答時間的にも無駄なことです。ですので、タイムスタンプ等の変更がない場合や、ユーザーからの明示的なアクションがない場合、一定時間以上経過した場合などを除いて、ブラウザに保存することで、同一のファイルを何度も取得する無駄を省いています。
サーバーのロジックにおいても、DB(データベース)や、一部あるいは全てのHTMLファイルのキャッシュなどを設定することもあります。
上述した、Webアプリケーションに実装されそうな、「自分のプロフィールページ閲覧」の機能を例として、HTMLキャッシュにおけるキャッシュのロジックミスについて考えます。
相対的なURLを採用したURL設計においては、GET /me
とアクセスした際に、自分自身のプロフィールが閲覧可能であり、単純化したロジックは以下です。
<?php public function me($request){ $user_id = Auth::user()->id; $user = App\User::getByID('id', $user_id); return $user; }
ここで、誤って、/me
というURLに対して、HTML全体をキャッシュする設定にしてしまった場合を考えます。
今回はあくまで例なので、少し不自然にサーバー内のロジックでレスポンスをキャッシュするようにしています。 実際の例では、CDNやリバースプロキシに誤ってキャッシュしてしまうなどの設定ミスが考えられます。
ロジックとしては下に示すようなものです。
<?php public function me($request){ // 実際には、同様の機構はCDNレイヤーに存在 if (Cache::has("/me")) { $user = Cache::get($user_id); return $user; } $user_id = Auth::user()->id; $user = App\User::getByID('id', $user_id); Cache::put("/me", $user, $expiry); return $user; }
このようなロジックで、以下のステップを踏んだ場合、他者に見られるべきではない情報が、第三者に漏洩します。
GET /me
のキャッシュが存在しない- ログイン済みのユーザーAが
GET /me
にアクセス /me
というキーでキャッシュを保存- 別のユーザーBが
GET /me
にアクセス - キャッシュヒットし、保存された
/me
というキーで保存されたユーザーAの情報が返る
今回の例では、サーバーのロジック内に無理矢理実装していますが、実際は、上述したように、CDNへのキャッシュについても同様の考え方が必要です。 キャッシュは静的ファイルであるが故に、認証済みかどうかをチェックすることはできません。 そのため、「キャッシュする情報」の公開範囲については、設定・実装する際に注意し、権限を持たないユーザーに対して誤って配信されることがないようにしなければなりません。
まとめ
本稿では、実装上の注意を網羅はできているわけではないですが、Webアプリケーションを実装する上で、特に見落としやすい点について、一般的にマイページと呼ぶページに付随する機能の観点から述べました。
Flatt Security Blogでは機能ごとにフォーカスしたセキュリティ記事を複数公開しています。ぜひ、あわせてご覧ください。
特に、ロジックの不備に関する脆弱性は、エンドポイント単体に対して行うツールのセキュリティ診断では検出しにくいのですが、診断を行う上で非常に重要な観点となっています。
弊社では、ツール診断だけでなく、手動による診断も行っているため、ロジックの不備等、アプリケーションの本来の仕様と実装の差異から来るであろう問題点についても検出することが可能になっています。
過去に診断を実施したが不安や課題がある、予算やスケジュールに制約がありどのように診断を進めるべきか悩んでいる等、お困り事にあわせて対応策をご提案いたしますので、まずはお気軽にお問い合わせください。
数百万円からスタートの大掛かりなものばかりを想像されるかもしれませんが、上記のデータが示すように、診断は幅広いご予算帯に応じて実施が可能です。ご興味のある方向けに下記バナーより料金に関する資料もダウンロード可能です。
また、Flatt Securityはセキュリティに関する様々な発信を行っています。 最新情報を見逃さないよう、公式Twitterのフォローをぜひお願いします!
ここまでお読みいただきありがとうございました。