読者です 読者をやめる 読者になる 読者になる

シアトル生活はじめました

20年以上すんだ東海岸から西海岸に引っ越してきました。MicrosoftのUniversal Storeで働いてます。

RESTFulなAPIとCSRFとその対策

テクノロジー

Cross-Site Request Forgery(クロスサイトリクエストフォジェリー)って何?

頭文字をとって「CSRF」ですが、出来るだけ平たく説明すると

「悪いヤツが作ったサイトから読み込んだHTMLやらスクリプトが、勝手に別のサイトにHTTP POSTのリクエストを送信して、知らない間にそのサイトにある自分のデータなどを変更される」

といった感じになるかな。

データの中には重要なデータもあるでしょう。Amazonで欲しい物リストがあったとして、それが全部勝手に「購入」されたら困りますよね。銀行の口座から別の口座にお金が入金されても困ります。(もちろん、Amazonや銀行のサイトなどではCSRF対策がしっかりと施されているでしょうから、大丈夫!・・っであることを祈る)

Cross-site とは二つのウェブサイトを跨いでること。サイトのひとつは当然「悪いヤツのサイト」でもうひとつは「ちゃんとしたサイト」になる。後者は銀行だとかSNSだとか、なんらかのログインが必要なサイト(ログインが必要でないサイトは攻撃してもあんまり得るもんがない)【1. Ambient Authority】

Request とはリクエスト(要求)。つまり「これちょうだい」もしくは「これやって」というユーザーが発する(べき)頼み事。

Forgery とは偽造のこと。ユーザーではなく別の悪いヤツが勝手に作るもの。CSRFではリクエストが偽造されます。

Cross-Site Request Forgery とは「サイトを跨いで勝手に偽造したリクエストを発信してしまう」タイプの脆弱性のことです。

Same Origin Policy (同一生成元ポリシー)を理解しよう

私もいまいちボンヤリとしか理解できてなかったんだけど、どうやら原因はウェブのセキュリティーの基本であるSame Origin Policy (SOP)の理解が不十分だったことにあるようです。

  • Cross-site での読み込み: 拒否される
  • Cross-site での書き込み :限定的に許可される
  • Cross-site での実行・埋め込み: 許可される

これがそのポリシーです。詳しく見ていきましょう。

Cross-siteな読み込み

最初の「Cross-siteでの読み込みは拒否される」ですが、これはWebセキュリティーの大原則です。例えば私がスクリプトを書いて、私のサーバーに置いて、あなたのブラウザーにそのスクリプトを読み込ませたとします。私のスクリプトはあなたの銀行サイトの入金履歴をHTTP GETで取り寄せさせて、さらにそのレスポンスの内容(つまり入金履歴)を読めるとしたらあなたはきっと困るでしょう?ダダ漏れというやつです。

なので、あるサイトから読み込んだスクリプトがサイトを跨いで別のサイトから送られてきた内容は読めないようになっているのです。

Cross-siteな書き込み

次に「Cross-site での書き込みは限定的に許可される」ですが、読み込みがダメなのに書き込みはOKってのはなんか変な感じがしますよね。

でも実際には、次のようなことが出来ます

  • 別のサイトに移動する
  • 別のサイトに<form>要素でPOSTする
  • 別のサイトにXMLHttpRequestを使ってPOSTする

などです。あるサイトから別のサイトへ働きかけることが可能なのでサイトとサイトを繋いで自動化させたり連携させたり出来て便利です。とくに、別のサイトに移動する(ブラウザーの内容を別のサイトの内容に書き換える)が出来なかったらインターネットはひどくつまらないものになっていたでしょうね。

そして、これがCSRF攻撃を可能にしている原因でもあるのです。(その方法は後述)

Cross-siteな実行・埋め込み

 最後に「Cross-siteでの実行は許可される」ですが、これは具体的には

  • <script src>タグで別のサイトからスクリプトをロードする(Google Analytics!)
  • <img src>タグで別のサイトの画像を表示する
  • <ifram src>タグで別のサイトのページを丸っと表示する
  • <link rel="stylesheet" href>タグで別のサイトのCSSを読み込む

などがあります。これらは「実行・埋め込み」もしくは「利用」はできますが、実は目をつぶって実行することのみ許可されていて、例えば別のサイトのCSSのクラスの内容を読むことはできないのです。別のサイトのイメージをユーザーに表示はできても、そのイメージのピクセルの色などは読み込めないのです。

まぁこれはCSRFの理解にはあまり関係ないのでこのぐらいで。

CSRFの理解にもっとも重要なSOPのルールは2番目のルール、特に「Cross-siteでのHTTP POSTは許可されている」ということです。

CSRF攻撃の流れ

CSRFは「Confused Deputy Attack」というカテゴリーに分類されています。「混乱した代理人攻撃」とでも言いますか。この場合の代理人はブラウザーを差し、ブラウザーが代理しているのはユーザーです。代理人を攻撃しているわけですが、実際にセキュリティーのダメージが発生するのは代理人が会話をするサーバーです。

その流れを見ていきましょう。

  1. ユーザーがサイトB(銀行)にログインする。(そして本来意図していた作業をする。残高をチェックするとか?)
  2. ユーザーが同じブラウザーの別のタブでメールをチェックする
  3. メールの中に「浮気現場を押さえた爆笑の写真!」とか、いかにもクリックしたくなるような内容(笑)のリンクが張ったメールがある。
  4. ユーザーがそれをクリックする(しちゃうよねぇ・・)
  5. リンク先のページがサイトAから読み込まれる。まぁ、お目当ての写真とかあったりするのかな。
  6. ただ、この時に、ユーザーが気付かない状態でサイトAからスクリプトがロードされ、そのスクリプトはサイトBに対してなんらかのHTTP POSTリクエストを送信する。このPOSTリクエストは、例えば攻撃者があらかじめ調べていた「送金フォーム」だったりします。
  7. サーバーはこのPOSTリクエストを受け取るわけですが、この瞬間にサーバーはこのリクエストが「ユーザーが意図した」ものなのか「どこかのサイトからロードされたスクリプトが送信したもの」なのか、判断のしようがないのです。判断できませんが、何もしないわけにはいきません。なにしろ「ユーザーが意図した」ものだったら指示通りにしないと銀行を使ってもらえなくなってしまいます。

ここでCSRF攻撃は完了しました。

あれ、データは漏れないの?っと思うかもしれませんが、CSRF攻撃ではデータは直接的には漏れません。SOPのおかげで、かりにPOSTがなんらかのデータをブラウザに返したとしても、ブラウザーはサイトAからロードされたスクリプトにサイトBから返されたデータを読ませるようなことはしないからです。(別のJSON配列ハイジャックという脆弱性を組み合わせて読むことはできましたが、それは最近のブラウザーでは対策が取られました)

こんな感じで、ブラウザーが「ユーザーからの指示」と「別のサイトからのスクリプトからの指示」を混同してしまう現象を利用してサーバーに、ユーザーの意図しない指令を送る、というのがCSRFなのです。

CSRF対策

さて、CSRFの対策ですが、ネットにいろいろ出てます。その中でもイチオシなのが「予測することが難しいトークンを使う方法です」

こういう流れで攻撃を食い止めます。

  1. ブラウザーがページを要求する。
  2. サーバーはトークンを生成する(できればリクエストごとに生成したいけど、セッションごとに生成するのでも構いません。ただし、「予測不可能」であることが条件)
  3. サーバーはページを返す際に、トークンも含めて返す。トークンの送信はクッキーでもいいし、formのフィールドでもいいでしょう。(重要:悪いヤツのサイトからロードされたスクリプトはこのトークンの値を読むことができません。なぜかって先に解説したSOPのルール「Cross-siteな読み込みは拒否される」が有効だから)
  4. ブラウザーがユーザーにページを表示する。
  5. ユーザーがページに必要な項目を入力し、「送信」する。
  6. ブラウザーは先ほどのトークンも含めて、サーバーにPOSTリクエストを送る。
  7. サーバーはトークンを見て「はたしてこれはサーバーが発行したものなのか?」という判定をする。この場合は「Yes」なのでリクエストを受け取って要求された通りに動作する。

一方、リクエストが悪いヤツの用意したスクリプトから来た場合は、そのリクエストにトークンは含まれていません。あったとしても、それはデタラメの値です(「Cross-siteな読み込みは拒否される」でしたよね?悪いヤツのスクリプトは別のサイト出身のCookieもDOMも読めません)。サーバーは、そのリクエストはユーザーが意図したものではない、っと判定してリクエストに応答することを拒否します。

これが、今あるCSRF対策の中でもっても有効だとされています。

RESTFulなAPIの場合はどうする?

さて、トークンを利用したCSRF対策は、伝統的なHTMLページを返すサイトの場合は実装するのはわりと簡単です。サーバー側スクリプトもそういうトークンを生成してくれるライブラリーを用意してくれてます(ASP.NET MVCPHPなど)

だけど、RESTFFulなAPIって

  • そもそも設計思想がステートレス(つまりセッション毎のトークンを保存したり、ページがPOSTバックされた時までにトークンを保持するメモリーなどが無い)
  • RESTRulなAPIが返すデータはJSONXMLであってHTMLではない

これらの理由でトークンを使ったCSRF対策の実装はやりにくいのが現状です。

それでもなんとかしてこの方法でCSRF対策を取るのがもっとも効果的であるのは言うまでもありません。この方法についてはまた別の機会に。

他の方法はない?

トークンを使った方法以外にCSRF対策をRESTFulなAPIに施すことはできないでしょうか?一つの方法としてCORSとの組み合わせて「リクエストを受け取った段階でOriginヘッダーをチェックする」というのがあります。

CORSってなに?

Cross-Origin Resource Sharing (CORS)

これはCSRFとは直接関係ないんですが、先ほどのSOPのセキュリティーと関連していて、RESTFulなAPIを実装する際にはおそらくこちらも実装した方がいい仕組みです。というか、「APIをホストするサイト」と「APIを使うコードをホストするサイト」が同じドメインにあるのでない限り、CORSを使わないとアプリケーションを稼働できません。

SOPの原則である「Cross-siteな読み込みは拒否」というのがありましたが、これって実はRESTFulなAPIを使ったアプリケーションを構築するにはむしろ壁になるのです。例えば、RESTFulなステキなAPIを作って、あるサーバーAに置いたとします。そして、HTML5JavaScriptだけ(クライエント技術のみ)を利用してフロントエンドを作ったとします。このフロントエンドは別のサーバーBに置いたとします。前提としてこの二つのサーバーはそれぞれ独立したドメインだとします(http://myapi.comhttp://myFrontEndなど)

そうすると、FrontEndのサイトからロードされたスクリプトが、APIのサイトに対してHTTP GETでデータを要求し、それが成功したとしてもFrontEnd出身のスクリプトAPIサイトから返ってきたデータを「読めない」のです。なぜかというとSOPの「Cross-siteな読み込みは拒否」というルールをブラウザーがきっちり守るからです。ブラウザーはAのスクリプトにBからのコンテンツを読ませないようにするのです。

 の制限を意図的に部分的に取り除くのがCORSです。

CORSとは

Cross-origin 生成元を跨った

Resource 資源を

Sharing 共有する

ための仕組みなのです。SOPがデフォルトで設定している制限をCORSで特別に緩めるわけです。

CORSにおける信頼できるOrigin

CORSの説明を細かくすると長くなるので、CSRFとのカラミで重要な部分だけ説明すると、CORSとはつまり「サーバーが、このサイト(origin)から来たスクリプトは信用してあげてもいいよ」とブラウザーに教える仕組みであり、信用されたスクリプトは自由にそのサーバーが返したDOMなどのコンテンツを読めるようにブラウザーが取り計らってくれることをいいます。

ということはサーバー側には「信用してもいいサイト(origin)」の情報がどこかに保存してあることになりますね(このOriginは一つだけでなく、複数、場合によっては誰でも、指定できます。ここでは複数の限定されたOriginとします。)

このリストをWhitelist(ホワイトリスト)と呼びます(ブラックリストの逆。つまり信用していいやつのリスト)

RESTFulなAPIでOriginを確認してCSRF対策

CORSで利用されるOriginのリストですが、APIがPOSTのリクエストを受け取った時に、リクエストのヘッダーのうちOriginをホワイトリストと照らし合わせて、もし信用出来ると判断したらリクエストに従って実行する。もしホワイトリストに含まれていないならば無視する、そうすることによって悪いヤツのサイト(Origin)から読み込まれたスクリプトが送信したリクエストを拒否することが出来るようになります。

Wikipedia にはHTTPリファラーを照合する方法も紹介されていますが、これはやや弱いとされています。その代わりにOriginを使った方がより強力だとされています。ブラウザーはあるサイト(A)からロードされたスクリプトが、別のサイト(B)の上にあるリソース(RESTなAPIが返すJSONデータなど)に取得しようとすると、自動的にOrigin=Aといった形でサーバーに要求元を知らせてくれます。サーバーは(A)を信用してるなら続けて実行を続ける。もし(A)のことを知らないのなら即座に処理を中止する。

Cross-siteな要求時にブラウザーが送ってくれるOriginを利用しよう、っというわけです。(注意:APIとフロントエンドを同じドメインの元に置いたとしたら、それはもはやCross-siteではないので、ブラウザーはOriginを送ってきません。逆に言えばOriginがあればCross-site、なかったらSame-siteと判定することが出来ます)

まとめ

CSRF対策としてはトークンを使った方法がもっとも効果的だとされていますが、RESTFulなAPIを実装しているサーバーに対してのCSRF対策としてはやや不向きな点があります(おそらくOAuthとの組み合わせがもっても効果的かと思いますが、私個人はそれを実装した経験がまだないので、その点に関してはなにも言えません)。

その点、Originをチェックする方法は実装するコストも低く抑えられ、ある一定の効果を持つと考えられます。少なくともダダ漏れ状態のAPIを晒しておくよりは良いでしょう。

追記

通信はSSL証明書を使ったTLSを実装することはいまやWebのセキュリティーでは当然だとされています。トークンを送る際にトランスポートレイヤーでトークンを抜かれてしまったらトークンを使った対策は破たんしてしまいます。SSLは必ず設定しましょう。

 

【1. Ambient Authority】間接的な権限の一種。例えば新聞記者に与えられる記者証のようなものと考えてもいいかな。記者(人間)が「私は権限がある!」という代わりに、記者証をチラっと見せるとき、実際は別の人間が成りすましていたとしても何も聞かずにどうぞそうずと部屋に入れてしまう、そういう感じのこと。