Flatt Security Blog

株式会社Flatt Securityの公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

株式会社Flatt Securityの公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

Webサービスの認可制御の不備によって起こる仕様の脆弱性と対策

はじめに

こんにちは。株式会社Flatt Securityセキュリティエンジニアの村上 @0x003f です。

本稿では、Webアプリケーション上で実装される「認可制御」で気をつけなければいけない「仕様の中で起きうる脆弱性」とその対策について解説していきます。

どういったアプリケーションであれ認可制御は何らかの形で行われているでしょう。今回は認可制御について不適切な設計・実装を行うとどういった脆弱性が起こりうるのかということを解説できればと思っています。

昨今は特に機密性の高いデータを扱うWebサービスも増えており、その対策の重要性は増しています。Flatt Securityは専門家の視点でセキュリティリスクを洗い出す診断サービスを提供しておりますので、興味のある方は是非あわせて事例インタビューもご覧ください。

想定アプリケーション

まずは本稿で解説する「認可制御」について、どういったWebアプリケーションを想定しているかという前提を説明します。 今回想定しているのは以下のような仕様の「ユーザー権限と操作」があるものとします。

  1. マルチテナント(※)のToBサービス
  2. ユーザーの種別に「管理者ユーザー」「一般ユーザー」がある
    • 「管理者ユーザーには各種機能に対する『読み取り、書き込み、削除、更新』の権限があり、一般ユーザーは『読み取り』の権限がある」というような差分があるものとする
  3. 管理者ユーザーは新規ユーザー作成機能等の特権機能を持っている

※マルチテナント...複数のユーザー(企業)の参照するリソースサーバーが同じシステムのこと。

セキュリティ観点と対策

認可制御が正しく行われているのか、というのは同一のアプリケーションにおいても機能ごとに異なります。ここでは前述の想定アプリケーションの仕様を元に認可制御が必要な機能について、どのような観点で確認を行う必要があるのかと、具体的な認可制御の不備がある実装のパターンをいくつか紹介します。その後、認可制御はどういった点に気をつけて実装を行えばよいかを対策として示します。

観点1: 権限を持たない機能の操作が可能

こちらはそのままで、あるユーザーの権限状態が正しくチェックされておらず本来行えない操作ができるという状態です。

本稿の例は一般ユーザーは読み取りしかできない仕様のはずなのに、実際には書き込みや削除ができてしまうというものになります。また本稿の例にはありませんが、アプリケーションによっては「一般ユーザー」であっても「本人からのみ操作が可能(プロフィール情報など)」というものは考えられ、そのようなものを侵害する場合もこれに当たると考えます。

このような脆弱性を生む原因として、どんなパターンがあるかを紹介します。

GUIから機能を隠しているだけになっている

例えば「管理者ユーザーには作成ボタンが表示されるが、一般ユーザーにはボタンが表示されない」といったようなGUIによる制御は権限の実装されたアプリケーションにはよくあるかと思います。

ここで、「GUIから消しているから大丈夫だろう」と、サーバー側では特にユーザーがどの権限を持っているかをチェックせずに処理を行うように実装していると簡単に権限外の機能を実行されることになります。

上記の例はわかりやすい管理者ユーザーと一般ユーザーの間の機能差分の例です。しかし、わかりづらいケースであっても攻撃者がある程度Webアプリケーションに精通していれば、ブラウザの開発者ツールから通信の履歴を取得し、アーキテクチャ等からAPIの構造を推測してリクエストを生成し実行される可能性があります。

操作元ユーザーのIDをリクエストに含んでしまっている

これはリクエスト内のユーザーを指定できる箇所にユーザーID等のユーザーを特定する情報を含んでしまい、かつその値が容易に推測可能なパターンです。この仕様を悪用されると攻撃者を操作元ユーザーと認識してしまいます。

非常に単純な例としては以下のようなリクエストになります。

POST /announcements HTTP/1.1
()

{
  "user_id": 12345678,
  "text": "hello"
}

これほど単純な例はあまり見受けられませんが、例えばヘッダー部にユーザーを特定するIDが含まれていてかつそれが推測可能な値である場合等も同様です。 IDを推測する方法としては「連番で生成されているので順番に利用する」「ユーザー一覧ページからIDが取得できる」等いくつかのパターンがあります。

操作権限に関する情報をリクエストに含んでしまっている

例えば上記のユーザーIDの例と同様に権限に関する情報をリクエスト内に含んでいる場合です。 ユーザーの特定はセッションやAPIキーのような形で他者から推測が不可能な形で管理されているが、操作権限に関する情報がリクエストに含まれてしまっているパターンです。

例えば以下のようなリクエストになります。

POST /announcements HTTP/1.1
()

{
  "role_id": 0,
  "text": "hello"
}

このリクエスト例では role_id が指定されています。アプリケーション側がこの role_id をみて権限をチェックするようになっていた場合、この role_id の値を変更することで本来操作不可能なユーザーが操作可能になってしまう可能性があります。

観点2: 他テナントのデータの操作が可能

先述の通り、今回題材とするサービスは複数のユーザー(企業)の参照するリソースサーバーが同じでありマルチテナントのシステムになっています。

このようなシステムの場合は「ある企業Aのユーザーが別の企業Bのデータを操作できないか」ということを考える必要があります。 当然ここでも認可制御を正しく行えていない場合には情報の取得や変更が行われ、ToBサービスであれば機密情報の漏洩などの大きな問題に発展することが考えられます。

送信元ユーザーの所属テナントをチェックしていない

ユーザーが管理者権限であるかどうか等のチェックは行えているが、「そのテナントの所属者であるかどうか」をチェックできていない場合にはテナントを超えた操作が行えてしまいます。

例えば以下のような企業情報の入力エンドポイントを考えます。

# 会社ID 123456の情報を更新する
PATCH /companies/123456

また、このエンドポイントを利用できるのは管理者権限のユーザーのみとします。

このとき、この会社ID123456に所属する管理者ユーザーAが 123456654321に変更してみるという不正な操作が考えられます。このとき正しく所属テナントのIDチェックを行えていなければ変更が可能になります。

コードにしてみると以下のような状態です。

user = User.get(user_id: current_user.id)
# ユーザーが管理者かどうかはチェックしている
return [400, 'you are not admin'] unless user.admin?

company = Comapny.get(company_id: parameter_comapny_id)
# 本当はここでuserがcomapnyに所属しているかをチェックする必要がある
comapny.update(parameter_comapny_data)
return [200, 'comapny data update!']

この例は非常に単純ですが、所有や所属の関係が複雑になってくると見落とす可能性が高くなります。

観点3: 権限の昇格が可能

ある機能を利用するときに、「管理者ユーザーかどうかの認可制御」が正しく行われていても、「一般ユーザーが管理者ユーザーとして振る舞える」ような状態になるのであれば意味がありません。

このような「権限の昇格」が行われる原因にどのようなパターンがあるかを紹介します。

一般ユーザーの権限情報を変更可能

こちらはそのまま、一般ユーザーが自分の権限情報を書き換えて昇格してしまうというものです。

例えば権限についての情報を先程も出てきたrole_idで管理しているとし、簡単なサンプルのエンドポイントを考えてみます。 あるユーザーが自分のユーザー情報を更新しようとして送信されたリクエストが以下のようなものだったとします。

PUT /users/123456
(略)

{
  "role_id": 0,
  "name": "フラット太郎",
  ....
  "tel": "000.........."
}

このリクエストにrole_idという権限に関する情報が含まれていることがわかります。 またこの role_id0 になっているので、他の数字に変えれば他の指定した数字のrole_idに変更できそうなことが推測できます。

このようなリクエストを用いて管理者権限のユーザーのrole_idを指定することができれば自由に権限の昇格が行えそうです。 これはデータ更新に関する権限の管理を「このユーザーの所有するデータかどうか」だけ行っている場合に起こりえます。

一般ユーザーが管理者ユーザーを作成可能

一般ユーザー自身が管理者ユーザーの権限を得られなくとも、一般ユーザーが管理者ユーザーを作成できて認証が可能であればそれは立派な権限昇格です。

これは前述の観点1: 権限を持たない機能の操作が可能のような実装がユーザー登録機能等の新規のユーザーを作成する部分に行われていれば起こりえます。

一般ユーザーが管理者ユーザーの認証情報を変更可能

自分の権限情報を書き換えられない、新規で管理者ユーザーを作成できない場合でもまだ権限の昇格を行えるパターンが存在します。

例えば「管理者ユーザーのパスワードを自分が知っているパスワードに変更してしまう」というような、既存の管理者ユーザーの認証情報を自分が知っているものに書き換えられる状態です。 こちらも観点1: 権限を持たない機能の操作が可能に挙げたような実装がそのような機能に行われている場合に起こりえます。

対策

ここまで様々なパターンの認可制御の不備によって起こりうる脆弱性を紹介しました。 このような脆弱性が起きないようにするためには基本的には「サーバー側で値の正当性をチェックする」というものになります。 これまで挙げた観点に対してどのような目線で実装の確認をしていくとよいかについて説明します。

「GUI上で表示しない」は認可制御にならないと理解する

観点1にも出てきましたが、GUIから導線を隠すのは単にユーザビリティの問題であってシステムとしての認可制御にはなりません。(「間違えてボタンを押すのを防ぐもの」と考えてもらうのがいいでしょう)

必ずサーバー側でユーザーごとに利用できる機能の検証等を行ってください。

クライアントの生成したリクエストの値を信用しない

クライアントが生成して送信する値はどのような値でも必ず改ざんされる可能性があります。

これは特殊なツールなどを用いなくてもリクエストの形式さえわかってしまえばcurlコマンドを利用したり、ブラウザの開発者ツールを使うことで容易に送信が可能です。 このように送信に際してサービス上の機能を使うとは限らないので「フォームでバリデーションを行う」も認可制御や値のコントロールにはなりません。

どのような値であれ、最終的にクライアントから送信されてきた値が許容できる値かどうかや指定されたパラメータ自体がその機能で受け付けてよいものなのか等はサーバー側でチェックを行ってください。

署名付きのリクエストでも安全ではない

クライアントから送信する値が信用できないのであれば、PKIを利用した署名をクライアントからの送信リクエストに付与して送信すればいいのでは?と思われるかもしれません。しかし、Webブラウザは動作に必要なリソースがブラウザ上で読み込めることが前提になっているため、署名に利用した秘密鍵は必ずブラウザを用いてアクセスしているユーザーに漏洩します。また、ソースコードが難読化されているとしても、解析にコストはかかりますが署名のアルゴリズムは復元され改ざんが可能になってしまいます。なので、やはりクライアント側の対策は根本対策になりえません。

リソース同士の関連付けを適切に行い、チェックする

URIのパスに含まれたリソースのIDやクライアントから送信されたIDは必ず送信元のユーザーに操作権限のあるリソースかどうかを必ずサーバーで検証してください。

今回のアプリケーション例で言えば、「あるユーザーの送信したID等に関する情報が本当にそのユーザーと紐付いているかどうか」もサーバー側でチェックしてください。 例えば観点2で出てきたソースコードを改善するとしたら以下のようになるでしょう。

Before

user = User.get(user_id: current_user.id)
# ユーザーが管理者かどうかはチェックしている
return [400, 'you are not admin'] unless user.admin?

company = Comapny.get(company_id: parameter_comapny_id)
# 本当はここでuserがcomapnyに所属しているかをチェックする必要がある
comapny.update(parameter_comapny_data)
return [200, 'comapny data update!']

After

# ユーザーが管理者かどうかはチェックしている
return [400, 'you are not admin.'] unless user.admin?

company = Comapny.get(company_id: parameter_comapny_id)

# ユーザーの持っているcompany_idと会社のidが一致しているかを調べる
return [400, 'company id is wrong.'] unless user.comapny_id == comapny.id

comapny.update(parameter_comapny_data)
return [200, 'comapny data update!']

終わりに

本稿では、Webアプリケーション上で実装される「認可制御」に関するセキュリティ観点について、具体例を交えながら解説しました。今回紹介した観点をあらためてまとめると以下のものになります。

  • 同レベルであっても権限のないデータ操作ができないか
  • 他テナントのデータ操作ができないか
  • 権限昇格や異なる権限のデータ操作ができないか

認可制御の不備は弊社の提供する診断サービスでも比較的多く報告されている脆弱性になっています。

認可制御に限らず、アプリケーションの仕様やビジネスロジックに照らし合わせなければ発見できない脆弱性を洗い出すため、Flatt Securityセキュリティエンジニアの手動検査とツールを組み合わせたセキュリティ診断サービスを提供しています。

過去に診断を実施したが不安や課題がある、予算やスケジュールに制約がありどのように診断を進めるべきか悩んでいる等、お困り事にあわせて対応策をご提案いたしますので、まずはお気軽にお問い合わせください。

数百万円からスタートの大掛かりなものばかりを想像されるかもしれませんが、上記のデータが示すように、診断は幅広いご予算帯に応じて実施が可能です。ご興味のある方向けに下記バナーより料金に関する資料もダウンロード可能です。

また、Flatt Securityはセキュリティに関する様々な発信を行っています。 最新情報を見逃さないよう、公式Twitterのフォローをぜひお願いします!

twitter.com

ここまでお読みいただきありがとうございました。