2019年12月5日木曜日

シェルを経由しないOSコマンド呼び出しがPHP7.4で実装された

この記事はPHP Advent Calendar 2019の5日目の記事です。

はじめに

私は6年前に、PHP Advent Calendar 2013として「PHPだってシェル経由でないコマンド呼び出し機能が欲しい」という記事を書きました。その中で、OSコマンドインジェクション対策の根本的かつ安全な対策は「シェルを経由しないコマンド呼び出し」であることを指摘した上で、末尾に以下のように書きました。
PHPコミッタのみなさま、PHP5.6の新機能として、シェルを経由しないコマンド呼び出しの機能を追加できませんか?
現実には当時からPCNTL関数にてシェルを経由しないコマンド呼び出しはできたのですが、当関数の使用が難しいことと、CLI版あるいはCGI版(FastCGIは可)のPHPでないとサポートされていないなどの制限があり、popenやproc_openなど使いやすいコマンド呼び出し関数において、シェル呼び出しのないコマンド実行機能が欲しいところでした。

この「私の願い」はPHP 5.6では実現しませんでしたが、PHP 7.4においてproc_open関数の拡張として実現しました。実に6年越しの実現ということになります。

proc_openの従来の問題点

proc_openに限りませんが、PHPの従来のコマンド実行機能(PCNTL関数は例外)の問題として、「常にシェル経由でコマンドを呼び出す」ことがあります。これを確認するための簡単なサンプルを示します。以下は、ps -fコマンドをproc_open関数で呼び出しています。
<?php
$cmd = "ps -f";
$process = proc_open($cmd, [], $pipes);
if (is_resource($process)) {
    $return_value = proc_close($process);
    echo "command returned $return_value\n";
}
呼び出し例は下記となります。赤字で示しているように、シェル(/bin/sh)経由でpsコマンドが実行されています。
UID        PID  PPID  C STIME TTY          TIME CMD
ockeghem 16921 16920  0 16:31 pts/0    00:00:00 -bash
ockeghem 18858 16921  0 17:18 pts/0    00:00:00 php-7.4.0 proc_open4.php
ockeghem 18859 18858  0 17:18 pts/0    00:00:00 sh -c ps -f
ockeghem 18860 18859  0 17:18 pts/0    00:00:00 ps -f
command returned 0
このため、コマンドラインにセミコロンなどにより追加のコマンドを実行できる可能性があり、OSコマンドインジェクション脆弱性の原因になっていました。ここで、psのオプションとして、-fの代わりに、「-f; echo Hello」を指定してみましょう。
<?php
$cmd = "ps -f; echo Hello";
$process = proc_open($cmd, [], $pipes);
// 以下省略
呼び出し例は下記となります。sh -c のパラメータとして; echo Helloが追加されていることと、echoコマンドの実行結果としてHelloが表示されていることがわかります。これがOSコマンドインジェクションの原理です。
UID        PID  PPID  C STIME TTY          TIME CMD
ockeghem 16921 16920  0 16:31 pts/0    00:00:00 -bash
ockeghem 18932 16921  0 17:30 pts/0    00:00:00 php-7.4.0 proc_open4.php
ockeghem 18933 18932  0 17:30 pts/0    00:00:00 sh -c ps -f; echo Hello
ockeghem 18934 18933  0 17:30 pts/0    00:00:00 ps -f
Hello
command returned 0
この対策として、コマンドラインのパラメータをエスケープ処理する方法もありますが、エスケープ処理自体が複雑になる可能性があり、実際にPHPのescapeshellcmd関数には脆弱性(こちらを参照)があるため使用を避けるべき状態でした。

proc_openのPHP 7.4での新しい呼び出し方

この状況に対して、PHP 7.4では、proc_openの第1引数を配列として指定することにより、コマンドとパラメータを明確に分離するとともに、シェルを経由しないコマンド実行ができるようになりました(パチパチパチ)。
先のスクリプトをこの形式で書き換えてみましょう。
<?php
$cmd = ["ps", "-f"];
$process = proc_open($cmd, [], $pipes);
if (is_resource($process)) {
    $return_value = proc_close($process);
    echo "command returned $return_value\n";
}
実行例は以下となります。シェルを経由せずに直接コマンドが実行されていることがわかります。
UID        PID  PPID  C STIME TTY          TIME CMD
ockeghem 16921 16920  0 16:31 pts/0    00:00:00 -bash
ockeghem 18895 16921  0 17:27 pts/0    00:00:00 php-7.4.0 proc_open4.php
ockeghem 18896 18895  0 17:27 pts/0    00:00:00 ps -f
command returned 0
続いて、先程同様に、-f オプションの代わりに -f; echo Hello を指定してみましょう。
<?php
$cmd = ["ps", "-f; echo Hello"];
$process = proc_open($cmd, [], $pipes);
// 以下略
実行結果は以下のとおりです。psコマンドにオプションとして「-f; echo Hello」が渡されたため、「unsupported SysV option」というエラーになっていますが、OSコマンドインジェクションにはならないことがわかります。
error: unsupported SysV option

Usage:
 ps [options]

 Try 'ps --help <simple all="" list="" misc="" output="" threads="">'
  or 'ps --help <s a="" l="" m="" o="" t="">'
 for additional help text.

For more details see ps(1).
command returned 1
この呼出方法(proc_openの第一引数を配列で指定)の場合、シェルを経由しないでコマンドを呼び出すことから、原理的にOSコマンドインジェクション脆弱性を避けることができます。今後PHP 7.4以降にて外部コマンドを呼び出す場合は常に、proc_open関数にて第1引数を配列で指定し、かつ配列の先頭要素(コマンド名)は固定とすることで、OSコマンドインジェクションを避けつつ簡便かつ安全な実装が可能になります。

まとめ

PHP 7.4にて新たに追加されたproc_openの新しい呼び出し方を紹介しました。私個人としても、6年越しの要望がかなえられた結果となり、よいクリスマスを迎えられそうです。

0 件のコメント:

コメントを投稿

フォロワー

ブログ アーカイブ