CodeIgniter + HTMLPurifier
2008年06月23日 18:39
CodeIgniter の入力フィルターとして、デフォルトでは /libraries/Input.php に含まれている xss_clean() メソッドを使いますが、更に強力なフィルターとして HTMLPurifier というライブラリを使えます。今回はこのライブラリの導入と、他のライブラリも含めて簡易テストする方法をご紹介します。
CodeIgniter の Input ライブラリ(XSSフィルター)について
CodeIgniter にデフォルトで用意されている XSS フィルターは、/system_folder/libraries/Input.php の xss_clean() というメソッドです。念のため使い方を確認しておくと、Input.php は特に何も呼び出しの必要なく使えます。また、application/config/config.php で、$config['global_xss_filtering'] の値を TRUE としていれば、特に xss_clean() メソッドを呼び出さなくても、POST データや COOKIE データを扱うときはグローバルにフィルタリングされます。ここで「POST データや COOKIE データを扱うとき」と言いましたが、それは $this->input->post() などの Input クラスのメソッドを使うときという意味であって、任意の $_POST[ 'foo' ] をそのまま使ってもフィルタリングされているという意味ではありませんので、ご注意ください(これを誤解すると、$_POST[ 'foo' ] のまま使えるといった、余計に脆弱なコードを書いてしまいかねない)。試しに、config.php で $config['global_xss_filtering'] の値を TRUE に設定した上で、適当なコントローラにて、
$_POST[ 'test' ] = ‘<script>alert(”xss”);</script>‘;
echo $_POST[ 'test' ];
などとやってみれば分かります。
いずれにせよ、$config['global_xss_filtering'] を TRUE に設定することは次の理由からおすすめできません。それは、このフィルタリングをグローバルに適用すると極端にパフォーマンスが落ちるからです。問い合わせフォームなど、個人のサイトで利用する分にはグローバルに適用しても殆ど問題はないと思いますが、それでも POST や Cookie のデータを何らかの変数に格納しないで、そのままグローバル変数としてループ処理するようなコードを書いてしまうと、それなりに影響は出ます。サンプルコードとして、
<?php
$_POST[ 'test' ] = ‘pDL2bHhRqa7EyFtJ}nVZ{5gxfYKuU)<3rjoXsSGQ[PAkBwC6ce6MvN(9d_m>T]i4′;
$result = ”;
$this->benchmark->mark(’code_start’);
for( $i = 0; $i < 100000; $i++ ) {
$result .= $_POST[ 'test' ];
// OR $result .= $this->input->post( ‘test’ );
}
$this->benchmark->mark(’code_end’);
echo $this->benchmark->elapsed_time(’code_start’, ‘code_end’);// DO NOT TERMINATE WITH PHP END TAG[EOF]
をローカルの環境で計測してみると、次のような結果になっています(Windows XP Pro + Apache 2.2.8 + PHP 5.2.6)。
- $_POST[ 'test' ] を XSS グローバル設定なしで試行
… 10回計測の平均で 0.18299 秒(0.1766, 0.1900, 0.1777, 0.1846, 0.1792, 0.1791, 0.1809, 0.2049, 0.1779, 0.1790) - $_POST[ 'test' ] を XSS グローバル設定ありで試行
… 10回計測の平均で 0.18165 秒(0.1812, 0.1811, .1784, 0.1872, 0.1819, 0.1810, 0.1792, 0.1801, 0.1887, 0.1777) - $this->input->post( ‘test’ ) を XSS フィルターオプションなしで試行
… 10回計測の平均で 0.51228 秒(0.4414, 0.4345, 0.4336, 0.4824, 0.5277, 0.4274, 0.7259, 0.5219, 0.4325, 0.6955)
上記と、次の計測結果を比べてください(ちなみに、WordPress が下の ol タグの start 属性を引っこ抜いてしまうので、文章を書き直すのに手間がかかりますね・・・)。
- $this->input->post( ‘test’, TRUE ) を XSS フィルターオプションなしで試行(グローバル設定あり)
… 3回計測の平均で 61.53645 秒(60.656250, 61.328125, 62.62500)
XSS フィルターを使うとパフォーマンスが極端に落ちることが分かると思います。しかも、最後のパターンは $_POST[ 'test' ] に ‘p’ という一文字しか入れていません(上記のランダムな文字列 64 文字のままだと、たいていの設定でタイムアウトするため)。’p’ を 10 万回フィルタリングするのに約1分かかるということです(上記のコードでベンチマーク計測してもにわかに信じがたかったので、ローカル環境の Apache Bench でも計測してみましたが、やはり結果は似たような処理時間でした)。
以上の結果から、まず Config でグローバルな XSS フィルターを設定しても $_POST の値に直接フィルターがかかるわけではないということが分かります(そのようなフィルターのかけ方は誤解を招きやすいので、もともと避けるべきであって当然とも言えます)。次に、XSS フィルターをグローバルないしはオプションとして使うと、処理結果が著しく異なります。おおよそ上記の結果から、1文字を10万回フィルタリングするのに1分とすると、単純に言って64文字では64分かかると言えます。1.~3. の処理時間と比較すると、非常に単純な結論として、XSS フィルタリングを使うとき(4.)と使わないとき(3.)では、7,200 倍の差があります。
このような次第で、XSS フィルターをグローバルに使うべきかどうかは、やはり具体的にどのような処理を書きたいかによって決まると言えますが、習慣の問題として XSS フィルターのオプションを Input::post() メソッドや Input::sever() メソッドへ常に追加する方が、セキュリティ意識の問題として考えても望ましいと思われます。CodeIgniter は RAD ツールのような上げ膳据え膳を目指してはいない筈ですし、セキュリティの実務に携わっている立場として、「どこかで勝手にやっていてくれる」という意識をプログラマが持ってしまうのは非常に危険なことだと考えます。
HTMLPurifier の導入
さて、以上にご紹介した Input ライブラリの xss_clean() メソッドの他に、サードパーティー製のフィルタリング・ライブラリも出ています。その代表格として、HTMLPurifier をご紹介しましょう。これは PHP で書かれているフィルタリング・ライブラリであり、XSS 対策だけではなく、HTML, CSS のコードも W3C の規準に従っているかどうか検証してくれます。PHP5 専用の 3.1.1(2008年6月23日現在)と PHP4 に対応する 2.1.5 の二系統に分かれており、CodeIgniter で使う場合もサーバがどちらの系統の PHP で動いているかに依存します(サーバが CGI モードでPHP を動かしている場合は、PHP ファイルへ個別に #! 行などを追加しなくてはならない場合もあります)。
ひとまず動作を確認してみたい方は、PHP4 に対応する HTMLPurifier 2.1.5 をお勧めします。フレームワークとして CodeIgniter を選択している方の中には、CodeIgniter が PHP4/5 の両方で動作するという点も理由として含まれているのではないでしょうか。HTMLPurifier の PHP4 対応版は、PHP5 の環境でも動きますので、PHP4/5 のどちらでも動くという CodeIgniter と同じ条件を保つには、PHP5 専用版でない方がよいと考えます。
CodeIgniter に HTMLPurifier を導入する方法は、Andy Mathijs さんの “Htmlpurifier and the CodeIgniter framework” に掲載されています。具体的には、
- htmlpurifier-2.1.5.zip(または tar.gz 版)をダウンロードする。
- 上記を解凍して、/library フォルダに入っているファイルを、全て CodeIgniter の /application_foler/libraries にコピーする。
- /application_foler/libraries/HTML_Purifier.php ファイルの先頭に、
set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path() );
という一行を追加する。
以上でインストールが完了します。使い方としては、
$this->load->library( ‘HTMLPurifier’ );
でライブラリを呼び出して、
$config = HTMLPurifier_Config::createDefault();
で設定をロードします。こうして、
$clean_html = $this->htmlpurifier->purify( $input_html , $config );
のように htmlpurifier->purify() メソッドを使ってフィルタリングできるようになります。
XSS フィルタリングのテスト
XSS フィルタリングをテストする機械的な方法はたくさんあって、もちろん Tenable Nessus や N-Stalker などのセキュリティスキャナを使ったり、Firefox であれば XSS Me / SQL-Inject Me のようなプラグインも有用です(現在は Firefox 3 に未対応)。また、これらをヒントにして、自分なりの攻撃パターンを組んでみてペネトレーション・テストも出来る限りやっておきたいものです。中でも、機械的にチェックできる点はすぐにでもやっておきたいので、いまご紹介したようなツールに加えて、次のようなパターンも試してみることをお勧めします。それは、XSS Cheat Sheet でよく知られている、”<” と “>” のさまざまなエンコーディング・パターンを試してみることです。
僕たちがよく知っている XSS のパターンは、あからさまなものを例として挙げると、
<img “”"><script>alert(”XSS”)</script>”>
のようなコードです。このようなコードに対する、きわめて初歩的な(現在では寧ろ有害と言ってもよい)「対策」として、”<” とか “>” とか “javascript” というフレーズをサニタイズすればよいというアイディアがあります。しかし、例えば $output = str_replace( “<”, “<”, $input ); のような置換は、”<” がもっと他にも色々なエンコーディングで表現可能であるという点を無視しており、”<” という一つのパターンしか相手にできません。
実際、ha.ckers.org の XSS Cheat Sheet で紹介されている実体参照や数字参照コードでは、ブラウザが一定の条件ではソースに “<” とレンダリングしかねないパターンとして、次のようなパターンが紹介されています。
<
%.3C
&.lt
&.lt;
&.LT
&.LT;
&.#60
&.#060
&.#0060
&.#00060
&.#000060
&.#0000060
&.#60;
&.#060;
&.#0060;
&.#00060;
&.#000060;
&.#0000060;
&.#x3c
&.#x03c
&.#x003c
&.#x0003c
&.#x00003c
&.#x000003c
&.#x3c;
&.#x03c;
&.#x003c;
&.#x0003c;
&.#x00003c;
&.#x000003c;
&.#X3c
&.#X03c
&.#X003c
&.#X0003c
&.#X00003c
&.#X000003c
&.#X3c;
&.#X03c;
&.#X003c;
&.#X0003c;
&.#X00003c;
&.#X000003c;
&.#x3C
&.#x03C
&.#x003C
&.#x0003C
&.#x00003C
&.#x000003C
&.#x3C;
&.#x03C;
&.#x003C;
&.#x0003C;
&.#x00003C;
&.#x000003C;
&.#X3C
&.#X03C
&.#X003C
&.#X0003C
&.#X00003C
&.#X000003C
&.#X3C;
&.#X03C;
&.#X003C;
&.#X0003C;
&.#X00003C;
&.#X000003C;
\.x3c
\.x3C
\.u003c
\.u003C* 上記の文字列には、WordPress のエンジンによってエンコードされてしまわないために、第一文字目と第二文字目のあいだにピリオド(”.”)をわざと挿入しています。また、一つ目の “<” は、このページのソースでは実体参照で書いていますが、実際は “<” という文字のままです(< ではありません)。
上記と、”>” のパターンを合わせると、<script> としてソース内にレンダリング可能な組み合わせだけで 70 の 2 乗 = 4,900 通りがあります。これに加えて、クロージングの </script> にもそれぞれ別々のエンコーディングパターンを掛け合わせると、全ての組み合わせは 70 の 4 乗 = 24,010,000 通りになります。もちろん、或るパターンで “<” と(ソース内で)レンダリングされてしまう場合は、”>” も可能であると思われるので、異なるパターンを掛け合わせても全てレンダリングされてしまうというのは、よほど出来の悪いブラウザだけかもしれません。しかし、可能性を一つずつ排除していかなければ、セキュリティというものはそこでレベルが止まってしまいます。
ということで、僕は最低でも一組の “<” と “>” のレンダリングをテストできるように、次のようなメソッドを使っています。
function _callXSSCode() {
$left = array( ‘<’,'%.3C’,'&.lt’,'&.lt;’,'&.LT’,'&.LT;’,'&.#60′,’&.#060′,’&.#0060′,’&.#00060′,’&.#000060′,’&.#0000060′, ’&.#60;’,'&.#060;’,'&.#0060;’,'&.#00060;’,'&.#000060;’,'&.#0000060;’,'&.#x3c’,'&.#x03c’,'&.#x003c’, ’&.#x0003c’,'&.#x00003c’,'&.#x000003c’,'&.#x3c;’,'&.#x03c;’,'&.#x003c;’,'&.#x0003c;’,'&.#x00003c;’, ’&.#x000003c;’,'&.#X3c’,'&.#X03c’,'&.#X003c’,'&.#X0003c’,'&.#X00003c’,'&.#X000003c’,'&.#X3c;’, ’&.#X03c;’,'&.#X003c;’,'&.#X0003c;’,'&.#X00003c;’,'&.#X000003c;’,'&.#x3C’,'&.#x03C’,'&.#x003C’, ’&.#x0003C’,'&.#x00003C’,'&.#x000003C’,'&.#x3C;’,'&.#x03C;’,'&.#x003C;’,'&.#x0003C;’,'&.#x00003C;’, ’&.#x000003C;’,'&.#X3C’,'&.#X03C’,'&.#X003C’,'&.#X0003C’,'&.#X00003C’,'&.#X000003C’,'&.#X3C;’, ’&.#X03C;’,'&.#X003C;’,'&.#X0003C;’,'&.#X00003C;’,'&.#X000003C;’,'\.x3c’,'\.x3C’,'\.u003c’,'\.u003C’ );
$right = array( ‘>’,'%.3E’,'&.lt’,'&.lt;’,'&.LT’,'&.LT;’,'&.#62′,’&.#062′,’&.#0062′,’&.#00062′,’&.#000062′,’&.#0000062′, ’&.#62;’,'&.#062;’,'&.#0062;’,'&.#00062;’,'&.#000062;’,'&.#0000062;’,'&.#x3e’,'&.#x03e’,'&.#x003e’, ’&.#x0003e’,'&.#x00003e’,'&.#x000003e’,'&.#x3e;’,'&.#x03e;’,'&.#x003e;’,'&.#x0003e;’,'&.#x00003e;’, ’&.#x000003e;’,'&.#X3e’,'&.#X03e’,'&.#X003e’,'&.#X0003e’,'&.#X00003e’,'&.#X000003e’,'&.#X3e;’, ’&.#X03e;’,'&.#X003e;’,'&.#X0003e;’,'&.#X00003e;’,'&.#X000003e;’,'&.#x3E’,'&.#x03E’,'&.#x003E’, ’&.#x0003E’,'&.#x00003E’,'&.#x000003E’,'&.#x3E;’,'&.#x03E;’,'&.#x003E;’,'&.#x0003E;’,'&.#x00003E;’, ’&.#x000003E;’,'&.#X3E’,'&.#X03E’,'&.#X003E’,'&.#X0003E’,'&.#X00003E’,'&.#X000003E’,'&.#X3E;’, ’&.#X03E;’,'&.#X003E;’,'&.#X0003E;’,'&.#X00003E;’,'&.#X000003E;’,'\.x3e’,'\.x3E’,'\.u003e’,'\.u003E’ );
$patterns = array();
foreach( $left as $l1 ) {
foreach( $right as $r1 ) {
$patterns[] = $l1 . ‘img src=”xss.js” /’. $r1 .”\n”;
}
}
$pattern = implode( ”, $patterns );
return $pattern;
// the end of definition: _callXSSCode()
}* 上記のコードでも、”&” と “#” のあいだにピリオドを挟んで、エンコードされてしまうことを防いでいます。上記のままコピペしても正しく動きませんので、ご注意ください。
このメソッドは、”_” で始まっていることでお分かりのように、プライベート扱いです。他のメソッドからしか呼び出せないので、適当なコントローラを作成して、index() メソッドから呼び出せばよいでしょう。
xss.js を作成しておき、alert() や window.open() を使って結果が分かりやすくなっていれば、フィルターの有効性をはっきりと確かめられます。ちなみに、上記のコードは HTMLPurifier には効果がありませんでしたが、僕の試した環境が Firefox 3 だったからかもしれません。エンコード上の問題はブラウザの実装に依存するため、このテストはできるだけたくさんのブラウザで試すことが望まれます(ちなみに、Opera 9.5 と IE 8 beta1 でも上記のコードは無効でした)。
最後に、試しに HTMLPurifier を使ってさきほどのベンチマークを取ってみると、”p” 1文字の処理をさせたところ所要時間は 587.4296 秒でした。こちらも、いちどフィルターを通した変数を使い回すようにした方がよさそうです。
