[PR]小規模ECサイトに最適なWAF、SiteGuard Lite

徳丸浩の日記


2009年09月14日 PHP以外では

_既にあたり前になりつつある文字エンコーディングバリデーション

大垣靖男さんの日記「何故かあたり前にならない文字エンコーディングバリデーション」に端を発して、入力データなどの文字エンコーディングの妥当性チェックをどう行うかが議論になっています。チェック自体が必要であることは皆さん同意のようですが、

  1. チェック担当はアプリケーションか、基盤ソフト(言語、フレームワークなど)か
  2. 入力・処理・出力のどこでチェックするのか

という点で、さまざまな意見が寄せられています。大垣さん自身は、アプリケーションが入力時点でチェックすべきと主張されています。これに対して、いや基盤ソフトでチェックすべきだとか、文字列を「使うとき」にチェックすべきだという意見が出ています。

たとえば、id:ikepyonの日記「[セキュリティ]何故かあたり前にならない文字エンコーディングバリデーション」では、このチェックは基盤ソフトウェアの仕事であって、アプリケーションでチェックするのはおかしいとしています。

一方、岩本隆史さんの日記「不正な文字列をどこでチェックすべきか」では、文字列を使う時(すなわち出力時)に文字エンコーディングの妥当性をチェックすべきだとしています。

ここで、そもそも、なぜ文字エンコーディングの妥当性確認が必要かという理由について考えてみると、

  • 不正な文字エンコーディングを利用したSQLインジェクションやXSSへの対策

というところから議論が始まっているようですが、いやいやそうじゃないだろうという突っ込みが入りそうです。より本質的な理由としては、以下のようなものが考えられます。

  1. 入力データがUTF-8(あるいは他の文字エンコーディング)であるというアプリケーション要件に対するチェック
  2. 内部データがUTF-8(あるいはUTF-16など)として妥当でないと、処理系が誤動作を起こすため

という理由が考えられます。1.に関連して、id:kazuhooku氏がはてなブックマークにて「UTF-8 を入れるべきフィールドに壊れた UTF-8 が渡されてきたら、そもそも受け付けるべきじゃない。8桁の郵便番号が入力されたら弾くのと同じ話」とコメントされていて、この意見に私も同意です。次に、2.について、現実の処理系として、Perl、Java、ASP.NET、Ruby、PHPについて、この問題の現状を復習してみます。

Perlの場合

大垣さんが参照されているところの、小飼弾さんの日記「perl - EncodeでXSSを防ぐ*1」をはじめ、小飼さんが繰り返し説明されているように、「入り口で decode して、内部ではすべて flagged utf8 で扱い、出口で encode する」という原則に従うことより、自動的に文字エンコーディングに対するバリデーション*2が働きます。これは、ファイル入出力などの場合も同様です。

一方、なんらかの理由で冗長なUTF-8となるバイト列がUTF-8フラグつきで入ってしまった場合でも、文字列比較などでは、最短形式に正規化した上で処理がなされている模様です。これについて、以下のコードで少し具体的に説明します。以下のコード(Perl5.8.8とPerl5.10.0で検証)について

$ cat uni01.pl
#!/usr/bin/perl
use strict;
use utf8;
use Encode 'encode';
# 非最短形式の'/'を強引に作成
my $s = pack("U0C*", 0xC0, 0xAF);
printf "is_utf8=%d\n", utf8::is_utf8($s);
print encode('utf8', "$s\n");
print $s eq '/' ? "eq\n" : "ne\n";
print $s =~ m#/# ? "matched\n" : "not matched\n";

pack("U0C*" ...) については、ここなどを参照下さい。UTF-8の内部形式をバイト列指定で強引に作成しています。このスクリプトの結果を予想できますか?

$ ./uni01.pl
is_utf8=1
/
eq
not matched

表示上は「/」となっていますが、ダンプするとこれは0xC0 0xAFがそのまま出ています。まぁ、エラーにしないのであれば、そうするしかないでしょうね。

注目すべきは、スラッシュとの比較と、正規表現でのマッチの結果です。比較においては「/」と同一、検索においては「/」にマッチしないという結果になっています。一貫性がないようにも見えますが、どうなるのが *正しい* 結果なのでしょうか。

比較において、(最短形式の)「/」と同一となるという結果は、いわば「安全サイド」の判断だと思います。こうしておけば、UTF-8の非最短形式の攻撃に対して、防御できる可能性があるからです。一方、正規表現の方はそうではなくて、たとえばディレクトリ・トラバーサル対策として、「/」や「.」をチェックしようとしても、このチェックをくぐり抜けてしまうことになります。

もっとも、Perlの場合、外部から取り込んだ文字列は全てdecodeするルールですので、その際に非最短形式のバイト列はU+FFFDに変換されるか、例外が発生するので、いずれにせよ、Perl5.8で導入されたutf8の仕組みをルール通り使っている限りは問題はおきません。詳しくは先ほど参照したこちらを参照下さい。

ここまでの内容を、入力・処理・出力の時点ごとに下表にまとめました。

入力時decode時にバリデーション
処理時何もしない。例外的に、比較処理などで正規化してから処理
出力時何もしない

Rubyの場合

Rubyの場合は、Ruby1.9にて文字エンコーディングの取り扱いが大幅に改善され、RubyでCGIプログラムを記述した場合早期に文字エンコーディングの検証が入るようになっています。たとえば、以下のような、クエリ文字列を受け取って表示するだけの簡単なプログラム。

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-
require "cgi"
cgi = CGI.new
is = cgi["p"]
ie = CGI.escapeHTML(is)
print "Content-Type: text/html; charset=UTF-8\n\n"
print "<html><body>"
print "inputdata = #{ie}</body></html>"

これに、%C0%AF(冗長な「/」)を指定すると、以下のようにCGI::InvalidEncoding例外が発生します。

$ ./test2.rb
(offline mode: enter name=value pairs on standard input)
p=%C0%AF
/usr/local/lib/ruby/1.9.1/cgi/core.rb:602:in `block (2 levels) in initialize_query': Accept-Charset encoding error (CGI::InvalidEncoding)
        from /usr/local/lib/ruby/1.9.1/cgi/core.rb:597:in `each'
        from /usr/local/lib/ruby/1.9.1/cgi/core.rb:597:in `block in initialize_query'
        from /usr/local/lib/ruby/1.9.1/cgi/core.rb:596:in `each'
        from /usr/local/lib/ruby/1.9.1/cgi/core.rb:596:in `initialize_query'
        from /usr/local/lib/ruby/1.9.1/cgi/core.rb:754:in `initialize'
        from ./test2.rb:3:in `new'
        from ./test2.rb:3:in `
' $

ブラウザから実行すると、500エラーになります。まぁ500エラーはあんまりなので、例外処理で捕捉することになりますが、ともかく文字エンコーディングの不正なバイト列による攻撃はできないことになります。

Webアプリケーションの入口でバリデーションが入ることは確認できましたが、無理矢理UTF-8の非最短形式を変数にセットしたらどうなるか、Perl同様のテストをしてみました。

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-
s = "\xc0\xaf"
puts s.encoding
puts s
puts s == '/' ? '==' : '!='
puts /\// =~ s ? 'matched' : 'not matched'

このスクリプトの実行結果は以下のようになります。

UTF-8
/
!=
./test2.rb:7:in `
': invalid byte sequence in UTF-8 (ArgumentError)

Perlの時とは異なり、==による比較では「等しくない」という結果で、正規表現によるマッチングでは例外が発生しています。この仕様は実用的なように思えます。セキュリティ対策のバリデーションやらエスケープの際には正規表現が多用されるので、そこで文字エンコーディングのチェックが入っていれば、セーフティネットとしての働きが期待できそうです。ともかく、RubyによるWebアプリケーション開発では、入口での文字エンコーディングチェックが働いていることになります。

Perl同様のまとめを行うと、下表のようになります。

入力時CGIクラスにてパラメタ取得時にバリデーション
処理時何もしない。例外的に正規表現にて例外発生
出力時何もしない

Javaと.NET

Javaと.NETの場合は、プログラム内部ではUTF-16が利用されており、外部から取り込んだ文字列は文字エンコーディングの変換が行われます。この過程で、文字エンコーディングの不正なバイト列はチェックされます。このあたりは以前ITproに書いたので、ここ(Javaの場合)ここ(ASP.NETの場合)を参照下さい。どちらも不正なバイト列はU+FFFD(Replacement Character)に変換されます。

U+FFFDをアプリケーション側でチェックしてエラーにした方がよいと思いますが、仮にエラーにしなくても、U+FFFDを悪用した攻撃は知られていないので、不正なバイト列を用いた攻撃は成立しないと考えられます。つまり、Javaと.NETは文字エンコーディングの不正なバイト列に関しては安全と言えます*3

PHPの場合

PHPの場合、内部処理に用いられる文字エンコーディングは特に規定されていないので、どんなバイト列でも扱うことができます。たとえば、UTF-8で処理している場合でも、UTF-8の非最短形式のデータも、変数に保持することは可能です。一方、文字列比較や正規表現でのマッチングにおいては、暗黙のうちにUTF-8の最短形式であることを要求しているため、非最短形式(冗長形式)のデータをうまく扱うことができません。これにより、ディレクトリ・トラバーサル対策として「..」や「/」をpreg_matchやmb_eregでチェックしていても、冗長形式のバイト列がマッチしないため、チェックをすり抜けることになります。

現実にUTF-8非最短形式のデータが悪さをするには、通常は別の文字エンコーディングに変換された後なので、mb_convert_encodingなどで文字エンコーディング変換した際に、不正なバイト列が捕捉されると考えられます。それでも、本来のチェックロジックをすり抜けて、「後でどこかで引っかかる」ことに期待するのは気持ち悪いし、安全でもないと考えられます。

従って、PHPを使う場合、データを入力した時点(ファイルなどからも含む)で文字エンコーディングの妥当性確認をするくらいしか、文字エンコーディングの問題に対する現実的な対策はないと考えます。

そもそも、どうあるべきか

ここまで見てきたように、PHP以外の言語(Perl、Java、.NET、Ruby)の最新版では、文字エンコーディングの不正をチェックする機構が組み込まれていて、これら言語の通常の使用の中で(プログラマが意識することなく)そのチェックが働くことが分かります。一方、PHPにおいては、このようなチェック機構は言語組み込みではなく、mb_check_encodingなどをアプリケーション(あるいはフレームワーク)側で呼び出してチェックしてやる必要があります。

あるべき論で言えば、id:ikepyonが書いているように、言語などで自動的に検証が働くべきだと考えます。文字エンコーディングの妥当性は、文脈に関係なく、自動的にチェックできることですから、わざわざアプリケーションでチェックを記述する必要性はないからです。

我々はどうすべきか

文字エンコーディングの問題だけに注目すれば、PHPを避けるのも一つの考え方かもしれません。すなわち、Perl、Java、.NET、Rubyについて言えば、この問題は既に「解決済み」の問題であり、これら文字エンコーディングに関して安全な言語を選択するという考え方です。前提として、最新の処理系を使用すること、Perlに関しては、use utf8;によるモダンな書き方をすることは必須です。

PHPに関しては、PHP6で内部の文字エンコーディングがUTF-16になり、IBMのInternational Components for Unicode(ICU)が組み込まれるとのことですので、PHP6に至ってようやくJavaやPerl並になると予想されます。しかし、当面PHP6が出てくる気配はなさそうですので、PHPプログラマがとれる道は次の二つだと考えます。

  1. PHPをあきらめ、RubyやPerlを使う
  2. プログラムの先頭で文字エンコーディングの妥当性をチェックできるフレームワークやライブラリを利用する

大垣さんは、おそらく後者を主張されているのでしょう。過去に、大垣さんは「なぜPHPアプリにセキュリティホールが多いのか?/第27回 見過ごされているWebアプリケーションのバリデーションの欠陥」の中で、以下のようなバリデーションプログラムを例示されています

<?php
function _validate_encoding($val, $key) {
    if (!mb_check_encoding($key) || !mb_check_encoding($val)) {
        trigger_error('Invalid charactor encoding detected.');
        exit;
    }
}
$vars = array($_GET, $_POST, $_COOKIE, $_SERVER, $_REQUEST);
array_walk_recursive($vars, '_validate_encoding');

ごらんのように、全てのGETとPOSTの変数、サーバー変数などを十把ひとからげにチェックするものです*4。便利そうですが、$_SERVERまでまとめて文字エンコーディングのチェックをしていいのかという疑問が出ますし、大垣さん自身の以下のコメントと矛盾しそうな気がします。

RoRの脆弱性に関連してRuby1.9では安全、と解説されていますが、それはRuby1.9は不正な文字エンコーディングを受け付けないからです。個人的には明示的にバリデーションコードで検出する方が良いと考えています。クライアントからの入力は文字列のみでなくイメージ等のバイナリも含まれるからです

[何故かあたり前にならない文字エンコーディングバリデーションより引用]

私なら、もう少し明示的に、以下のようなライブラリ関数を用意するか、同等の機能をフレームワークに組み込む、あるいは類似機能を持つフレームワークを選択するでしょう。

// 文字エンコーディング、文字種、文字数のチェック
function checkValue($val, $pattern) {
  if (! mb_check_encoding($val, 'UTF-8')) {
    trigger_error('文字エンコーディングの不正');
    exit;
  }
  // 文字エンコーディングの変換(必要があれば
  // $val = mb_convert_encoding($val, 'UTF-8', 'UTF-8');
  if (! mb_ereg($pattern, $val)) {
    trigger_error('文字種または文字数が範囲外です');
    exit;
  }
  return $val;
}
// 文字エンコーディング、文字種などのチェックつきパラメタ取得
function getGETvalue($key, $pattern) {
  return checkValue($_GET[$key], $pattern);
}
//呼び出し例(制御文字以外で10文字以下)
$p = getGETvalue('p', "\\A[[:^cntrl:]]{0,10}\\z");

ご覧のように、文字エンコーディングだけでなく、文字種と文字数のチェックも同時に行っています。文字種のバリデーションも同時に行える「便利な関数」を提供することにより、文字エンコーディングの問題をプログラマに意識させなくても自動的にチェックできるようにしています。これくらいのものを用意しないと、アプリケーションでの文字エンコーディングチェックを定着させるのは難しいでしょう。上記で出てきた正規表現などの説明はこちらを参考にしてください。

まとめ

文字エンコーディングの妥当性チェックについて、現在Webアプリケーション開発によく用いられるPerl(5.8以上)、Ruby1.9、Java、ASP.NET、PHP5.Xについて現状と、あるべき使い方について検討しました。

全ての言語において、変数に文字エンコーディングとして異常な値が入ると、その後の処理(文字列比較、正規表現による検索など)が異常になる場合があります。その意味で、入り口(データを変数に格納した時点)での文字エンコーディングの妥当性確認は必須です。別の観点から言えば、アプリケーション要件として(セキュリティとは関係なく)文字エンコーディングの妥当性確認をする必要があります。

前述のように、PHPを除く言語(プラットフォーム)では、既に文字エンコーディングのバリデーションは言語自体で行われることが当たり前になっており、アプリケーションプログラマが意識しなくても、自然な形でバリデーションが行われます。一方、PHPにはそのような機能がまだ組み込まれておらず、当面ライブラリやフレーワークの工夫により文字エンコーディングのチェックを確実にする必要があることを指摘しました。

以上の議論から、大垣さんのブログタイトルは、以下のようにされた方がより正確でありましょう。

何故か *PHPでは* あたり前にならない文字エンコーディングバリデーション

続きはこちら→「文字エンコーディングバリデーションは自動化が望ましい

*1 私のITpro連載に対する補足を頂いた内容となっています

*2 U+FFFD(Replacement Character)への変換も含めます

*3 先に書いたように、Java SE6 Update 10までのJava実行環境は、UTF-8の非最短形式を許容していたため、アプリケーションの記述方法によってはこの影響を受ける場合がありました。Java SE6 Update 11以降であれば問題ありません

*4 $_REQUESTは、PHPマニュアルによると、「$_GET、 $_POST そして $_COOKIE の内容をまとめた連想配列です」

本日のツッコミ(全12件) [ツッコミを入れる]
_ 通りすがり (2009年09月16日 02:33)

あなたの述べていることは正しいが、PHPに対しての嫌悪を感じる。<br>例えば、PHPでも入口・出口でのチェック機能はある。しかし、<br>Perl、Ruby、.NET、Javaではその点を書いているのに、PHPでは書かず、まるで文字コードチェックが一切ないように読める。

_ kayes (2009年09月16日 03:57)

私も通りすがりですが、「そもそも、どうあるべきか」の項を読むとわかると思いますが、PHP以外の言語は「プログラマが意識しなくても自動的にチェックする仕組みを持っている」のに対し、PHPは「プログラマが意識してチェックしないとチェックされない」ので、他と区別して書かれているのだと思いますが。<br><br>すべてのプログラマが意識して書けるなら、それほど嬉しいことはないですよね(笑)。

_ Ruby使い (2009年09月16日 10:25)

Ruby1.8についても解説してください。

_ 通りすがり (2009年09月16日 18:09)

> PHP以外の言語は「(略)」のに対し<br>ここに挙げられている言語がWebアプリで使われる全ての言語ではない。<br>例えば、CやC++にはない。付け足せば、PHPやPerlなどのCモジュール内部で起こった不正な文字はスルーされうる。<br>よって、「Perl、Java、.NET、Ruby、PHPの中では」と書けば筋は通るが、「PHP以外では」は誤り。<br>そしてそんなことを、PHPの(脆弱性撲滅に注力している)開発者に言ったら、喧嘩を売られたと受け止められて当然。<br>最も、大垣さんの記事にも少なからず問題があるが。

_ otsune (2009年09月16日 20:09)

>そしてそんなことを、PHPの(脆弱性撲滅に注力している)開発者に言ったら、喧嘩を売られたと受け止められて当然。 <br><br>「現実的にこれこれこういう問題がPHPには有りますよ」と主張しているんだから、そう受け止められても何の問題も無いんじゃないかな? 根拠も無くFUDを書いてる訳じゃないんだし。<br>もしかして「たしかに今は問題が有るけど、いろいろ頑張っているから努力を認めて見逃して」みたいな役に立たない人情を最重視しているのでしょうか?

_ 通りすがり (2009年09月16日 22:30)

それはあなたの早とちり。<br>そこの1行だけではなく、文章全体から主張をくみ取ってほしい。<br>初めの書き込みから、書き方が公正でないと言っている。<br>(PHPに関して冒頭部分に結論がひっくり返る恐れのあるほどの誤りが見られるが、それはもう大垣さんが指摘しているので私は突っ込まない。お二人で議論してすれ違いを解消して欲しい。)<br><br>大垣さんは、文字エンコード問題を*技術者に広く知らせる*ためにあの記事を書いた。大垣さんの2回目の記事からもそれが読み取れる。<br>そこに徳丸さんが、PHPではUTF-8の非最短形式のチェックができない。*PHPだけが文字エンコード問題をいまだに持っている*と主張した。<br>ここで論点がずれている。お互い最も伝えたいことの次元が違っている。<br>結果、大垣さんは「PHPプログラマだけが文字エンコードの問題を知っていればいいと言うのか」と怒った。<br>元の大垣さんの記事とは別物として書けば問題なかった。ブログのタイトルが不適切と書かずに、単にPHPにこんな問題があると書けばよかった<br>加えてPHPにある文字コード関連のチェック機能も他言語と同じように紹介しないと不公平で誤解を与えると私は主張している。<br>これではPHPそのものが駄目と読み取られかねないし、実際そう読み取られた。条件反射的に批判する大垣さんには感心できないが、原因はそれだけではなかったと言いたいから、こうやって初めて書きこませてもらった。<br>セキュリティの甘さに情けをかけるつもりは全くない。

_ otsune (2009年09月17日 05:16)

通りすがりさん。<br>主張自体はわかりましたが、じゃあどうして細かい根拠を書かずに「喧嘩を売られたと受け止められて当然」と余計なことを書いて、説明をはしょったのかが理解できませんでした。<br><br>喧嘩を売ってるだとか態度が悪いだとか口調が気に喰わないだとかみたいな話は、この脆弱性の論点そのものとは全然関係ないんじゃないかなぁ。反論が有るのなら書いてくれればいいのになぁ。と、思ったのでツッコミしました。

_ 通りすがり (2009年09月17日 14:00)

説明をはしょったのは私のミス。<br><br>喧嘩を売ってるとも態度が悪いとも書いていないし、そんな風には思わない。<br>そう思われかねないと言っている。なぜなら、書き方が不公平だから。<br>人格攻撃している訳ではない。<br><br>「PHPの場合(略)規定されていないので」<br>この行で、徳丸さんはPHPの文字コード関連について分かってないのではないかと思った。<br>mb_internal_encoding()がPHP5では標準であるのだから。<br>ただ、私はそれぞれの言語に詳しくないし(しかもすでに嘘を書いている。詳細はトラックバック)、もう大垣さんが指摘しているので、別の視点から書いた。<br>otsuneさんは脆弱性議論として見られているのかもしれないが、私はそうではない。「公平に書いて。でないとこの記事は攻撃的に見えるよ」という、脆弱性の論点以前のもの。

_ otsune (2009年09月18日 15:00)

なるほど。わかりました。<br>技術的な観点で脆弱性の話をしているblogエントリーに、なぜか技術的な観点 *以外* の個人的な価値観による苦言(感想? 提言?)を、まったく前提説明無しでいきなりコメントしただけなんですね。<br><br>文脈を大きく変えるときは、変える方が「これは技術的な話ではないですが、(略)喧嘩を売られたと受け止められて当然。」などと、明確にわかりやすく誰が読んでも違う文脈だとわかるように書くもんだと思い込んでましたが、「(書いてないけどコメント全体の雰囲気から脆弱性の論点以前の話をしてるのだと)理解してほしい」と無茶ぶりする人が出現する可能性を全く考慮してませんでした。

_ 通りすがり (2009年09月18日 18:13)

私が説明不足だったのは本当だ。私の責任。「PHP以外では」問題と「主張ずれ」問題(と「内部エンコードなし」問題)をすりかえたのだから。<br>しかし、あなたも読解力不足。「まるで文字コードチェックが一切ないように読める」と最初に書き方の問題点を指摘しているのに、脆弱性の論点以前の話と分からなかった。結果的に大垣さんと同じパターンだ。「脆弱性の論点そのものとは全然関係ない話題」は不適切な表現とその誤解から生まれる。<br>苦言?感想?この記事を読んだ人が誤解しないようにするために書いたのだからそれは誤り。「ツッコミ・コメントがあればどうぞ!」だからツッコんだだけ。<br>私も自らの誤りを認めたにも関わらず、あけすけに汚い表現を連ねて粘着される人が出現する可能性は全く考慮していなかった。今回徳丸さんも大垣さんも、あなたのような言い方は全くしなかった。<br>お互い譲りそうにないし、完全に記事の趣旨から外れたし、徳丸さんが大垣さんに伝えたい具体的なPHPの問題点を、次の記事に誤解を生まない表現でとても分かりやすく書かれたし、これ以上のあなたとの議論?は技術的議論を望む人には無意味だ。徳丸さんに迷惑であるしまだ続けたいなら別所に移るべきと思うがどうか。

_ otsune (2009年09月19日 02:46)

>苦言?感想?この記事を読んだ人が誤解しないようにするために書いたのだから<br><br>まぁ私の目的は「喧嘩売られてる? それが具体的に何の関係があるんだろ」という疑問を解消するためですから「誤解を解消するという啓蒙行為のが目的なのに、なぜか啓蒙行為にはまったく効果がない具体的な事を書かずに論点以前の話を分かりにくく書く」という状態さえ無くなればそれでいいんですよね。<br>「読解力不足」なのは全ての読者に取っては当然のことで、文章を書く人が他人に対してそれを言ってもしょうがないですしね。<br>(誤解されるblogだからツッコミをした。と書いている同じ人が、自分のコメントの説明不足を読解力不足というのは高度な自爆ギャグだとしか思えませんが)

_ 通りすがり (2009年09月19日 11:08)

私の主張を理解して頂いたようで嬉しいです。



[PR]小規模ECサイトに最適なWAF、SiteGuard Lite

ockeghem(徳丸浩)の日記はこちら
HASHコンサルティング株式会社

最近の記事

最近のツッコミ

  1. 通りすがり (09-19)
  2. otsune (09-19)
  3. 通りすがり (09-18)
Google