まず、Webアプリ以前に、一般に、ボタンを押すなどして画面を次々に遷移していくアプリケーション(スタンドアロンアプリケーション(ネットワークなしに単体で動作するもの)を含めて)を作成するときは、設計段階から(もしくは事後に)図1のような画面遷移図を描いて検討(ないし確認)するだろう。
水色の四角は画面を表し、白抜き実線枠の四角はボタンを表す。
これを、Webアプリという実装手法を選択する場合に特化すると、図2のような遷移図が描ける。
実線矢印はブラウザが送信するHTTPのrequest(ヘッダおよび、POSTの場合はボディを含む)を表し、黄色の丸がサーバ側での1アクセスの処理を表し、点線がその処理結果を返すHTTPのresponse(ヘッダおよび、HTML)を表す。responseの上の文はHTMLの内容を説明するものである。黄色の丸の中の文は処理内容の説明であり、ここから複数のresponse矢印が出ている場合、処理の結果によって遷移先の画面が異なる場合であることを表し、破線の白抜き四角がその分岐の条件を概説している。
この図で例に用いているのは、ECサイトやblogサービスなどに見られる典型的な「登録個人情報変更」の機能である。「メインメニュー」画面の「登録情報変更」のリンクをクリックすると、「登録情報DB読み出し」の処理が実行され、その結果(現在の登録情報)が以下のようなHTMLとして構成されて出力され、
<form action="......" method="POST"> 氏名 <input type="text" name="name" value="高木"> 住所 <input type="text" name="addr" value="東京都"> 電話 <input type="text" name="tel" value="03-XXXX"> <input type="submit" value="確認">
「登録情報編集」の画面が以下のように表示される。
「確認」ボタンを押すと、「様式検査」処理(電話番号が電話番号の形式をしているかどうかなどの検査)が行われ、正常であれば「プレビュー」画面に進む。プレビュー画面は次のような画面となるだろう。
これでよろしいですか?
氏名: 高木
住所: 東京都
電話: 03-XXXX
さてここで、この次の画面に進ませる(「保存」を押したときの処理を実現する)ためにどのような方法を用いるか、次の2種類の実装方法がある。
一つ目は、「保存」ボタンを押したときのアクションをPOSTメソッドとし、そのパラメタにこれらの情報を渡すようにする方法で、それらパラメタが画面上に見えないようにするために <input type="hidden" ...> を用いる。つまり、「プレビュー」画面のHTMLは次のようになる。
<p>これでよろしいですか?</p> 氏名: 高木<br> 住所: 東京都<br> 電話: 03-XXXX<br> <form action="......" action="POST"> <input type="hidden" name="name" value="高木"> <input type="hidden" name="addr" value="東京都"> <input type="hidden" name="tel" value="03-XXXX"> <input type="submit" name="back" value="戻る"> <input type="submit" name="save" value="保存"> </form>
これを画面遷移図に書き込んだのが図3である。
青い矢印はPOSTメソッドによるHTTP requestであることを表す。
二番目の方法は、図4に示すものである。
図中の六角形はサーバ側のオブジェクトを表す。「登録情報編集」画面で入力されたデータが「確認」ボタンでサーバに送信されたとき、サーバは「様式検査」の処理の前に、入力データをサーバ側オブジェクトに記憶し、その後の処理ではこのオブジェクトの値を参照して実行するようにする。プレビュー画面の出力や、プレビュー画面で「保存」ボタンを押したときの「DB書込」処理でも、このオブジェクトから値を参照して処理を実行する。
この方式を採用した場合、「プレビュー」画面に hiddenパラメタは不要となる。その結果として、「プレビュー」画面からの遷移のHTTPアクセスはPOSTメソッドでなくてもよいことになる。
さて、この「サーバ側オブジェクト」はどのように実現すればよいだろうか。
安易なその場限りの方法でよければ、「セッションオブジェクト」(ログインユーザごとに1つ用意された記憶空間。「セッション変数」と呼ぶミドルウェアもある)に「name」「addr」「tel」という名前の変数で記憶しておくという発想が出てくるだろう。
しかし、本物のプログラマ*1であれば、ここで、それがそんなに単純な話でないことにすぐに気づくはずだ。メインメニューから使える他の機能があるなら、変数名が衝突しないようにしなければならないし、それよりなによりも、同じ機能を同時に複数使う場合にどう管理すればよいのか――といったことに思いをめぐらすはずだ。
こうした考え方は、オブジェクト指向プログラミングがGUIシステムの開発に向いているとして発展してきた経緯に符合する。 オブジェクト指向プログラミングを知らないプログラマがGUIシステムを作ろうとすると、変数管理を自前でやるという汚いコーディングに陥ることがある。つまり、同じ画面が複数のウィンドウとして開かれたときに、そこで使う変数名が衝突することを避けようとして、変数を配列にして、ウィンドウ番号をインデックスにしてアクセスするという発想だ。オブジェクト指向プログラミングを知っていればそうした汚い作り方は自然に回避される。ウィンドウを開く時点で、ウィンドウオブジェクトを「new」することにより、そのウィンドウ専用の記憶空間が用意される。変数はそのオブジェクトにローカルであるので、他のオブジェクトでどうなっているかを気にする必要がない。
今日のスタンドアロンアプリケーションのGUIシステム開発では、GUIライブラリがそうしたオブジェクト指向スタイルで設計され、提供されているので、開発者は自然とそうした開発手法に従うことになる。
ところが、GUIシステムをWebアプリとして作るとなるとどうだろうか。
スタンドアロンのGUIアプリと同様に、Webサーバ側を、オブジェクトを「new」しながら動作するように作ることはできる。あとは、それらのオブジェクトをどうやってクライアント側のWebブラウザと結びつけるかだ。
ひとつの自然な方法は、新しく開くウィンドウごとにシリアル番号を振り、その番号をブラウザに渡し、アクセスごとに返させるようにすることである(図5)。
図5は、2つの「追加」処理が同時に進行している様子を表したものである。「追加」のリンクが辿られると、ウィンドウオブジェクトを生成し、ウィンドウを識別するシリアル番号を得る。生成したオブジェクトをそのシリアル番号で検索できるように、オブジェクトテーブルに登録する(セッションオブジェクトから辿られるテーブルに)。処理を進める各画面のHTMLには、
<input type="hidden" name="winid" value="2">
のようにしてウィンドウのシリアル番号「winid」を埋め込んでおき、POST(またはGET)アクションの各処理では、HTTP request中のwinidの値から(セッションオブジェクト経由で)オブジェクトを検索し、その参照を得る。「様式検査」の処理でそのオブジェクトのフィールドに値を書き込み、「DB書込」の処理でその値を読み出してDB更新の処理を実行する。
こうした作り方は、オブジェクト指向なGUIシステム設計の流れからすれば自然であるところ(いくつかのWebアプリフレームワークではこうした実装方法を提供しているであろうが)、しかしながら、実際にWebアプリでこうした実装方法をとっている例は少な目かもしれない。
にもかかわらず、複数ウィンドウを用いた同時編集を許すWebアプリというのが、珍しいわけではないだろう。上のような一般化されたオブジェクト指向Webアプリフレームワークを用いずに、複数同時編集を実現するにはどうするのだろうか。たいていの場合、編集開始の最初の段階で、機能に固有の何らかの識別コードを確定させて使う方法が使われているのではないか。「何らかの識別コード」とは、システムでユニークな文書番号だったり、日記システムであれば日付がそれに該当する(図6)。
日記システムであれば、「追加」機能の最初の段階で編集する日付を確定させ、「date」パラメタにその値を入れて次画面へと渡す。セッションオブジェクトで記憶しておくべきデータは、日記の日付から検索して読み書きするように作る。
この実装方法では、同じ日のエントリを同時に編集することはできなくなるが、この場合はユーザにもその挙動は直感できることであるため、この仕様で満足とされる。
たいていのWebアプリはこの方法で作れてしまうので(あるいは図3の方法で作ることが多いため)、このようなアドホックな実装方法のことしか考えたことのないWebプログラマも少なくないかもしれない。
クロスサイトリクエストフォージェリ(CSRF)攻撃による被害は、Webアプリが設計上想定していない画面遷移を辿られることによって起きる。であれば、Webアプリが想定している画面遷移しか許さないようにすることによって(も)解決できるということになる。
画面遷移を完全に制御するとなったとき、本物のプログラマがまず思い浮かべるのは図5の実装であろう。ただし、想定外のユーザ操作によって画面遷移を外れることを防ぐためには、図5のようにウィンドウ識別番号としてシリアル番号でよかったところ、これが悪意ある第三者の攻撃を想定するとなると、この値は予測困難な値としなくてはならない。これは、「new」したときのwinidに十分な長さの乱数値を与えるようにすればよい。
CSRF対策に「ワンタイムトークン」を用いるという話が出ると、本物のプログラマ達は、まず図5の実装(でwinidを乱数にする)を頭に浮かべるのではなかろうか。
しかし実際には、図3ないし図6の実装方法ばかりが採用されているという現実がある。機能的にはそれで十分だからそうしていたのに、後になって図5の実装方法が「必須だ」と言われたら困惑するだろう。全体にわたって作り直すことになってしまう。
昨年7月3日の日記「クロスサイトリクエストフォージェリ(CSRF)対策がいまいち進まなかったのはなぜか」では、CSRF対策が進まなかった原因を複数挙げたが、その中で、
センスある開発技術者の立場からすれば、「あんな対策も必要、こんな対策も必要」と言われることは、プログラミング美学に反すると感じられることもある。
XSSやSQLなどのインジェクション系の脆弱性は、脆弱性であるという以前に、本来正しいコーディングをしていればそういう穴にはならないのだから、美学に反するものではない。バッファーオーバーフローもそうだ。センスある開発技術者にとっては「対策」不要というのが美学だ。
その点、CSRF対策が必要というのは、どうにも気持ち悪いものとなる。画面の遷移をすべてチェックするというのであればまだわかるが、一部の画面に姑息な手段で対策を仕掛けないといけないとなると、なんだか美しくないように感じられる。
と書いている。画面遷移を完全に制御するのは自然な実装であるが、その実装はオーバースペックであるし、後からそのように変更するのは大変すぎる。
本物のプログラマには「対策は簡単でない」と思われていることが対策を遅らせているのではないかと考えた。
それなのに、「ワンタイムトークン」方式を採用すべきと主張する人達は、「実装が簡単なんだからやればいいじゃないか」と言う。
hiddenとセッション(とCookie)に予測不可能な文字列を入れる
セッションIDをハッシュする手法に比べ、使用するのは無意味な文字列なので安全性が高い。
- 推奨: する
- 効果: 大
- 回避法: 特にない
- 実装: 簡易
- 副作用: ない
ワンタイムチケット(トークン)のコードなら4/27の時点でも、ちょっと探せばいくらでも見つかったはずです。
>ワンタイムトークンを正しく使うという解決策が書かれているのだけれど、
>正直これはちょっと面倒である。
本当に面倒ですか?実際にコードを書いてみると数行しか違わないんじゃないで
しょうか。僕は本業がウェブアプリ開発なのですが、例えば「日記を書き込む」
みたいな、ウェブアプリケーション本来の機能の複雑さ・面倒くささに比べれば
ワンタイムトークン方式を組み込むことくらい何でもないのですが。
> そうですね、このあたりは感覚の違いだと思います。
> 本来の機能と比べてしまうとちょっとアレですが、
> 僕はやっぱりワンタイムにする面倒さを考えると、
ワンタイムにする場合は処理2でセッションオブジェクトからトークンを削除す
る必要があるので、例えばJavaなんかだと
session.removeAttribute( "token" );
を加えないといけませんね。
…1行多いですね。これ面倒ですか?(笑
なぜこんなに簡単だ簡単だと言えるのだろう? と不思議に思えるところ、要するに、彼らが主張する実装は、図7のような方式ということである。
グローバル変数1個に「トークン」を入れる方法なので、これでは同時編集ができなくなる。
実際のコード
// 確認画面 session_start(); (認証など略) $uniq_id = md5(uniqid(rand(),1)); // 推測不可能な文字列を生成 $_SESSION['uniq_id'] = $uniq_id; // セッションに保存 setcookie('uniq_id', $uniq_id); // Cookieにも保存 // hiddenにも入れておく print "<input type=hidden name=uniq_id value='{$uniq_id}'/>";
> 2画面以上開かれてしまった場合の手当てとか、
> もっと複雑な気がなんとなくしてました。
失礼しました。私が間違ってました。
ご指摘どおり2画面以上の場合を考慮するため、ワンタイムトークン方式は実際
にはトークンの名前もそれぞれの機能ごとに別のものを利用し、管理するのがベ
ターです。
つまり「日記を書く」のトークンはtoken1、「日記を消す」のトークンはtoken2
にする、などですね。
そうなると処理1及び処理2で「トークンの名前を求める」という処理が必要にな
るので、さらに1行か2行くらいずつ増えそうです。(面倒かどうかはアレですが
笑)
変数を機能ごとに別々にしても、同じ機能の同時編集は依然としてできない。(インスタンス変数を使うべきところにクラス変数を使うような話である。)
自分だけが使うWebアプリに対策したときに、同時編集不能になろうが、それは自由だ。ある特定のWebアプリで同時編集が不要であれば、その対策方法を採用することもできるだろう。
だが、Webアプリ一般における対策方法を説くときには、アプリの基本設計に制限をもたらすようなものでは駄目だ。
図5の実装方法を推奨できれば話は早いが、先に述べたように、残念ながらこのオブジェクト指向スタイルのWebアプリ実装はあまり普及していない。図3の実装方法を用いている場合でも、図6の様々な実装方法の場合でも、どんな場合にも使える対策方法を示したいところだ。
編集操作のインスタンスごとにトークン用の記憶域を確保しようとすると、図6の実装方式の場合、「識別コード」の存在にまで踏み込んで説明しないと、記憶域の確保方法を説明できない。ひとつの例について書くことはできてもそれはすべてではない。
ここで、あえて図7の方式をベースに改善することを考えてみる。たとえば、次の図8のようにすれば解決できるだろう。
トークン格納用変数を集合型オブジェクトとし、任意個の発行されたトークンを格納していく。「DB書込」処理の冒頭では、hiddenパラメタで渡されたトークンがこの集合の中に含まれているかどうかを調べて、正規のリクエストであるかを確認する。
図5の、想定外のユーザ操作によって画面遷移を外れることを防ぐ場合には、IDが一致していないといけなかったが、第三者からの攻撃だけを防ぎたいときには、IDが既存のものであるかを調べればよい。同じセッションに格納されたトークンはすべて正規の画面遷移によるものだからである。
このようにして考えると、トークンはログインセッションで共通の1個でよいことがわかる。画面ごとに別の値にする必要もないし、操作インスタンスごとに変える必要もない。
「ワンタイムトークン」という発想は本来、画面遷移を完全にコントロールすることでCSRFを解決しようとするものだろう。だが、CSRFを解決するだけの目的には、第三者からの攻撃リンクによる画面遷移なのか、正規の画面遷移なのかさえ区別できればよいのである。
もし、画面遷移の完全コントロールを必要としていて既に図5の実装をしているならば、CSRF対策はウィンドウIDを予測困難な値にすればよい。だが、画面遷移の完全なコントロールは一般的には不必要であり、CSRF対策のためだけに図5の実装を推奨するのは開発者に対して酷であろう。
そのため、昨年4月27日の日記「クロスサイトリクエストフォージェリ(CSRF)の正しい対策方法」では、
不適切な解説の例
- ワンタイムトークンを使わなくてはならない。
と書いたのだった。
乱数を生成するということはそう容易いことではない。自動的にセキュアであると謳うCSRF対策をするならば、チェック用の値は暗号学的に安全な擬似乱数生成系で生成しなければならない。
自分の使っている環境でそのような乱数生成系がすぐに使える状態になっているからそれを使うというのは自由だけども、他人に対策を説く場面では、あらゆる環境を想定して、そのような乱数生成系の使用を推奨しなくてはならない。
セッションIDを使うという方式は、そうした乱数生成の問題(プログラマが杜撰な乱数生成方式を採用してしまう事態)を避けることができる。なぜなら、セッションIDは暗号学的に安全な擬似乱数生成系で生成されているはずだからである。(そうでなけれは、そのセッション管理機構自体に脆弱性があるということになり、それがまず修正されるべきとなる。)
tDiaryのCSRF対策でも、乱数を用いるのを避けている。その理由は、tDiaryは一般的なCGI環境とRubyがあれば動作することを保証しているため、セッション管理機構の導入が必須になるような対策方法は採りたくなかったこと、そして、Rubyの乱数生成系(少なくとも当時の)は暗号学的に安全なものではなかった(どのOSにおいてもという意味で)からである。
そこで、tDiary 2.1.2のCSRF対策では、Referer:チェックによる対策をデフォルトとし、どうしても Referer:を空にしても使えるようにしたいユーザは、ユーザの責任においてユーザ固定キーを設定して使うことにしたものである。ユーザ固定キーは、暗号学的に安全な擬似乱数よりも弱いものであるが、ユーザの責任で設定するものであり、システムが自動的にセキュアであると謳うものではない。
どうも素人さんには「hiddenは簡単に読める」という思い込みがあるらしい。
(略)何も、戻る時に追加の情報をポストする必要がない、という前提ではあるが、Cookieでセッション管理していれば大抵、そういう状況だと思う。(そもそもセッション管理をinput[type="hidden"]等で行うべきではない。改竄が容易というのもあるし、もしCSS3のcontentやappearanceが実装されると、簡単にブラウザ上にこの要素をinput[type="text"]のように表示してデータを変更して送信し直せる可能性が高いからだ。Cookieなら改竄できないかというとそういう訳ではないが、CSS3のこれらのプロパティはWebアプリケーションにとっては、驚異的である。また、(略)
「改竄」というのは、ブラウザのユーザがhiddenパラメタを弄ることを指しているのだと思われるが、このような理由で「セッション管理をhiddenで行うべきでない」とするのは、二重の意味で誤りである。
第一に、ブラウザが送信する値をユーザが自由に変えられるのは当然であり(「改竄」などという表現を用いること自体が不適切なのだが)、どんなふうに変えられてもセッション管理が安全であるようにしなくてはならない。そのために、セッションIDには、予測困難な十部に長い暗号学的に安全な擬似乱数を用いることが必須になっている。(「容易に改竄されるので危険」などと思う人は、ユーザ名をセッション管理に用いていたりするのだろうか?*2)
第二に、hiddenの値を変更して送信する困難さと、cookieの値を変更して送信する困難さは同一である。(HTTPのヘッダにあるか、ボディにあるかの違いでしかない。)
こうした思い込みは少なくないようで、「HTMLソースで見える」という経験がそうした感覚を生じさせるのだろう。だが、cookieを読むのだって、アドレスバーに「javascript:document.cookie」と打ち込むだけでできる。
これも、読まれる可能性はcookieと同等である。
通常、読まれるのが困る場合はSSLで防止する。
CSRFチェック用に用いる秘密情報の候補としては、
などが考えられるところ、「クロスサイトリクエストフォージェリ(CSRF)の正しい対策方法」でセッションIDをそのまま使う方法を挙げたことに対して、いくつか批判があった。検討に値するのは、サーバ側やブラウザの脆弱性によって漏洩するケースである。
まず、漏洩の可能性のパターンで脆弱性を分類すると次のようになる。
このうちhiddenパラメタが漏れるのは、1. および 3. と 4.である。
1.の脆弱性があるとき、hiddenパラメタが漏れるときはcookieも漏れるのだから、hiddenのリスクだけを個別に検討する理由にならない。
次に、3.の脆弱性だけがあるとき、hiddenに(cookieと同じ値の)セッションIDを入れている場合、どのようなリスクがあるか。ただし、RefererチェックによるCSRF対策をしていない(hiddenパラメタによるCSRF対策をしている)とする。
この場合、脆弱性を突かれてhiddenの値が漏れることが、漏れないはずのcookieの値が漏れたのと同じ結果を招くことから、「hiddenにそのような値を入れるのは好ましくない」という議論が出てくる。
しかし、セッションIDが漏れることによって起きるリスクはセッションハイジャック攻撃であるところ、3.の脆弱性がある場合、いずれにせよ(hiddenにセッションIDを入れなかったとしても)セッションハイジャックと同等の脅威が生じる。その理由を以下に記す。
CSRF攻撃の脅威がセッションハイジャック攻撃と異なる第一の差は、セッションハイジャックでは個人情報などを閲覧できる場合があるのに対し、CSRFではHTTPのresponseを取得できないため、そうした直接的な情報漏洩の脅威はないという点である。
しかし、今ここでは「HTMLテキスト(hiddenパラメタを含む)が漏れる脆弱性」の存在を仮定しているので、攻撃者はCSRF攻撃のresponseを取得できる。つまり、直接に個人情報等を盗み出すことができる。
第二の差は、CSRF攻撃は通常、単発のアクセスしかできないという点である。つまり、Webアプリの構成によっては、1ページ目のresponseに含まれる値を使って2ページ目のrequestに含めないと目的を達成できない画面があり得るところ、通常のCSRFではそれら2つのページへ連続した攻撃をしかけることはできない。(この性質があるからこそ、hiddenパラメタに秘密情報を埋め込んでおくことがCSRF対策となり得る。)それに比べて、セッションハイジャック攻撃は、連続したページのアクセスを自由に操れるのであるから、単発アクセスしかできないCSRF攻撃より強力だということになる。
だが、今ここでは「HTMLテキスト(hiddenパラメタを含む)が漏れる脆弱性」の存在を仮定している。それは、1ページ目へのCSRF攻撃のresponseを攻撃者は読めるということを意味するのだから、その値を使った2ページ目のCSRF攻撃へと続けることができてしまう。
よって、セッションハイジャック攻撃を用いなくても、それと同じ脅威をもたらす攻撃は可能である。
脅威に差がないのであるならば、hiddenパラメタにcookieと同じセッションIDを入れておくことが新たな脅威を生むことはない。
これは新しい話ではない。たとえば、2000年に立て続けに発覚したJavaの「ネット接続機能が任意サイトへのアクセスを許す」脆弱性は、まさにその危険性のあるものだった。攻撃者の仕掛けたアプレットを表示したとき、この脆弱性を突かれると、そのJavaアプレットは、ユーザがログイン中のWebアプリにアクセスして、操作を完全に乗っ取ることができた。その原因は、攻撃者のプログラムが、攻撃先サイトのresponseを得られるところがミソであった。
同様に、JavaScriptの「同じホストのページのDOMにしかアクセスできない」という基本制約が破れる脆弱性がこれまでに多数発見されては修正されてきたが、この脆弱性がある場合も、同じ脅威があった。(もっともこのケースでは、cookieも盗まれるものであったが。)
残るは 4.のケースであるが、hiddenだけ漏れ、それ以外のHTMLテキストは漏れないという、そんな脆弱性がはたして生ずるのだろうか。つまり、以下の強調部分以外は漏れず、強調部分だけ漏れるという脆弱性である。
氏名: 高木<br> 住所: 東京都<br> 電話: 03-XXXX<br> <form action="......" method="POST"> 氏名 <input type="text" name="name" value="高木"><br> 住所 <input type="text" name="addr" value="東京都"><br> 電話 <input type="text" name="tel" value="03-XXXX"><br> <input type="hidden" name="sessionid" value="2aa5adb93356d85697cd0a114eb2100f"> <input type="submit" value="確認">
このような著しく特殊な脆弱性が将来生ずる可能性があると心配するならば、「セッションIDをそのままhiddenに入れないほうがよい」という理屈は成り立つ。
次に、「セッションIDをハッシュしておけばよい」「ハッシュしないよりした方がよいに決まってるし、コストもかからないのだからやっておけばいい」という主張があったが、はたして本当だろうか。
(続く)
*1 Webデザイナあがりの日曜掲示板プログラマでなく。
メモ書き: とりあえず、現状で3を達成する脆弱性としてCSSXSSが存在する(という理解は正しい?) CSRFとセッションジャックの違いとして、はてな等のような長期セッションの場合に、 CSRFでは本来の利用者のアクセスをトリガとして引き起こされる セッションジャックでは、..
エアダスター 液晶拭くやつ http://video.google.com/videoplay?docid=6176491654107670145 http://takagi-hiromitsu.jp/diary/20060409.html http://faker.but.jp/column/index.php?p=84 あとでみよう http://japan.cnet.com/news/ent/story/0,2000047623,20098867,...
ものすごく遅くなってしまったが、高木氏の反論を読ませて頂いた。
...
Link/Security/CSRF ▲ ▼CSRF(クロスサイトリクエストフォージェリ) CSRF - クロスサイトリクエストフォージェリ 大量の「はまちちゃん」を生み出したCSRFの脆弱性とは? CSRF Blog クロスサイトリクエストフォージェリ(CSRF)の正しい対策方法 CSRFは「シーサー...