今回は、サーバ上に配置するファイルの扱いについて解説します。公開すべき範囲の決め方、公開してはいけないファイルの扱い、中身を見せてはいけないファイルの扱いなどについて解説します(編集部)
今回は、本題に入る前に緊急のお知らせがあります。PHPのバージョン5.3.10が登場しました。先日、バージョン5.3.9が「セキュリティの面で脆弱な部分を修正したバージョン」として公開されたばかりですが(第38回参照)、修正した部分に新たに脆弱な部分を作ってしまったようです。速やかにバージョンアップしましょう。
今回発覚した問題は、攻撃者がPHPを操作して、任意のコードを実行できてしまうというものです。すでに実証コードも出回っているので、バージョン5.3.9を使い続けるのは危険です。
5.3.9では、サービスを提供不能にするDoS攻撃を許す脆弱性を修正してつぶしたわけですが、その過程でより深刻な問題を作り込んでしまうのも皮肉な話です。こういうこともたまにはあります。いずれにしても、迅速にアップデートすることが肝要です。
本題に入りましょう。今回はPHPの実行時設定の中でも、ファイル操作に関係する設定項目について解説していきます。ファイルの操作もデータベース操作と同じく、悪意ある攻撃者の標的になりやすい部分です。例えば、アプリケーションを作るとき、ユーザーが送信してきたリクエストデータに応じて、操作するファイルを決定するようにコードを書くことがあります。このとき、アプリケーションの作り方を間違えるとまずいことになります。攻撃者が「攻撃する」という明らかな意図を持ってリクエストデータを送り付けてくると、開発者が意図していないファイルに対する操作を許してしまうことがあるのです。
脆弱な点を残した未熟なアプリケーションを作る開発者が悪いと言ってしまえばその通りですが、過失によって脆弱な点を作ってしまうこともあり得ます。アプリケーションから利用する必要のないファイルは、開けないようにしておくに越したことはありません。そこで「open_basedir」ディレクティブの登場です。
open_basedirにはディレクトリを指定します。「:」でつなげることで複数のディレクトリを指定することも可能です。ディレクトリを指定すると、指定したディレクトリ以下にあるファイルだけが開けるようになります。指定したディレクトリよりも上位のディレクトリにあるファイルを開こうとするとエラーとなります。このエラーが発生すると、Webブラウザやログファイルなどにエラーを説明するメッセージが出力されます。エラーの出力先を変更する方法は前回解説しました。
それでは実際にこのディレクティブの効果を確認してみましょう。以下に示すサンプルプログラムをfileget.phpという名前で公開ディレクトリに設置します。
<?php print_r(file($_GET['file'])); ?>
このプログラムは、GETメソッドのリクエストパラメータに指定してあるファイルを読み込んで表示します。関数file()は、引数として指定したファイルの内容をすべて読み込んで配列として返すというものです。$_GETはGETメソッドのリクエストデータが入る変数です。そして、print_r()は配列の中身まで表示する関数です(第38回参照)。つまり、このプログラムのURLに「?file=ファイル名」とするだけで、どのファイルでも外部から取得可能ということになります。
実際は、このようにプログラム中でファイル名を指定する部分に、リクエストデータをそのまま使うのは禁じ手です。このプログラムはあくまでサンプルで、動作を確認するために作ったものだということを忘れないでください。このように明らかに不備があるコードを意図して書くことはないでしょうが、バグやミスによってファイル名を指定する部分にリクエストデータが入り込む、ということもあり得ますので、注意が必要です。
それでは、「?file=/etc/passwd」をURLに加えてcurlコマンドを実行してみましょう。
curl 'http://www3026ub.sakura.ne.jp/fileget.php?file=/etc/passwd' Array ( [0] => root:x:0:0:root:/root:/bin/bash [1] => bin:x:1:1:bin:/bin:/sbin/nologin [2] => daemon:x:2:2:daemon:/sbin:/sbin/nologin (以下略)
/etc/passwdファイルの内容が取得できてしまいました。現在では、/etc/passwdにパスワードを記述することはありませんが、OSのユーザーを一覧できる非常に重要なファイルです。プログラムの書き方が悪いと、このように重要なファイルも流出しかねないということがお分かりいただけると思います。
open_basedirディレクティブを使えば、重要なファイルの流出は防げます。open_basedirに/srv/httpdを指定して、再度コマンドを実行してみましょう。/srv/httpdは、Apache HTTP Server(以下Apache)のためのディレクトリです。
$ curl 'http://www3026ub.sakura.ne.jp/fileget.php?file=/etc/passwd'
今度は、curlを実行しても何の反応も返って来ません。これは前回、エラーメッセージをHTMLで出力させずに、ログファイルに記録するようにしたためです。ログファイルを見ると、次のような行を確認できます。open_basedirの設定によってエラーとなったことが分かります。
[Thu Feb 09 09:50:16 2012] [error] [client 49.212.32.64] PHP Warning: file() [<a href='function.file'>function.file</a>]: open_basedir restriction in effect. File(/etc/passwd) is not within the allowed path(s): (/srv/httpd) in /srv/httpd/www3026ub.sakura.ne.jp/webspace/fileget.php on line 2 [Thu Feb 09 09:50:16 2012] [error] [client 49.212.32.64] PHP Warning: file(/etc/passwd) [<a href='function.file'>function.file</a>]: failed to open stream: Operation not permitted in /srv/httpd/www3026ub.sakura.ne.jp/webspace/fileget.php on line 2
これで少なくとも、/etcや/varなど、OSの動作に関係する重要な情報が格納されているファイルを開くことはできなくなりました。しかし、/srv/httpdにあるファイルにはアクセス可能なままです。内部にはphp.iniや、Apacheの設定などの重要な情報もあります。では、open_basedirにはどういった値を設定するのが良いでしょうか?
まず、OSやApacheなどの動作に関係しないディレクトリを作成し、open_basedirに設定してみます。第40回で、ファイルのアップロード用に一時的に使うディレクトリを/home/uploadsとしました。これと同じように、/home/phpdataのように、PHPスクリプトが利用するデータファイルをひとまとめに置くディレクトリを新たに用意するということです。
open_basedirに/home/phpdataと設定し、先ほどと同じようにcurlコマンドでfileget.phpを実行してみます。その結果、ログに以下のような記録が残りました。
[Thu Feb 09 10:02:22 2012] [error] [client 49.212.32.64] PHP Warning: Unknown: open_basedir restriction in effect. File(/srv/httpd/www3026ub.sakura.ne.jp/webspace/fileget.php) is not within the allowed path(s): (/home/phpdata) in Unknown on line 0 [Thu Feb 09 10:02:22 2012] [error] [client 49.212.32.64] PHP Warning: Unknown: failed to open stream: Operation not permitted in Unknown on line 0
先ほどご覧いただいたものと同じようなログに見えますが、よく読むと、open_basedirの設定によって、PHPスクリプトの実行が失敗していると分かります。つまり、open_basedirで公開するディレクトリを限定すると、PHPスクリプトを起動しようとして、PHP実行エンジンがそれを読み込むという動作にも関係するのです。
つまり、少なくともプログラムファイルを設置するWebの公開ディレクトリはopen_basedirに設定しなければならないということになります。そうしなければプログラムの実行すら不可能になります。/home/phpdataのように、PHPスクリプトが利用するデータファイルのために完全な別ディレクトリを作ったとしても、そこだけをopen_basedirに設定しても仕方ないのです。
もう1つ問題があります。Webの公開ディレクトリはバーチャルホストごとに異なったディレクトリだということです。つまり、open_basedirにはバーチャルホストごとに別々のディレクトリを設定しなければなりません。しかしphp.iniへの設定は、サーバ全体の動作に関係するものです。php.iniでは、バーチャルホストごとの設定はできません。では、php.iniには、open_basedirを設定しない方がよいのでしょうか。
ここで1つ覚えておいていただきたいことがあります。open_basedirの設定は、より厳しくする方向であれば上書きが可能です。php.iniの設定で/srv/httpdと指定したとしても、/srv/httpd/fooという具合に、より範囲を限定するように設定を上書きできるということです。/etcや/homeには変更できません。
第23回で説明したように、この連載では、/srv/httpdの中にバーチャルホストごとのディレクトリを配置しています。open_basedirの設定を上書きする際の制約とディレクトリのレイアウトを考えると、php.iniではopen_basedirには、各バーチャルホスト共通の親ディレクトリである/srv/httpdを設定するのがよいでしょう。
続いて、それぞれのバーチャルホストごとにopen_basedirを設定します。前述の通り、Web公開ディレクトリである「/srv/httpd/バーチャルホスト名/webspace」がまず必要です。さらに、非公開の領域にデータファイルなどを置くことを考え、「/srv/httpd/バーチャルホスト名/phpdata」といったディレクトリを作成して加えるとよいでしょう。
この連載で作ってきた環境を例にすると「/srv/httpd/www3026ub.sakura.ne.jp/apache.conf」に、次の行を追加するということになります。
PHP_Value open_basedir /srv/httpd/www3026ub.sakura.ne.jp/webspace:/srv/httpd/www3026ub.sakura.ne.jp/phpdata
ここまで設定を済ませて、phpinfo()の出力を見ると、図1のようになります。右側の列がサーバ全体の設定であるマスター設定値で、php.iniで指定したものです。左側はつい先ほど設定したもの、つまりphpinfo()を実行している環境の設定になります。apache.confに記述した設定になっていることが確認できます。
これで、PHPプログラムに問題があったとしても、/srv/httpdにあるphp.iniや、Apacheの設定ファイルの内容が丸見えになることはなくなりました。
ここまで設定しても、open_basedirで許可している公開ディレクトリやデータディレクトリに置いてあるファイルが流出する恐れはあります。しかし、PHP環境の設定で対応できる策はここまでです。まず何より、脆弱性のないプログラムを書くということが大切であり、設定による防止策はあくまでいざというときの最後の防波堤と考えなければなりません。
PHPではファイルを開くときに、ファイル名ではなく、URLでファイルを指定できます。PHPのコマンドラインで確認してみましょう。
$ /opt/php-5.3.10/bin/php -a Interactive shell php > $data = file('http://www3026ub.sakura.ne.jp/phpinfo.php'); php > print_r($data); Array ( [0] => <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/x html1-transitional.dtd"> [1] => <html xmlns="http://www.w3.org/1999/xhtml"><head> (以下略)
この機能があるため、HTTP経由でアクセス可能なサーバにあるファイルを、クライアントにあるファイルと同じように扱えるようになっているのです。
この機能は便利ではありますが、先ほど解説した、「設定による防止策はあくまで防波堤」とする考えからするとお勧めできる機能ではありません。先ほどのサンプルプログラムのように、リクエストデータで指定した名前のファイルを読み込めてしまうという脆弱性がある状況では、URLを指定することで同じことができるためです。例えば、プログラムが読み込むファイルの内容を攻撃者の好きなように変えることもできますし、ほかのサイトへの攻撃の踏み台にすることもできるでしょう。
HTTP経由でデータを取得する方法はほかにもあります。そのためのライブラリも存在します。ファイル名の代わりにURLでファイルを指定できるこの機能は、絶対に必要なものというわけではありません。そこで、この機能は無効にします。「allow_url_fopen」ディレクティブでOffを指定すれば設定できます。
この機能を無効にした状態で、先ほどのように、PHPのコマンドラインでファイルを開いてみましょう。allow_url_fopenをOffにしたphp.iniファイルを「-c」スイッチで指定してインタラクティブシェルを起動します。
/opt/php-5.3.10/bin/php -c /srv/httpd/php.ini -a Interactive shell php > $data = file('http://www3026ub.sakura.ne.jp/phpinfo.php'); PHP Warning: file(): http:// wrapper is disabled in the server configuration by allow_url_fopen=0 in php shell code on line 1 PHP Warning: file(http://www3026ub.sakura.ne.jp/phpinfo.php): failed to open stream: no suitable wrapper could be found in php shell code on line 1 php > print_r($data) php >
URLでファイルを指定しても中身を見ることができないということを確認できました。
allow_url_fopenに似たディレクティブとして「allow_url_include」があります。PHPにはinclude()やrequire()といった、別のPHPファイルを読み込むための関数がありますが、ここでファイル名の代わりにURLを使えるようにするというものです。
現在、この機能は標準で無効となっています。リクエストデータで、読み込むファイル名が制御できる脆弱性がある状況を考えてみてください。外部から任意のPHPファイルを読み込ませることができる、つまり任意のコードを実行できることになります。これは最悪のセキュリティホールと言えます。また、外部のPHPスクリプトを読み込む必要性もほとんどありません。このような事情から、標準では無効になっています。
ここまで解説してきた内容を反映させると、php.iniは次の通りになります。
memory_limit = 32M max_execution_time = 30 expose_php = Off magic_quotes_gpc = Off variables_order = GPCS register_long_arrays = Off register_argc_argv = Off post_max_size = 64K max_input_vars = 100 upload_tmp_dir = /home/uploads upload_max_filesize = 1 display_errors = Off display_startup_errors = Off log_errors = On error_reporting = E_ALL | E_STRICT open_basedir = /srv/httpd allow_url_fopen = Off
次回はセッションに関係する設定などを解説します。
Copyright © ITmedia, Inc. All Rights Reserved.