なぜPHPアプリにセキュリティホールが多いのか?

第20回文字エンコーディングとセキュリティ(2)

前回に引き続き、今回も文字エンコーディングとセキュリティをテーマに解説します。前回は文字エンコーディングを利用した攻撃で、JavaScriptインジェクションやSQLインジェクションなどが可能であることを紹介しました。今回はなぜ、文字エンコーディングを利用して攻撃できるのか、簡単に紹介します。

文字エンコーディングを利用した攻撃の原理

文字エンコーディングを利用した攻撃には3種類の方法があります。

  • 不正な文字エンコーディングを利用する方法
  • 文字エンコーディングを誤認識させる(誤認識を利用する)方法
  • 文字エンコーディングのエスケープ方式を利用する方法

この連載は攻撃方法を詳しく解説する事が目的ではありません。具体的なの攻撃方法の解説はできる限り控え、なぜこの3つ手法が攻撃に利用できるのか解説します。

前回も触れましたが、パス遷移攻撃には文字エンコーディングを利用した攻撃方法もあります。

  • 文字列の正規化を利用する方法

この方法はJavaを利用した環境でよく発生する脆弱性です。今回の解説を行う3つの攻撃方法とは、内容と対策が異なります。

文字の正規化以外にも、文字エンコーディング以外のエンコーディングも攻撃対象となる場合があります。

  • URLエンコーディングを利用する方法

URLエンコーディングは文字エンコーディングといえませんが、エンコーディング全般のリスクを紹介するために紹介します。

文字エンコーディングについて

文字エンコーディングを利用した攻撃手法を解説する前に、簡単に文字エンコーディングについて解説します。

文字エンコーディングは「文字コード」と呼ばれることがあります。しかし、厳密には文字エンコーディングと文字コードは同じではありません。文字コード(文字符号)とは文字をコンピュータで処理可能にするため、文字に番号を与えたコード体系です。文字エンコーディングは文字コードをコンピュータが取り扱いやすい形に符号化(エンコーディング)したデータ形式といえます。同じ文字コードであっても、複数の文字エンコーディングが存在します。最も有名な例は、UnicodeとUTF(UCS Trasformation Format)エンコーディングでしょう。

Unicodeは文字コードですが、通常はUnicodeのままでは利用しません。最もよく利用されているUnicodeのエンコーディング形式はUTF-8とUTF-16です。UTF-8はASCIIコードとの互換性が考慮されたエンコーディングで、1バイトですべての文字を表せる欧米のアプリケーションであれば、ASCIIコードの範囲内で文字列を利用している限り、修正なくプログラムが動作します。このため、非常に多くのアプリケーションがUTF-8エンコーディングを採用しています。

UTF-8エンコーディングの場合、1文字を表現するために1バイトから4バイトを利用します。日本語の場合1文字を格納するために通常3バイトを必要とします。また、文字データのバイト数でなく文字数を知りたい場合、マルチバイト文字なのかシングルバイト文字なのか常に判別しなければならないので処理が面倒です。そこで、UTF-16を内部文字エンコーディングとして利用しているシステムが多くあります。JavaやWindowsは文字列を内部的にはUTF-16として扱っています[1]⁠。

UTFエンコーディングの種類
UTF-7, UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE

日本語の文字エンコーディングにはUTF以外にJIS(ISO-2022-JP⁠⁠、Shift-JIS、EUCがありますが、基本となっている文字コードはおおよそ一緒です。利用している文字コードはASCII、JIS X 0201、JIS X 0208、JIS X 0212、JIS X 213などです。

  • 参考:文字コード研究 ⁠ラトルズ⁠⁠ ISBN4-89977-051-0

不正な文字エンコーディングを利用した攻撃

日本のWebサイトで利用されている文字エンコーディングは、すべてマルチバイト文字です。すべてのマルチバイト文字エンコーディングは、不正な文字エンコーディングによる攻撃のリスクがあると考えて構いません。

不正な文字エンコーディングを利用した攻撃には、本来必要なバイト数未満のデータを送り、特殊文字を無効化する方法が利用されます。文字列の終端を表す「"」⁠ダブルクオート⁠⁠、⁠'」⁠シングルクオート)を無効化し、JavaScriptインジェクション/HTMLインジェクション/XMLインジェクション/SQLインジェクションを行います。

不正な文字エンコーディングを利用した攻撃では、本来の長さより短いデータや、正しいエンコーディングでは有り得ないデータを送信させ、そのデータを受け取ったプログラムを誤作動させる攻撃です。Webアプリケーションではブラウザやプラグインを誤作動させたり、Webアプリケーションがサーバ側で利用しているSQLデータベース、XMLデータベースなどを誤作動させます。結果として、不正なJavaScriptの実行や不正なSQL文の実行を可能にします。

PHPのXML関連モジュールに利用されているlibxml2も壊れた文字エンコーディングに対する脆弱性が何度も発見されています。壊れた文字エンコーディングがあるとXMLインジェクションが可能となったり、サービス不能攻撃が可能になるケースがありました。PHPのHTMLエスケープ関数であるhtmlentities/htmlspecialcharsも壊れた文字エンコーディングに対する複数の脆弱性が発見されています(PHP 4.4.9には脆弱性が一部残されています⁠⁠。Webアプリケーションが壊れた文字エンコーディングを利用した攻撃に対して脆弱でなくても、出力先のWebブラウザが脆弱では意味が有りません。壊れた文字エンコーディングを利用した攻撃は大きな脅威と言えます。

対策:

入力時のバリデーション処理で、すべての文字列に対してエンコーディングが正しいかチェックする。

PHPで文字エンコーディングが正しいかチェックする場合、mb_check_encoding関数を利用します。mb_check_encoding関数は文字列型のみチェックするので、$_GET、$_POST、$_COOKIE、$_SERVERなどをチェックする場合、こられらは配列も含むのでarray_walk_recursive関数と一緒に使います。

文字エンコーディングのチェック例
function check_encoding($value, $key) {
  if (!mb_check_encoding($value, 'UTF-8')) {
    die('Invalid charactor encoding detected');
  }
}

array_walk_recursive($arr, 'check_encoding');

この攻撃に対する対策は非常に簡単ですが、ほとんどのアプリケーションは未対策です。特に海外製のPHPアプリケーションはほぼすべて未対策である、と言ってよいくらい、文字エンコーディングが正しいかチェックしていません。

壊れた文字エンコーディングを検出していれば、htmlentities/htmlspecialchars関数のバグを利用したエスケープ処理を回避した攻撃も行えませんでした。libxml2の脆弱性にも、文字エンコーディングをチェックしていれば回避できた問題も多くありました。壊れた文字エンコーディングをチェックしていれば、不正な文字エンコーディングを利用たSQLインジェクションも回避できました。

不正な文字エンコーディングを利用したセキュリティ上の問題はどこで発生してもおかしくありません。文字エンコーディングが正しい文字エンコーディングであるかチェックするのは、Webアプリケーションのみでなく、すべてのアプリケーションに必須のセキュリティ対策だといえます。

文字エンコーディングをチェックしていないWebアプリケーションをそのまま利用するのはリスクが高いので、php.ini設定のauto_prepend_file設定などを利用し、文字エンコーディングをチェックするコードを追加して利用したほうが安全です。ただし、URLエンコードでバイナリデータを受け渡ししているアプリケーションは、auto_prepend_file設定を利用してすべての入力をチェックすると正常に動作しないので、除外リストを作成してバイナリデータは除外するようにします。

文字エンコーディングを誤認識させる(誤認識を利用する)攻撃

文字エンコーディングを誤認識させる方法は、クロスサイトスクリプティング脆弱性の危険性を指摘したCA-2000-02でも指摘しているのは既に紹介した通りです。もっとも分かりやすい例は、UTF-7エンコーディングを利用した攻撃方法です。UTF-7の場合、HTMLの特殊文字である<、>がエンコードされ、別の文字列に変換されます。

UTF-7エンコーディングへの変換
<?php echo mb_convert_encoding('< >', 'UTF-7');?>
出力
+ADw +AD4-

UTF-7に変換後には<、>がないので、UTF-7エンコーディングの文字列データをhtmlentities/htmlspecialchars関数などでエスケープ処理しても意味がありません。UTF-7はASCIIデータと変わらないので、不正な文字エンコーディングを検出していても役に立ちません。UTF-7以外で記述されたページに、UTF-7の文字列データを送り、ブラウザなどが誤ってUTF-7文字エンコーディングと認識してまうと不正なスクリプトやHTMLタグの埋め込みが可能となります。

Webブラウザには文字エンコーディングの自動識別機能があります。そして、多くのユーザは文字エンコーディングの自動識別機能を有効に設定しています。このため、攻撃者はWebアプリケーションにUTF-7でエンコードした文字列を送るだけで、簡単に攻撃が成功してしまう場合もあります。

2000年のクロスサイトスクリプティング脆弱性のアドバイザリ(CA-2000-02)「すべての動的に生成されたWebページは明示的に適切な文字コードセットを設定しなければならない」としているのは、この種の攻撃を防ぐために記述されています。

対策:

明示的に文字エンコーディングを指定する。例えば、HTTPヘッダのcharset属性でアプリケーションが利用する文字エンコーディングを指定する。

HTTPヘッダで文字エンコーディングを設定している場合、本来であればWebブラウザはエンコーディングの自動識別を行うべきではありません。しかし、自動識別を行っているブラウザではHTTPヘッダの指定を無視して、自動的に文字エンコーディングを識別してしまうブラウザもあります。追加の対策としてMETAタグでも文字エンコーディングを指定するほうがよいでしょう。

METAタグでの文字エンコーディング指定例(UTF-8)
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

PHPでHTTPヘッダのContent-Typeヘッダのcharset属性で文字エンコーディングを指定する場合は、ini_set関数でdefault_charsetを設定するか、php.iniのdefault_charsetを設定します。

ini_set関数
ini_set('default_charset', 'UTF-8');
php.ini
default_charset="UTF-8"

JSPやASPでWebアプリケーションを作ったことがある方は、お約束として文字エンコーディングを記載するようになっているサンプルコードが多いです。その結果として正しい文字エンコーディングの取り扱いとなっているアプリケーションが多いですが、PHPの場合、php.iniで設定できるのでアプリケーションで設定していないことが少なくありません。

筆者はphp.iniファイルの規定値としてdefault_charsetに何らかの文字エンコーディングを指定すべきだと考えていますが、ソース版のPHPに付属するphp.iniにはdefault_charsetが設定されていません。多くのPHPアプリケーションがdefault_chaset設定を変更していないので、php.iniで設定するか、アプリケーションの初期化ファイルなどに手を加えない限り脆弱な状態になってしまっています。

文字エンコーディングの誤認識を利用した攻撃を防ぐには、先ほど紹介した対策である「文字エンコーディングが正しいエンコーディングかチェックする」対策も重要です。Webサイトを参照したユーザが「文字化けかな?」と思って、使用する文字エンコーディングを切り替えた時に不正なJavaScriptを実行させる攻撃手法もよく知られています。

ブラウザの文字エンコーディングの自動認識機能を無効にしてWebサイトを利用していると、比較的有名なサイトでも文字エンコーディングの取り扱いに不備があることに気が付くことが少なくありません。この記事を読まれる方は開発者の方とは思いますが、普段Webページを参照するエンドユーザとしてのセキュリティ対策として「ブラウザの文字エンコーディング自動認識機能は無効にする」ことをお薦めします。より安全にブラウジングできるだけでなく、自分の構築しているサイトが誤った処理をしていないかチェックするためにも役立ちます。

まとめ

すべての文字エンコーディングを利用した攻撃については解説できませんでしたが、文字エンコーディングと文字列を厳格に取り扱う必要性は理解頂けたと思います。次回も文字エンコーディングを用いた代表的な攻撃の手法の解説を続けます。

おすすめ記事

記事・ニュース一覧