パート 3

クライアント・ステートの操作

ユーザが Web アプリケーションと対話する際、間にブラウザを介しています。 ユーザがボタンをクリックする、もしくはフォームを送信する際に、ブラウザは Web サーバにリクエストを送信します。 ブラウザは攻撃者が制御可能なマシーン上で動作しているため、アプリケーション側はブラウザから送信されるデータを信頼してはいけません。

ユーザのデータを信頼しないということは、Web アプリケーションの作成を不可能にするかもしれませんが、そうではありません。 ユーザが商品を購入するためのフォームを送信する場合、そのデータを信頼するのは問題ありません。 しかし、送信したフォーム内に商品の価格を含める場合、そのデータは信頼できないでしょう。

特権の昇格

white box アカウントを管理者アカウントに変更してください。

ヒント

ヒント 1

ユーザや管理者がプロフィールを設定する editprofile.jtl ページを見てください。 もし管理者でない場合は、少し表示が異なります。 アカウントの更新にこのページを使う際、Gruyere を騙す方法がわかりますか ?

ヒント

ヒント 2

アカウントの更新にこのページを使う際、Gruyere を騙す方法を考えてください。

ノート

攻撃方法と解決方法

以下のリクエストのいずれかを発行することにより、アカウントを管理者に変更することができます:

  • http://google-gruyere.appspot.com/123/saveprofile?action=update&is_admin=True
  • http://google-gruyere.appspot.com/123/saveprofile?action=update&is_admin=True&uid=username (どんな username も管理者に変更することができます)

この URL を訪問すると、アカウントは管理者としてマークされますが、まだ Cookie はそうではありません。 一度サインアウトし、新しい Cookie を取得してください。

このバグは、サーバ側でリクエストを承認する際に、バリデーションされていないことです。 テンプレート内のコードでユーザの変更を制限する唯一の方法は、アクセスしてはならない UI の部分を隠すことです。 リクエストを受信した時点で、サーバが認証を確認するのが望ましいでしょう。

クロスサイトリクエストフォージェリ (XSRF)

前章で “ユーザが商品を購入するためのフォームを送信する場合、そのデータを信頼するのは問題ありません。” と述べたように、 ユーザが送信したフォームというのが確実な場合、それは真実です。 もし XSS から保護しているとしても、保護すべきもう一つの攻撃があります: クロスサイトリクエストフォージェリです。

ブラウザがサイトにリクエストを作成する際、リクエストがどこから来ているかに関係なく、 それは常にそのサイトのための Cookie を送信します。 その上、一般的に Web サーバは、 ユーザが意図したアクション (例えばユーザが “送信” ボタンをクリックする)によって開始されたリクエストと、 ブラウザを介さずに作成されたリクエスト (例えばページに埋め込まれた画像) を区別することができません。 したがって、いくつかのアクション (メールの削除、連絡先の変更など) を実行するリクエストを受信しても、 それはこのアクションがユーザにより意図的に開始されたのか知ることはできません - リクエストの認証 Cookie を含む。 攻撃者はこの事実を利用し、サーバをだましてユーザが意図しないアクションも実行することができます。

ノート

詳細情報

例えば、Blogger に XSRF 攻撃に対する脆弱性があると思ってください。 そして、Blogger のダッシュボードに以下の URL の “Delete Blog” ボタンがあるとします:

http://www.blogger.com/deleteblog.do?blogId=BLOGID

攻撃者の Bob は、彼の http://www.evil.example.com というページ上に以下の HTML を埋め込みます:

<img src="http://www.blogger.com/deleteblog.do?blogId=alice's-blog-id"
    style="display:none">

もし被害者 Alice が www.blogger.com にログインし、上記ページを表示すると以下が発生します:

  • 彼女のブラウザは上記の img を含むページ http://www.evil.example.com をロードします。
  • ブラウザーは画像をロードするために、 http://www.blogger.com/deleteblog.do?blogId=alice's-blog-id へのリクエストを作成します。 Alice は Blogger にログインしています - 彼女は Blogger の Cookie を持っています - ブラウザーはリクエストに Cookie を送信するでしょう。
  • Blogger は Cookie を Alice の正当なセッションの Cookie としてを承認します。 それは alice's-blog-id というブログをAlice が所有していると確認します。 そして Alice のブログを削除します。
  • Alice はまったく攻撃がわかりません。

この攻撃例では、各ユーザが自身のブログ ID を保持してため、 攻撃は特定の一人を目標とする必要がありました。 しかし、多くの場合このようなリクエストは、ユーザ固有のデータをまったく含んでいません。

XSRF にチャレンジ

ここの目標は、 Gruyere にログインしたユーザが知らない間に、そのアカウントを変更する方法を見つけます。 ユーザが自分の Web ページを訪問すると仮定してください。

black box Gruyere ユーザのスニペットを削除してください。

ヒント

ヒント

スニペットを削除するために使われる URL はなんですか ? スニペットに続く “X” の URL を見てください。

ノート

攻撃方法と解決方法

攻撃方法 は、以下のリクエストを作成するページを作成し、ユーザに訪問させます:

http://google-gruyere.appspot.com/123/deletesnippet?index=0

相手に気がつかせないため、自分の Gruyere のアイコンをこの URL に設定し、 被害者がメインページを表示した際に攻撃が実行されます。

解決方法 は、 最初に /deletesnippet は状態を変更するアクションのため、 POST リクエストで動作するように変更する必要があります。 HTML フォーム内の、 method='get'method='post' に変更します。 サーバサイドでは、 GETPOST リクエストは、 異なるハンドラーを呼び出す以外は同じように見えます。 例えば、Gruyere は Python の BaseHTTPServerGET リクエストは do_GET を呼び出し、 POST リクエストは do_POST を呼び出しています。

しかしながらPOST へ変更したとしても、それだけでは解決できていないので注意してください! (Gruyere は GET リクエストを利用しており、それはハッキングをちょっとだけ簡単にします。 POSTGET よりセキュアではありませんが、より適切です: ブラウザはアクションを成功させるために一度ならず GET リクエストを再発行します; ブラウザーはをユーザの同意をえずに POST リクエストを再発行することはありません) またユーザに対して一意で、規則性のないトークンを発行し、 それがアクションを実行する際に送り返させる必要があります。 この認可されたトークン action_token のために、 ハッシュ化した現在のタイムスタンプとこのトークンをユーザの Cookie に追加し、 状態を変更するすべての HTTP リクエストの HTTP パラメータにこのトークンを含めます。 GET ではなく POST リクエストを使う理由は、 action_token を URL のパラメータとして渡すと、 それが HTTP リファラーヘッダから漏れるかもしれません。 タイムスタンプをハッシュに含める理由は、 古いトークンを失効させることができ、それが漏れた場合のリスクを軽減します。

リクエストを処理する際、Gruyere はトークンを再生成し、 リクエストに含まれる値と比較します。 値が等しい場合にのみ、アクションを実行する必要があります。 そうでない場合は、拒否します。 トークンを生成し、確認する関数は以下のようになります:

def _GenerateXsrfToken(self, cookie):
  """Generates a timestamp and XSRF token for all state changing actions."""

  timestamp = time.time()
  return timestamp + "|" + (str(hash(cookie_secret + cookie + timestamp)))

def _VerifyXsrfToken(self, cookie, action_token):
  """Verifies an XSRF token included in a request."""

  # First, make sure that the token isn't more than a day old.
  (action_time, action_hash) = action_token.split("|",1)
  now = time.time()
  if now - 86400 > float(action_time):
    return False

  # Second, regenerate it and check that it matches the user supplied value
  hash_to_verify = str(hash(cookie_secret + cookie + action_time)
  return action_hash == hash_to_verify

おっと! この関数にはいくつかの誤りがあります。

ノート

何が足りないのでしょうか ?

時間をトークンに含めることにより、それが永久的に利用されるのを防いでいますが、 もし攻撃者がトークンを複製した場合、 24 時間以内であれば何回でもそれを再利用できます。 トークンの失効時間は、ユーザがリクエストにかかるであろう最小の時間にセットする必要があります。 このトークンも、リクエストに使われたトークンのうち 1 つが傍受されると、 それとは異なるリクエストに利用されてしまうという攻撃からは保護されません。 トークンは action_token という名前の通り、 ページの URL のような状態を変更するためのアクション毎に固有である必要があります。 _GenerateXsrfToken のより良いシグネチャは (self, cookie, action) になるでしょう。 スニペットの編集のようなとても長いアクションのために、 ページのスクリプトにより、送信する際にトークンを更新するようサーバに要求できます。 (しかし、攻撃者がその新しいトークンを読みとれないようにするため、次章の XSSI に関する情報も読んでください。)

XSRF の脆弱性が存在すると、 攻撃者は簡単に一連のアプリケーションのリクエストを実行するスクリプトを書くことができ、 ユーザがそのページを訪問するとでそのスクリプトを余儀なく実行させます。 この種の攻撃を防ぐためには、 アカウントを変更する全ての リクエストに対し、 攻撃者がスクリプト化できない、規則性のない値を含めます。 いくつかのアプリケーションフレームワークには XSRF からの保護機能がビルトインされています: それは自動的に一意のトークンをレスポンスに含め、すべての POST リクエストでそれを確認します。 その他のフレームワークも、それを利用可能な関数が提供されています。 このどちらでもない場合は、 自作 する必要があります。 これは機能しないので注意してください: GET の代わりに POST を利用するのは望ましいですが解決策ではなく、 Referer ヘッダの確認は対策としては不十分であり、 Cookie をフォームの hidden フィールドに複製するのは逆に Cookie のセキュリティを低下させます。

クロスサイトスクリプトインクルージョン (XSSI)

ブラウザはあるドメインのページが、他のドメインのページを読込むのを防ぎます。 しかし、あるドメインのページが、他のドメインのリソースを参照するのは防ぎません。 具体的には、他のドメインの画像をレンダリングしたり、他のドメインのスクリプトを実行します。 読込んだスクリプト自体にはセキュリティコンテキストがありません。 それを読込んだページのセキュリティコンテキストで動作します。 例えば、 www.evil.example.comwww.google.com 上にあるスクリプトを読込んだ場合、 スクリプトは google のコンテキストではなく、 evil のコンテキストで実行されます。 これにより、そのスクリプト内に含まれるすべてのデータが “漏れてしまいます” 。

XSSI にチャレンジ

white box XSSI を利用し、他のユーザのプライベートスニペットを読む方法を見つけてください。

つまり、他の Web サイト上にページを作成し、プライベートなスニペットを読むことができるようにします。 (Web サイトとして公開する必要はありません: ホームディレクトリ (デスクトップとか) に .html ファイルを作成し、ブラウザーで開いてください。)

ヒント

ヒント 1

HTML ファイルに以下を追加すると、他のドメインでスクリプトが実行できるようになります:

<SCRIPT src="http://google-gruyere.appspot.com/123/..."></SCRIPT>

Gruyere にはどんなスクリプトがありますか ?

ヒント

ヒント 2

feed.jtl はスクリプトです。 このスクリプトから、どのようにプライベートなスニペットを取得できますか ?

ノート

攻撃方法と解決方法

攻撃方法 は、HTML ファイルを以下のようにします:

<script>
function _feed(s) {
  alert("Your private snippet is: " + s['private_snippet']);
}
</script>
<script src="http://google-gruyere.appspot.com/123/feed.jtl"></script>

feed.jtl 内のスクリプトが実行されると、 それは攻撃者の Web ページのコンテキストとして実行され、 読込んだ Web サイトに対し、ほしいデータをすべて手に入れることが可能な _feed 関数を利用します。

引数のない関数の呼び出しをなくせば解決すると思うかもしれません。 そうすると、スクリプトがインクルージョンによって実行される際、レスポンスが評価され、それは破棄されます。 Javascript がデフォルトのコンストラクターを再定義することを許可するので、それは動作しません。 オブジェクトが再評価される際に、ホスティングページのコンストラクタが呼び出され、 ほしいデータをすべて手に入れることができます。

解決方法 は、いくつかの変更を加えます。 この変更のいずれかにより、現段階で発生しえる攻撃を防ぐことができますが、 いくつかのレイヤーへの防御 (“`多層防御`_”) (Wikipedia: 多層防御) を追加すると、 その防御のうち 1 つに誤りがあった場合や、将来のブラウザの脆弱性から保護されるでしょう。 最初に、自身のページにのみ機密情報を含む JSON レスポンスを許可するために、前章で確認した XSRF トークンを利用します。 2 つめに、JSON レスポンスページは POST リクエストのみ処理するようにし、 スクリプトタグによりロードされて利用されるのを防ぎます。 3 つめに、スクリプトが実行可能でないことを確実にします。 これを実現するための一般的な方法は、 ])}while(1);</x> のように実行不可能にするためのプリフィックスを追加します。 同じドメインで動作するスクリプトはレスポンスの内容を読み込むことが可能なため、 プリフィックスを取り除くことができますが、他のドメインで動作しているスクリプトはできません。

NOTE: それでもスクリプトを実行不可能にできているかは微妙です。 新しいスクリプトの機能や新しい言語が登場した場合、 将来的にスクリプトが実行可能になってしまうこともありえます。 一部の人が、スクリプトを /**/ で囲みコメントアウトすれば保護できると言いますが、 そんな単純ではありません。 (ヒント: 誰かがスニペットに */ を含めた場合はどうなりますか ?)

もっと XSSI について 調べてください。 JSON のバリエーションに JSONP というのがありますが、 これは 設計上 スクリプトを混入を許可しているので利用は避けるべきです。 また、HTML ファイル内のスクリプトを解析する E4X (Ecmascript for XML) (Wikipedia: E4X) というのがあります。 驚くことに、E4X への攻撃を保護する方法は、ファイルに </x> のような無効な XML を含めることです。