これからのプログラムの作り方 – 文字エンコーディング検証は必須

Security 6月 12, 2006
(Last Updated On: 2016年3月3日)

最近PostgreSQL、MySQL両方にSJISエンコーディングを利用している際のエスケープ方法の問題を修正がリリースされています。この件は単純に「データベースシステムにセキュリティ上の脆弱性があった」と言う問題ではなく「アプリケーションの作り方を変える必要性」を提起した問題です。

参考:セキュアなアプリケーションのアーキテクチャ – sandbox化

PostgreSQL、MySQLの脆弱性は特にSJIS等、マルチバイト文字に\が含まれる文字エンコーディングが大きな影響を受けますが、同類の不正な文字エンコーディングを利用した攻撃方法が他の文字エンコーディングでも可能です。例えば、UTF-8エンコーディングは1文字を構成するバイト列の最初のバイトの何ビット目までが1であるか、を取得してUTF-8文字として1バイト~6バイト必要なのかわかる様になっています。アプリケーションやライブラリによってUTF-8文字列の解釈の方法が異なると問題が発生する可能性があります。最も単純な例としてはUTF-8文字の中にASCIIの特殊文字を埋め込む方法が考えられます。この壊れたUTF-8文字列をASCIIとして処理してしまうと様々な問題が発生してしまうことは容易に想像できます。

今後は全てのセキュアなアプリケーション、ライブラリで文字エンコーディングが妥当であるかチェックすべきです。壊れた文字がある場合は削除してはいけません。安易な他の文字への変換も脆弱性を生む場合があります。

これからプログラマは常識として以下の項目を実践すべきと思います。

  • 文字エンコーディングが妥当であるか必ず入力検査時にチェックする
  • 壊れた文字エンコーディングを見つけた場合はエラーにして処理を中止する(当然エラーを記録する)
  • 壊れた文字は「絶対」に削除しない
  • 壊れた文字を置き換える場合は安全性に十分注意する(非推奨)
  • システムで利用する文字エンコーディングはできるだけ統一する(努力目標)

PHP 5.1.4, PHP 4.4.3からはmb_check_encoding関数が文字エンコーディングの妥当性をチェックするための関数として追加されました。この関数は配列を引数に取れないのでarray_walk関数などと一緒に利用して$_GET,$_POST,$_COOKIE,$_SERVER等の値をチェックします。PHP5.1.4, PHP 4.4.3より古いPHPではmb_convert_encoding関数を使用し、変換前と変換後の文字エンコーディングに同じ文字エンコーディングを指定し、変換前と変換後の文字列に変化があるかを確かめる事により不正な文字エンコーディングを検出できます。
(注:PHP 4.4.3は現時点ではリリースされていません。そして、今mb_check_encoding()はマニュアルに記載されていないことにも気が付きました。(汗 これくらいは自分ですべきと思いますが時間が… 申し訳ないです > 関係先各位 )

Cなどのアプリケーションやライブラリの場合、libiconvを使えば不正なエンコーディングをerrnoを利用してチェックする事が可能です。(PHPの場合もiconvモジュールを使って同様のチェックも行えます。mbstringが利用できない場合はiconvモジュールを利用すると良いでしょう)

以前から文字化け対策としてよく言われていた事ですが、セキュリティ面からもシステムとして利用する文字エンコーディングはできる限り統一した方が良いです。UTF-8に統一する事が多くなると思いますが、UTF-8は正規化により他の文字エンコーディングに無い脆弱性を作ってしまう場合があります。(特にIDS/IPS関連)蛇足ですが、UTF-8を利用する場合は必ずアプリケーション側で全てのデコード処理が終わった後に入力が期待する入力かチェックします。

ところでPostgreSQL 8.1.4のlibpqからPQescapeStringが非マルチスレッドセーフになりました。この影響により複数の文字エンコーディングを利用しているマルチスレッド環境(PHPのWindows版 – Apache, IIS両方がスレッド型Webサーバであるため影響を受けます)では問題が発生する可能性があります。単独でサーバを管理している場合は問題が発生することはあまりないと思いますが、Windows環境でWebサーバを複数の利用者で共有している場合は影響があるので注意が必要です。

補足:
PHPでの文字エンコーディングチェックのサンプルコード(mb_check_ecodingの有無もチェックしています)

// expected char encoding
define('APP_ENCODING', mb_internal_encoding());

// define call back function for array_walk()
if (function_exists('mb_check_encoding')) {
	// mb_check_encoding exists. just use it
	function encoding_check($val, $key, $encoding) {
		if (is_array($val)) {
			array_walk($val, 'encoding_check', $encoding);
		} else {
			if (!mb_check_encoding($val, $encoding)) {
				trigger_error('Encoding attack. ', E_USER_ERROR);
				exit;
			}
		}
		if (!mb_check_encoding($key, $encoding)) {
			trigger_error('Encoding attack. ', E_USER_ERROR);
			exit;
		}
		return true;
	}
} else {
	// mb_check_encoding does not exist. use mb_convert_encoding()
	function encoding_check($val, $key, $encoding) {
		if (is_array($val)) {
			array_walk($val, 'encoding_check', $encoding);
		} else {
			$val2 = mb_convert_encoding($val, $encoding, $encoding);
			if (!($val2 === $val)) {
				trigger_error('Encoding attack. ', E_USER_ERROR);
				exit;
			}
		}
		$key2 = mb_convert_encoding($key, $encoding, $encoding);
		if (!($key2 === $key)) {
			trigger_error('Encoding attack. ', E_USER_ERROR);
			exit;
		}
		return true;
	}
}

$inputs = array($_GET, $_POST, $_COOKIE, $_REQUEST, $_SERVER);
foreach($inputs as $input) {
	array_walk($input, 'encoding_check', APP_ENCODING);
}

Unicode(UTF)の場合、BOM、正規化などの問題の為意図した結果にならない場合があります。 EUC、SJISの場合は特に問題ないと思います。EUC-JPよりはEUCJP-win、SJISよりはSJIS-winをエンコーディング名に指定した方が問題が発生しづらいかも知れません。

投稿者: yohgaki