開発者のための正しいCSRF対策著者: 金床 <anvil@jumperz.net>http://www.jumperz.net/ ■はじめにウェブアプリケーション開発者の立場から見たCSRF対策について、さまざまな情報が入り乱れている。筆者が2006年3月の時点において国内のウェブサ イトやコンピュータ書籍・雑誌などでCSRF対策について書かれている記事を調べた結果、おどろくべきことに、そのほとんどが誤りを含んでいたり、現実的 には使用できない方法を紹介したりしていた。そこで本稿ではウェブアプリケーション開発者にとっての本当に正しいCSRF対策についてまとめることとす る。また、採用すべきでないCSRF対策とその理由も合わせて紹介する。■あらゆる機能がターゲットとなりうるウェブアプリケーションの持つ全ての機能がCSRF攻撃の対象となりうる。まずこのことを認識しておく必要がある。AmazonのようなショッピングサイトやGoogleのような検索サイト、そしてはてなダイアリーやMixiのような日記やブログを書くサイトなど、 ウェブアプリケーションにはさまざまなものが存在する。そしてそれぞれのアプリケーションは細かい「機能」の積み重ねによって成り立っている。 例えばショッピングサイトは、以下のような機能の集まりである。 現実のケースとしてCSRF攻撃の対象になるのは「商品をカートに入れる」などの「攻撃者にとってうまみのある機能」であることが多いが、理論的にはアプ リケーションに存在する機能は全てCSRF攻撃の対象となりうる。CSRF対策を考える上でまず行うべきことは「アプリケーションのうち、どの機能につい てCSRF対策を行うか」を決定することである。全ての機能について対策を施してもよいし、重要と思われるいくつかの機能だけに絞って対策を行うという選 択肢もあるだろう。 「データベースを更新する機能のみCSRF対策をすればよい」のような考えは正しくない。どの機能について対策を行うかは、ウェブアプリケーションの運営 者や開発者が決めるべき事である。 ■ひとつの機能は2画面で構成されるそれがどんな機能であれ、ひとつの機能は基本的には2画面で構成される。CSRF対策を考える上で、このことをしっかり認識しておく必要がある。例としてまず「ログインする」という機能について考えてみる。ユーザはログインしたいサイトのトップページなどにアクセスする。するとブラウザにはユーザ IDとパスワードの入力欄を持つ画面が表示される。これが「画面1」である。次にユーザはユーザIDとパスワードを入力し、「ログイン」のボタンをクリッ クする。するとブラウザには「ようこそ金床さん。」のようにログインに成功したことを示す画面が表示される。これが「画面2」である。 2つめの例として「日記を書く」という機能について考えてみる。ユーザは日記を書くためのページへアクセスする。するとブラウザには日記を記入するための テキスト入力欄を持つ画面が表示される。これが「画面1」である。ユーザはテキスト入力欄に日記を書き、「書き込む」というボタンをクリックする。すると ブラウザには「日記を書き込みました」などの画面が表示される。これが「画面2」である。 3つめの例として検索エンジンを使った「検索する」という機能について考えてみる。ユーザはGoogleやYahoo!などのサイトのトップページにアク セスする。するとブラウザにはキーワード入力欄を持つ画面が表示される。これが「画面1」である。次にユーザは検索したいキーワードを入力し、検索ボタン をクリックする。するとブラウザには検索結果が表示される。これが「画面2」である。 最後の例として「ログアウトする」という機能について考えてみる。ユーザは既にウェブアプリケーションにログイン済みである。そのため、ブラウザに表示さ れている画面の隅には「ログアウトする」というリンクやボタンがある。この画面が「画面1」である。ユーザが「ログアウトする」をクリックすると、「ご利 用ありがとうございました」などの画面が表示される。これが「画面2」である。 HTTPレベルで考えると、まず画面1を表示する際に一度リクエストとレスポンスのやりとりが発生する。そして画面2を表示する際に、さらに一度リクエス トとレスポンスがやりとりされる。以下の説明ではこれらをそれぞれ と呼ぶことにする。 サーバー側に存在するプログラムは、リクエスト1を受信すると何かしらの処理を行い、レスポンス1を送信する。また同様に、リクエスト2とレスポンス2の 間にも処理を行う。これらの処理をそれぞれ と呼ぶことにする。また、レスポンス1、レスポンス2によってブラウザに表示される画面をそれぞれ と呼ぶことにする。 ■「CSRF以前の問題」については考えないCSRF対策を考える上で、非常識的な「CSRF以前の問題」が存在すると考えていては話が先に進まない。「CSRF以前の問題」とは次のようなものだ。このような仮定の下ではCSRF対策を議論する意味がない。以下の説明ではウェブアプリケーションを取り巻く状況はごく常識的なものであるとする。 ■CSSXSS脆弱性を無視しないCSSXSS脆弱性はIE(InternetExplorer)に存在するバグである。これを悪用すると攻撃者が仕掛けた罠のページにレスポンス1のボ ディ部分の内容が読み込まれ、hiddenフィールドに格納されたワンタイムトークンなどの値が攻撃者の手に落ちることがある。これは深刻なバグであるに も関わらずパッチがリリースされておらず、2006年3月の時点で既に数ヶ月もの間脆弱なままの状態が続いている。「いちいちウェブブラウザのバグを気にしていてはCSRF対策など行うことができない。これはいわゆる(上の項で説明した)『CSRF以前の問題』なので はないか」という意見はもっともだ。しかしIEは9割近いシェアを持っているため、このバグを無視したCSRF対策を行っても、実際には1割の利用者しか 保護できないということになる。あなたはウェブサイトの利用者に対して「私たちのサイトではCSRF対策を行っています。しかしIEのバグについては関知 しません。その結果としてIEの利用者に対してはCSRF攻撃が可能です」などと説明できるだろうか。 せっかくCSRF対策を行おうとしているのだから、利用者全体に対して安全を提供できるような方法を採るのがよいだろう。本稿ではこのような考えの元、 CSSXSS脆弱性も考慮に入れてCSRF対策を行う。 ■正しいCSRF対策それでは、筆者が安全だと考える具体的なCSRF対策について説明する。これらの方法のうち、どれかひとつだけを採用すればよい。ここではCookieを 利用したセッション管理を行うアプリケーションを対象とする。ここで注意してほしいのは、ユーザIDやパスワードを使ったいわゆる「ログイン」機能がある かないかは、あまり重要ではないということだ。Cookieでセッション管理が行われているかぎり、CSRF対策は「ログイン」という概念の存在に左右さ れない。ちなみに、hiddenフィールドにセッションIDを格納し全ての画面遷移にPOSTを用いるアプリケーションには、CSRF攻撃は通用しない。リクエス ト1がPOSTであることからCSSXSS脆弱性の影響も受けない。 ■正しい対策その1: ワンタイムトークンを正しく使用する方法まずひとつめはワンタイムトークンを使う方法である。ここで題に「正しく使用する方法」と付けたのには理由がある。ひとくちに「ワンタイムトークンを使 う」と言っても開発者によって実装方法がまちまちであり、場合によってはCSRF対策とならないワンタイムトークンの使い方をしてしまうことがあるのだ (後ほど実例を挙げる)。そこでここでは押さえるべきポイントを明確にした、ワンタイムトークンの正しい使用方法を紹介する。まず、アプリケーションの作りとして、リクエスト1が必ずPOSTを用いるような形にする。これによってCSSXSS脆弱性を利用したhiddenフィー ルドの情報の抜き取りを防ぐことができる。また、通常POSTリクエストに対するレスポンスはキャッシュに残らないため、hiddenフィールドの内容が キャッシュとして残ってしまう可能性を下げることにも繋がる。処理1においてリクエスト1のメソッドを判別し、GETの場合は処理を行わないようにする。 または、再度POSTを用いてアクセスしなおすようなレスポンスを返すようにしてもよいだろう。 リクエスト1を受信後、サーバー側のプログラムは処理1を開始する。処理1において、もしまだセッションが開始されていなければ、セッションを開始する。 トークンを生成する。トークンにはセッションIDなどと同様に充分な長さを持つ予測不可能な文字列を使用する。 トークンをセッションと結びつけた形でサーバー側で保存する。いわゆるセッションオブジェクトに格納するのが分かりやすい方法だ。セッションオブジェクト ではなくデータベースを利用する場合には、トークンとセッションIDが1対1で対応付けされた状態で格納する。 hiddenフィールドにトークンを格納したレスポンス1をクライアントへ送信する。この際、レスポンス1がキャッシュに残らないようにするため、適切な ヘッダーフィールドを付ける。 処理1は以上となる。 続いてクライアントはトークンを含んだリクエスト2を送信してくる。このリクエストのメソッドはGETでもPOSTでもよい。 リクエスト2を受信後、サーバー側プログラムは処理2を開始する。まず、送られてきたトークンとセッションが正しい組み合わせであることを、処理1の際に サーバー側に保存しておいた情報を用いて確認する。この組み合わせが正しくない場合にはエラーとして扱い、処理をそこで終了する。組み合わせが正しい場合 には次にすすむ。 サーバー側で保存していたトークンの情報を破棄する。これによってトークンが「ワンタイム」であることになる。 以上でワンタイムトークンの処理が終了する。続いて「日記を書き込む」などの、アプリケーション本来の機能を実行する。 以上が正しい対策その1である。 (下図参照。図は上野宣氏に作成して頂いた。感謝。) リクエスト1がGETでも安全に動作するようにしたい場合は、処理1においてトークンをhiddenフィールドには格納せず、Cookieに格納してクラ イアントに渡すようにする。そしてレスポンス1を受信したブラウザがJavaScriptを使ってCookieからトークンを取り出し、hidden フィールドにその値を格納するようにする。具体的には次のようなレスポンス1を返す。 HTTP/1.0 200 OKまた、トークンをワンタイムのものとするため、処理2においてCookieのトークンをダミー値に置き換える。 CSSXSS脆弱性はレスポンス1に含まれるクッキーの値を取得できないので、このようにすることでリクエスト1でGETを使用できることになる。 英語で書かれたウェブサイトや文献ではCSRF対策の基本としてワンタイムトークン方式がまっさきに挙げられていることが多いが、CSSXSS脆弱性の存 在までを考慮したものは現時点(2006年3月)では見あたらなかった。 ■正しい対策その2: パスワードの再入力を求める方法この方法はアプリケーションがユーザIDやパスワードを用いたいわゆる「ログイン」機能を持つ場合のみ使用できる。ワンタイムトークンを使用する場合と異なり、レスポンス1にはトークンのような秘密情報が含まれない。そのため、この方法はCSSXSS脆弱性の影響を受 けない。また、レスポンス1がキャッシュに残っても問題が生じない。そのため、リクエスト1はGETでもPOSTでもよい。 処理1は何も行わない。 ユーザは画面1においてパスワードを入力する。そしてパスワードを含むリクエスト2が送信される。画面2の中のリンクなどからリファラーを通じてパスワー ドが漏洩してはまずいので、このリクエスト2はPOSTでなければならない。 処理2において、サーバー側プログラムは送信されてきたパスワードが正しいかどうかをサーバー側のデータベースなどの情報を用いて照合する。パスワードが 間違っていればエラーとして扱い、処理を停止する。 以上でパスワード再入力によるCSRF対策の処理が完了する。続いて「日記を書き込む」などの、アプリケーション本来の機能を実行する。 以上が正しい対策その2である。 ■正しい対策その3: CAPTCHAを正しく使用する方法CAPTCHAはトークンの一種である。トークンがhiddenフィールドに格納された文字列ではなく、画像データとしてクライアントへ送られるという点 が上で説明したワンタイムトークン方式との違いだ。CAPTCHAは本来、HTTPクライアントを操作しているのが人間なのか、あるいは自動化されたプロ グラムなのかを判別するために使われる技術であり、CSRF対策とは無関係だ。ただ、CAPTCHAはトークンの一種であるので、正しくワンタイムトーク ンとして使われれば、結果としてCSRF攻撃を防ぐことができる。CAPTCHAを使用する方法はつまりはワンタイムトークン方式である。そのため、上で説明したhiddenフィールドを使ったワンタイムトークン方式に 比べ、CSRF対策としての利点はない。また、ユーザビリティや実装の複雑さで劣るため、進んでCAPTCHA方式を採用する理由は存在しない。それがワ ンタイムトークンであることを知らずに「CSRF対策にはCAPTCHAを使おう。画像だから安全だ。」と考えるのは明らかに誤りなのである。 それでもCAPTCHA方式を採用する場合には、それが正しくワンタイムトークンとして動作するよう、次のように実装する必要がある。 レスポンス1にはトークンのような秘密情報が含まれない。そのため、リクエスト1はGETでもPOSTでもよい。 レスポンス1にはCAPTCHA画像へのリンクが含まれる。そのため、自動的にCAPTCHA画像へのHTTPリクエストが発生する。このリクエストをリ クエスト1.5とする。 リクエスト1.5はPOSTでなければならない。理由はワンタイムトークン方式と同様、CSSXSS対策と、キャッシュ対策を兼ねる。リクエスト1.5は 仕組み上最初にGETで送られるので、再度POSTを用いてリクエストをしなおすような内容のレスポンスを返す必要がある。 CAPTCHA画像を含むレスポンス(レスポンス1.5とする)はキャッシュされないよう適切なヘッダーフィールドを持つ必要がある。 CAPTCHA画像に含まれるトークンはセッションと結びつけた形でサーバー側で保存しておく。 ワンタイムトークン方式と同様、リクエスト2はGETでもPOSTでもよい。 処理2はワンタイムトークン方式と同じように行う。 以上が正しい対策その3である。これはCSRF対策として正しく機能するが、先述の理由により推奨しない。 ■Basic認証の場合Basic認証を用いて「ログイン」機能を提供しているアプリケーションでも、必要なCSRF対策は変わらない。Cookieを使ったセッション管理を行 い、上で説明した対策のうちのひとつを正しく実装することでCSRF対策を行うことができる。■リファラーについてリクエスト2のリファラーをチェックすることでCSRF対策が行えるのではないか、とする説明が存在する。しかしこのリファラー方式は、正規ユーザのウェ ブブラウザがきちんとリファラーを送ってくる場合のみ機能する。アンチウイルスソフトウェアやユーザ自身の意向によりリファラーを送出しない設定で使用さ れているブラウザも多く存在するため、この方式は現実的には機能しないケースが多い。利用者が限定されており、その全員にブラウザをリファラーを送るよう な設定で使用するように求めることができるケースでは、リファラー方式はCSRF対策として正しく機能する。リファラー方式を採用する場合には次のように実装する。 処理2においてリクエスト2のリファラーを調べ、それが外部サイトのものであればエラーとして処理する。また、リファラーが存在しない場合も同様にエラー として処理する。 また、リファラーを調べることにより、攻撃者がCSRF攻撃のために仕掛けた罠ページを発見できる可能性がある。そのため、実際にCSRF攻撃が行われて いるのかどうかを把握したい場合には、リクエスト2のリファラーを(外部サイトからのものに限り)記録しておくとよい。 ■SSLについてSSLの使用はCSRF対策には基本的には関係がない。しかしHTTPSの通信はキャッシュされない、また盗聴されにくくなるなどの利点もあるため、可能 な場合には上で挙げたCSRF対策と合わせて利用するのがよい。■採用すべきでないCSRF対策ここからは筆者がウェブサイトや書籍で見つけた、採用すべきでないCSRF対策について説明する。これらの対策は意味がなかったり、かえってシステムを危 険な状態にしてしまうので、実施してはいけない。■採用すべきでない対策その1: セッションIDをトークンとして使う
2006年3月の時点で、国内の多くのウェブサイトや書籍などで紹介されている方法である。 |