セキュリティ専門家でも間違える!文字エンコーディング問題は難しいのか?

(Last Updated On: 2018年8月13日)

一見徳丸さんのブログは分かりやすいように思えますが、それは単純な実験により分かりやすいように見えるだけで複数の間違いがあります。

その間違いとは

  • 意図の取り違い – 誤読
  • 言語の仕様と実装の理解不足
  • HTTPやPHP仕様の理解不足
  • セキュリティ対策をすべき場所の理解不足

です。(※0)

徳丸さんは非常勤とは言え、国の出先機関の研究員であるし、その出先機関は職務放棄とも言える文書(「例えば、PHPを使用しない」と勧める文書)を公開している(いた?)のでしっかり反論しておく必用がありますね。IPAのあの文書は職務放棄と言える文書だと思っています。これについても後で意見を述べます。

 

意図の取り違い – 誤読

最初の間違いは私のブログのエントリ「何故かあたり前にならない文字エンコーディングバリデーション」に対する理解です。特にPHPユーザに対して文字エンコーディングバリデーションを実行して欲しいという思いからこのタイトルになっています。他の言語は他のブロガーが書くべきと思ってもいたからです。しかし、中身を読めば私の意図はシステム全般に対して文字エンコーディングを厳格に処理しなければならない、と主張している事は分かるはずです。

未だに間違いが多いHTTPプロトコルレベルでの文字エンコーディングの取り扱いや、データベースの文字エンコーディングでの取り扱いにも十分に注意するように書いています。データベースに至っては、デフォルトの文字エンコーディングがバイナリかそれに近いエンコーディングに設定されたサーバも少なくありません。これが如何に危険な事であるか、環境やコードによっては致命的であるかは、ここなどを参照してください。

アプリケーションを含め、システム全体が文字エンコーディングを厳格に取り扱わないと十分なセキュリティは確保できません。だから、アプリケーションプログラマもデータベースやXML処理系の様に文字エンコーディングを厳密に取り扱わなければならないと考えています。こう考える理由は、最後の「セキュリティ対策をすべき箇所」を読めば理解できると思います。

私のブログの読者層を考慮したエントリなのでWeb開発者を意識したエントリになっていますが、本当にお願いしたかったのは、すべての開発者(Webに限らず)に文字エンコーディングバリデーションを行う事です。多重セキュリティを実現すべきということです。現在では多重のセキュリティの必要性は説明する必用ないでしょう。

 

言語の仕様と実装の理解不足

徳丸さんは言語の仕様と実装を十分理解しないで、簡単な実験による結果のみでこのブログのエントリを「何故か”PHPだけは”あたり前にならない文字エンコーディングバリデーション」とのタイトルにすべきだろうと間違った評価をしています。

PHP 4.2からmbstringはデフォルトモジュールとなりました。デフォルトモジュールすべきと提案したのは私だったのでよく覚えています。mbstringにはインプットを自動的に内部文字エンコーディング変換する機能とアウトプットに指定した文字エンコーディングを自動変換する機能を持っています。現状の良くある”日本のPHP環境”ではPerlよりは随分マシな状況です。

徳丸さんのPerlのサンプルコードは以下のようなコードです。

#!/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";

これと同等のPHP 5.2のサンプルコードは次のようなコードになります。

[yohgaki@dev tmp]$ cat -n tt.php
     1  <?php
     2  mb_internal_encoding('utf-8');
     3  ini_set('mbstring.strict_detection','on');
     4  $s = pack("C*", 0xC0, 0xAF);
     5  echo $s = mb_convert_encoding($s, 'utf-8'), "\n";
     6  echo $s == '/' ? "eq\n" : "ne\n";
     7  mb_ereg('/', $s) ? print "matched\n" : print "not matched\n";
     8  ?>
[yohgaki@dev tmp]$ php -v
PHP 5.2.10 (cli) (built: Jul 22 2009 23:50:59)
Copyright (c) 1997-2009 The PHP Group
Zend Engine v2.2.0, Copyright (c) 1998-2009 Zend Technologies
[yohgaki@dev tmp]$ php tt.php

ne
not matched

徳丸さんが簡単な実験で明らかにしている”自動”(?)バリデーションはPHP4の頃、mbstringがデフォルトモジュールになる前、PHP3から出来ているのです。また、この動作は徳丸さんも納得できる仕様と指摘しているRuby 1.9の動作と同じです。かなり古いPHPで今と同じように動作していたかは怪しいですが、とにかく10年以上前の90年代からこのように動作していました。

PHPの場合はPerlよりもっと簡単で、コードを一切書き換えなくてもphp.iniの設定をするだけで”自動”バリデーションが行えます。これはPerlの仕様よりも「随分マシ」ですね。しかも、UTF-8以外にもEUC-JP、EUC-TWなどのエンコーディングでも動作します。(注:必ず追記を参照!)

mbstring.input_encodig = AUTO
mbstring.internal_encoding = UTF-8

追記:現在(2016)のWebクライアントはHTTPヘッダーで指定した文字エンコーディングを使用します。input_encodingをAUTOにする必要は全く無く、自動的にサニタイズ(お勧めしないですが)するならUTF-8などを指定します。

Perl 5.8> のエンコーディングチェック機能だけでは不十分であることは明らかです。Perlは関数や正規表現でUTF-8として扱うだけで壊れた文字列は”そのまま変数に保存”されます。PHPの場合、壊れた文字列の文字は指定された文字に”変換され保存されません”。Perlよりは「随分マシ」と言えます。Ruby 1.9場合は文字列として扱う関数などで例外が発生するのでPerl, PHP比べれば「随分マシ」です。しかし、十分に安全と言うには不十分しょう。Ruby 1.9の仕様をもって文字エンコーディングのバリデーションで十分とするのは、サニタイズで十分と主張することと同じ種類の危惧を感じます。これは次の項の解説を読むと理解できると思います。

ところで、tDiaryをRuby 1.9で運用している知人が居ます。いろいろ苦労しているようです。現実の世界ではRuby 1.8が当たり前でしょう。Ruby 1.8はPerlとあまり変わらない状況と言えるでしょう。セキュリティはリアリズムが必用だと思っています。

Ruby 1.9のようにエンコーディングに厳格なシステムでも任せきりにできないことは次に説明します。ちなみにPHP6はRuby 1.9と同じ様に様々な場所で不正なエンコーディングが検出されるようになります。

 

HTTPとPHP仕様の理解不足

先ほどの項目で

PHPの場合はPerlよりもっと簡単で、コードを一切書き換えなくてもphp.iniの設定をするだけで”自動”バリデーションが行えます。

と自分自身で書いていながら何故、文字エンコーディングのバリデーションが必用なのか?と疑問に思った方も居ると思います。PHPはファイルアップロードを行う場合にフォームに

enctype=”multipart/form-data”

を指定すると入力エンコーディングに自動変換は動作しない仕様になっています。enctypeが違っても普通のフィールドも送信できます。もし文字エンコーディング変換を行うなら、これらのフィールドには明示的な文字エンコーディング変換が必用です。つまり、明示的なバリデーションが必用になります。

ファイルアップロードを行わなくても通常のGETやPOSTでも%エンコーディングでバイナリも送信可能です。バイナリを処理したい場合は多くは無いはずですが、バイナリ送信もできるのですから送りたい時もあるでしょう。そういった場合にも、Ruby 1.9やPHPの自動エンコーディングチェックは困った機能になってしまいます。両者とも”自動”のチェックは無効にする方法があります。

徳丸さんも$_SERVERを無差別に文字エンコーディングをチェックの問題点には気づかれたようです。サーバの環境変数も$_SERVERに含まれ問題になる事があります。HTTPは所謂ASCIIくらいしか考慮に入れてなかった仕組みなのでユーザエージェントには7bitASCIIのプリンタブルな文字しか設定すべきではありませんが、世の中にはISO-8859-*とかUTF-8とかを使っているブラウザもあるかも知れません。不要な要素がチェックに引っかかる可能性を指摘しているにも関わらず、任意でバイナリを送れる事実に気が付かないのは理解に苦しみます。(※1)

enctype=”multipart/form-data”が指定された場合に文字エンコーディング変換が動作しないことはPHPの仕様ですが、文字エンコーディングバリデーションはシステムに任せきりで大丈夫、という姿勢ではセキュアプログラミングを教える場合には問題ありと言わざるを得ません。

こういう教え方をすると「SQLインジェクション対策にプリペアードクエリ」とだけ教えられたプログラマが間違えることと同じ間違いをしてしまう可能性があります。未検証のパラメータからテーブル名やフィールド名を指定したプリペアードクエリを実行したり、WHERE句の部分(特に多いのがIN句)にパラメータを書き込んでプリペアードクエリを実行するなど、SQLインジェクションの原理を知っている人には当たり前でも「プリペアードクエリを使えば大丈夫」と盲目的に教えられたプログラマなら仕方ありません。

徳丸さんのブログを読んだ人には「Perl 5.8, Ruby 1.9以上なら何もしなくて大丈夫」と盲目的に信じてしまった人もいるのではないでしょうか? 安全性の優劣でなく、脆弱さの優劣を競っても無意味ですが実際にはPerlの方がPHPより脆弱であるにも関わらず、真反対であると理解した人が多いのではないでしょうか?

 

セキュリティ対策をすべき場所の理解不足

次の図はWebアプリケーションが動作するコンポーネントです。赤い×印が不正な文字エンコーディングを利用した攻撃が可能となる箇所です。白い線は入出力の可能性がある部分です。

フレームワークを利用していてもフレームワークの仕組みを意図的にバイパスしたり、意図的でなくてもバイパスしてしまう事は良くある事です。

この図の中で、文字エンコーディングをバリデーションする為に一番相応しいコンポーネントはどれでしょうか?まさか、Webブラウザなんて人は居ないですよね?ブラウザでは無理でしょう。エンコーディングがちょっとおかしいだけでページを表示しないブラウザなんて使ってもらえません。サーバ側のWebアプリ周辺のコンポーネントは頑張れば、何とかなるかもしれないですがブラウザだけは制御しようが無いです。

最も確実にチェックできるのは、中心に居るWebアプリです。

フレームワーク? フレームワークも頑張ればそこそこ行けるでしょう。開発環境である以上、フェールセーフには出来てもフールセーフには出来ません。(バカバカしいコード対策は無理)

CMSでも設定によっては簡単にバイパスできる物もありますが、どうしてもフールセーフフレームワークが欲しい方はCMSなどを最小限のカスタマイズで利用した方が良いと思います。

 

まとめ

徳丸さんのブログを見てびっくりすると同時に失望しました。徳丸さんがWebの基礎的な技術仕様や言語の仕様、実際の現場での利用方法などを理解していないで評論していることが分かったからです。PHPconのビジネスデイでは私も同じように思っていた「良い事」を沢山言われていたのに残念です。

徳丸さん風に書くとすれば、このエントリは「何故か”セキュリティ専門家”でさえ理解してない文字エンコーディング問題」とすべき、といったところでしょうか。

 

IPA文書の問題

どなたが書いたのか知りませんが、IPAの文書に「例えば、PHPを使わない」というセキュリティ対策を例示している文書があります。文字エンコーディングベースのXSS対策については全くダメなアプリばかりが現状のRubyやPerlについても「例えば、Ruby, Perlも使わない」と書くつもりなのでしょうか?

個人ブログでどの言語は良く無い、使うべきでない、使うに値しない、と書いても何の問題ありません。しかし、IPAは公的要素が強い独立行政法人です。そこの文書でセキュリティ対策として「例えば、○○は使わない」と書いて良いものでしょうか?

例のIPAの文書は職務放棄の文書だと言っても良いでしょう。Perl、Ruby、Python、PHPも作る人が作れば安全なアプリは作れるのです。

PHPにCVEが多かった理由はデフォルトモジュールとバンドルしていた物が比較的多かった事と共有サーバでsuExecなのどの仕組みを利用せず、Apacheモジュールを共有していたホスティング会社が多かった為、本来はフェールセーフ機能であるセーフモードのすり抜けがCVE登録された事にあります。PHPプロジェクトのセキュリティに対する意識の問題もありました。随分マシにはなりましたが、不満は沢山あります。しかし、これはIPAにとって「使わない」と勧める理由にはなり得ません。

PHPアプリに初心者が作るようなセキュリティホールが沢山あったのは、本当に初心者が沢山居たうえ、大量のアプリを作ったからです。また、PHPが簡易フレームワークの役割をしており時期的に本格的フレームワークを利用したアプリが少なかった事も理由の一つです。

IPAの職務に詳しくはありませんが、初心者がセキュリティホールを作るようなソフトウェアの利用を勧めない事が職務なら、真っ先にWindowsXPやFlash, PDFの利用を止めるように勧めるべきでしょう。

IPAの職務とはユーザや開発者がコンピュータをより安全に利用する為の方法を啓蒙する事ではないのでしょうか?プロダクトの利用を中止する勧める、しかも間違った認識に基づき「例えば、PHPを利用しない」などと文書に記載するのは職務放棄ではないでしょうか?

私の言っている事の説明が必要なのであればいつでも呼んで下さい :)

 

おまけ

以前、大阪?東京?でお会いした時、徳丸さんは私のWebアプリセキュリティ対策入門に書いていたSQLインジェクション対策が気に入らなかったらしく、全てのパラメータを文字列として扱いエスケープする方法ではなく、

(int)$number

のようなキャストであるべきと言われていました。「技術者なら気持ちが悪い」といった感想をお持ちのようで「技術者としてのセンスが悪い」といった旨の事を言われていたようです。

それから、全て文字列にすると遅くなる、などと言う理由も挙げられてました。しかし、速度を求めるならプリペアードクエリを使うべきです。クエリ文字列をダイナミックに生成する必要などありません。

何故間違っているか簡単に解説したのですが理解されていたのかどうか分かりませんでした。間違いを指摘されても、簡単に「はい、そうですね」とは言えない状況なので深追いはしませんでしたけど、どうなんでしょう?

キャストする方法は間違っている場合も多く、注意しなければ問題の原因になります。しっかりしたDB管理者ならDBの整数を言語の整数型等でキャストする危険性を十分理解していると思います。簡単な例を挙げます。DBのID型などは符号無し64整数であるのに、言語は符号付き32整数であることが多いです。64bitプラットフォームでも符号付きであることが多いです。つまり、キャストは意味的に正しくないことが多いのです。このような方法は最初に教えるべきではなく、基礎を理解した上でどちらでもよいTips程度に薦めることが妥当だと思います。このような問題はDB管理者で無くても普通のCプログラマなら知ってて当たり前でしょう。C/C++でプログラムはされた事はない?DBには任意精度型もあるので普通の言語でキャストしても思い通りならない事を前提とすべきです。

データベースアクセスの抽象化ライブラリなどでプリペアードクエリをサポートしないDBでも、プレイスホルダが使えるようになっています。メタデータを参照してキャストしていると思いますか?まだ疑問に思える方は各言語のDBアクセスの抽象化ライブラリの実装などを参照すると良いです。

そもそも自分でコンパイラなどを実装した事がある人であれば解る話です。SQLパーサを普通に実装すればパラメータはリテラルであれば良いことにするでしょう。リテラルには文字列、整数、任意精度整数、日付などが含まれると定義するはずです。SQLは方言の固まりでもあるので実装によっては使えない物もあるかも知れませんが、広く使われているDBMSの場合は問題ありません。

技術的知識を背景に最適な対策は何であるか?合理的でセキュアな対策は何であるか?選択する事はそれほど難しくないと思うのですが….

あまり身の無い議論は不毛なのでやる事やりましょう!

(※0)まっちゃさんが横から眺めて喜ばれそうな展開です。しかし、今回はブログで決着が着く話ですね。

(※1)実際に私が使った例をあげると、多数のフォーム入力でバリデーションを最適化するために、前のフォームでバリデーションした結果を圧縮してメッセージダイジェストを取り、改ざん防止をしてフォームに埋め直すという処理等を行うコードがあります。普通のサイトには必要ないですが、これはネットワーク負荷低減とパフォーマンス向上に役立って便利な事があります。

(※2)この記事を書くためにRFC1869を検索したのですが、最初に見つかった文書がHTTPヘッダと文書のエンコーディングが一致していない事には思わず苦笑いしてしまいました。こういうページがあるからブラウザのデフォルト設定が文字エンコーディングを自動認識するようになってしまうのです。自動認識を有効にすると文字エンコーディングベースのXSSにより脆弱になるのですけどね。

重要:現在のWebクライアントはHTTPヘッダーで指定した文字エンコーディングを使用します。input_encodingをAUTOにする必要は全く無く、自動的にサニタイズ(お勧めしないですが)するならUTF-8などを指定します。UTF-8を使う場合、現在(2016)のお勧めの設定は「デフォルトのまま何もしない」です。PHP 5.6以降のデフォルトはUTF-8になっています。入力文字列の文字エンコーディングは自分でバリデーションします。

追記:「PHPで手っ取り早く対策するには?」と聞かれたので書いておきます。エントリ中にも書いていますが、UTF-8の場合は

mbstring.strict_detetion = on
mbstring.http_input = auto
mbstring.internal_encoding = utf-8
mbstring.http_output = pass
mbstring.encoding_translation = On

とします。(strict_detectionを入れ忘れていたので追加しています)

php.iniや.htaccess、httpd.confでphp.iniの値を変更します。php.iniで変更した場合、多少自由度が落ちます。バイナリで受け取りたい場合、formを作ってenctype=”multipart/form-data”にしなければなりません。(apacheを使っているなら.htaccessやhttpd.confでオーバーライドも可能です)

基本的に入力データをサニタイズしているのと同じです。個人的には

mbstring.strict_detetion = on
mbstring.http_input = pass
mbstring.internal_encoding = utf-8
mbstring.http_output = pass
mbstring.encoding_translation = On

として、すべてのパラメータをしっかりバリデーションし不正な入力はログして、プログラムを停止するようにして欲しいと考えています。

ユーザエージェントなどを表示することは比較的よくあると思いますが、$_SERVER,$_ENV,$_FILESは一切エンコーディング変換されません。注意してください。

他にも考えなければならないケースがいろいろあります。exifなどにも不正な文字エンコーディングが含まれていて攻撃される可能性があります。id3などに含まれる情報も同じです。アップロードされたファイルの中身を読む(CSVなど)場合もバリデーションが必用です。

トータルでセキュリティを考えるなら、文字エンコーディングのバリデーションは自分で行うことを基本とした方がより安全なWebアプリを作れるようになります。exif, id3等でのJavaScriptインジェクション、SQLインジェクションにも当然注意してください。

追記2: バグっぽいと勘違いしたのはPHPのじゃなくてスクリプトのバグでした。バグ以外に古いほうのスクリプトを貼っていたようなので直しました。実行結果は変わりません。

追記3: mbstring.encoding_translation = On(デフォルトoff)を記述する事を忘れてました。これが無いと変換されません。

投稿者: yohgaki