SQLインジェクション対策はおすみですか?
開発開始時点からのコンサルティングから、公開済みWebサイトの脆弱性検査、
脆弱性発見後の適切な対策まで
トップ «前の日記(2009-08-31) 最新 編集
過去の日記

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 の内容をまとめた連想配列です」

本日のリンク元
アンテナ
その他のリンク元
検索

SQLインジェクション対策はおすみですか?
開発開始時点からのコンサルティングから、公開済みWebサイトの脆弱性検査、
脆弱性発見後の適切な対策まで
トップ «前の日記(2009-08-31) 最新 編集