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

第26回まだまだ残っているファイル読み込みバグ

PHPのスクリプト読み込み用の関数である require/indclude文は、PHPが埋め込み型の言語であるために非常に危険なバグの温床となっています[1]⁠。

つい最近のファイル読み込みバグ

執筆時点(4/24)でレポートされているファイル読み込みバグです。

  • WebPotal CMS(4/23)
  • NotFTP(4/22)
  • TotalCalender(4/22)
  • SMA-DB(4/17)
  • Job2C(4/16)
  • FreeWebshop(4/16)
  • GuestCal(4/15)
  • Jamroom(4/15)

ファイル読み込みバグはサーバの乗っ取りも可能とする致命的なバグですが、現在でも脆弱性が次々にレポートされているバグであることが分かります。

require/include文とファイル読み込みバグの問題

PHPはスクリプト(プログラム)と出力を同じファイルの中に書き込める、埋め込み型の言語です。この仕様により簡単なWebアプリケーションであれば、非常に短いコードで記述することができます。しかし、この言語仕様が非常に深刻なセキュリティ問題を発生させる原因となっています。

ファイル読み込みバグがある場合、システムの上のファイルの読み取りだけでなく、任意スクリプトの実行を可能とします。つまり、サーバを完全に乗っ取られる可能性があるのです。

require/include文

require/require_once/include/include_onceでプログラムとテキスト(実際にはテキストに限らずバイナリも読み込み可能)を同時に扱えることは、セキュリティ上のリスクをもたらします。

PHP以外の言語では、プログラムとテキストを両方読み込める仕組みを持つ言語はあまりありません。プログラムとテキストを両方含む仕組みを持てるようにしている言語拡張は多くありますが、元々言語が持っている機能として持っている言語はあまりありません。

さらに問題なのは、PHPはプログラムだけを含むファイルを読み込む関数がありません。このため、プログラムだけを読み込みたい場合でも、プログラムとテキストの両方を読み込めるrequire/include文を使用しなければなりません。

テキストとプログラムを同じ文で読み込む危険性

経験豊富なプログラマでも、経験が少ないプログラマでも、include/require文でファイルを読み込む危険性を正しく理解していない場合が多いです。

PHP初心者がよく作ってしまう致命的なセキュリティホールには次のようなコードがあります。

if (!empty($_GET['plugin'])) {
  require_once(PLUGIN_DIR.DIRECTORY_SEPARATOR.$_GET['plugin'].'.php');
}

経験豊富なプログラマであっても、プログラムファイルだけ読み込んでいるつもりでこのようなコードを書いてしまうことがあります。実際にZend社のWebサイトにこれと同類のコードがサンプルコードとして掲載されていたことがあります。

このコードは非常に危険です。なぜなら、⁠..⁠を利用して上位ディレクトリのファイルにアクセスできる上、⁠%00⁠(ヌル文字)を利用して⁠.php⁠拡張子だけの読み込みに限定している部分も無効化できます。

つまりこのようなコードを書くと、権限を持つシステム上のファイルに自由にアクセスできるようになってしまいます。

テキストの読み込み

PHPは<?php ?>等で囲まれた部分以外はecho文で出力したのと同じ動作をします。

先ほど紹介したコードで$_GET['plugin']に⁠../../../etc/password¥0⁠等の文字列を設定することにより、システム上のファイルにアクセスできるようになります。

プログラムの読み込みだけ可能であれば、UNIXシステムのパスワードファイルである/etc/passwordファイルを読み込もうとしてもエラーになり、被害は発生しません。しかし、PHPのinclude/require文はプロブラム以外のテキストを標準出力に出力してしまいます。これはWebサーバの場合、ブラウザに出力する事を意味します。この仕様によりどんなファイルでも読み取って盗むことが可能になります。

コードの読み込み

requrie/include文はコードを読み込み実行できます。しかも、攻撃者に都合よいことに、コード以外の部分は標準出力に出力するので、エラー無くPHPコードを実行できます。

例えば、画像ファイル、PDF等のバイナリに埋め込まれたPHPスクリプトでもinclude/require文で読み込めば実行していまいます。

例:PHPスクリプトを含むgifファイル
GIF89a
<?php system('rm -rf /'); ?>

このファイルはgetimagesize関数ではgif画像として認識されてしまいますが、require/include文で読み込まれると、

system('rm -rf /');

を実行してしまいます。UNIX系のシステムではアクセス権限がある全てのファイルを消してしまいます。

テキストだけでなく、どんなバイナリファイルでもPHPコードを含ませることが可能です。任意のファイルが読み込めてしまうバグがあるために、ファイルアップロードを許可しているアプリでは致命的な欠陥となってしまいます。

リモートスクリプト

さらに問題を複雑にしているのがリモートスクリプトです。PHPはrequire/include文でほかのサイトにあるPHPスクリプトを実行できます。攻撃用のスクリプトを何らかの方法でアップロードしなくても攻撃できるのです。

この仕様の危険性はようやく認識され、PHP5.2ではデフォルトで無効となるように設定されました。

スクリプト読み込みの対策

意図しないスクリプトの読み込みによる情報漏洩と任意スクリプト実行を防止するには、ホワイトリスト方式によるバリデーションが最もよいです。

すべてではありませんが、スクリプト読み込みバグを発生させているコードの多くは、プラグインの読み込みのコードで発生しています。アプリケーション機能のプラグイン、多言語化のための言語プラグインなどで例にあげたようなバグが発生しています。

open_basedir設定の活用

require/include文に限ったことではありませんが、php.iniのopen_basedirを設定することにより万が一の場合のリスクを軽減できます。

open_basedir=/www/appication

と設定している場合、/etc/passwd などの本来アクセスできるはずもないファイルへのアクセスを防げます[2]⁠。

このような設定を行っていれば、open_basedir設定に違反したコードがエラーを発生させ、ログに記録されるので、実際に攻撃され被害が発生する前に対処できる可能性もあります。

スクリプト専用のrequire/include

筆者はこの問題に対する根本的な対策はスクリプト専用のrequire/include文の導入だと考えていますが、残念ながらこの機能は追加されないでしょう。この機能の導入は随分前に議論され、導入しないことが決まってしまったからです。

たとえば、requre/include文の第二引数(または最後の引数)にtrueが指定された場合、ファイル全体をスクリプトと見なし、⁠<?php ?>⁠等、スクリプトタグの使用をできないようにすればよいのです。

require($_GET['plugin'], true);

この仕様変更は比較的簡単です。危険なスクリプト読み込みバグが次々に作られて明らかになってきている現状を考えると、PHP開発者が考えを変えるときがくるのもそう遠くないかも知れません。しかし、あまり大きな期待はしないほうがよいです。少なくともPHP5.3には導入されません。

まとめ

include/require文に変数を使用する場合は細心の注意が必要であることを忘れないでください。プログラムだけでなく、どんなファイルでも読み込みが可能で、その中の一部にでもPHPスクリプトが含まれる場合、エラー無く実行されてしまうことに注意してください。

おすすめ記事

記事・ニュース一覧