【PHP】安全にお手軽にexecするためのsymfony/processの紹介

 PHPから同期的に外部プログラムを呼び出すときにはexec関数ないしshell_exec関数を使います。非同期的に外部プログラムを呼び出すにはproc_open関数を使います。これらの関数はコンソール上でコマンドを叩くのとおおよそ同様に動作します。
PHP: exec – Manual
PHP: shell_exec – Manual
PHP: proc_open – Manual
 いずれの場合も実行コマンド文字列を渡す必要のある関数です。PHPにはこういった実行コマンド用のエスケープ関数であるescapeshellarg, escapeshellcmdが用意されていますが、もし漏れがあり、それがコマンドインジェクションに繋がると悲惨なことに任意コード実行、事実上のサーバ乗っ取りまで起きます。そうなるとPHP実行ユーザの権限で許される限り好き放題されます。
PHP: escapeshellarg – Manual
PHP: escapeshellcmd – Manual
 
 こういった問題を解決するためにはコマンドになる素材を渡すと自動でエスケープした状態でコマンドを実行する関数を用意するのが定番です。symfony/processはそういったもののリッチなバージョンです。エスケープによる安全さのみならず、同期実行、非同期実行の様なユースケースに合った動作の切り替え、標準出力や標準エラー出力をはじめとしたコマンドに関わる情報を得る便利機能までついています。
symfony/process – Packagist
 symfony/processはSymfonyフレームワークの一パッケージです。Symfonyの派生であるLaravelにはデフォルトで入っています。単品で導入するには例によってcomposerでインストールです。使い方は例えば次です。

$option = '-latr';
$process = new Process(['ls', $option]);
$process->run();
$output = $process->getOutput();
/** $outputの中身
 * total 80
 * drwxr-xr-x   2 root root  4096 Apr 11  2018 srv
 * drwxr-xr-x   2 root root  4096 Apr 11  2018 opt
 * drwxr-xr-x   2 root root  4096 Apr 11  2018 mnt
 * drwxr-xr-x   2 root root  4096 Apr 11  2018 media
 * lrwxrwxrwx   1 root root     8 Oct  1 10:15 sbin -> usr/sbin
 * lrwxrwxrwx   1 root root     9 Oct  1 10:15 lib64 -> usr/lib64
 * lrwxrwxrwx   1 root root     7 Oct  1 10:15 lib -> usr/lib
 * lrwxrwxrwx   1 root root     7 Oct  1 10:15 bin -> usr/bin
 * drwxr-xr-x   1 root root  4096 Oct  1 10:15 usr
 * -rw-r--r--   1 root root 12123 Oct  1 10:16 anaconda-post.log
 * drwxr-xr-x   3 root root  4096 Dec 29 23:27 boot
 * drwxr-xr-x   1 root root  4096 Dec 29 23:27 var
 * dr-xr-x---   1 root root  4096 Jan  7 09:05 root
 * drwxr-xr-x   1 root root  4096 Jan  7 14:13 home
 * drwxr-xr-x   1 root root  4096 Jan 27 09:58 etc
 * -rwxr-xr-x   1 root root     0 Jan 27 09:58 .dockerenv
 * drwxr-xr-x   1 root root  4096 Jan 27 09:58 ..
 * drwxr-xr-x   1 root root  4096 Jan 27 09:58 .
 * dr-xr-xr-x 168 root root     0 Jan 27 17:52 proc
 * dr-xr-xr-x  13 root root     0 Jan 27 17:52 sys
 * drwxr-xr-x   5 root root   340 Jan 27 17:52 dev
 * drwxr-xr-x   1 root root  4096 Jan 27 17:52 run
 * drwxrwxrwt   1 root root  4096 Jan 27 17:52 tmp
 */

 配列でコマンドになる文字列を渡してProcessインスタンスを生成。Processインスタンスをrunメソッドで同期実行するか、startメソッドで非同期実行するかします。すると外部プロセスが終了し次第、Processインスタンス中に標準出力などが代入され、getOutputメソッドなどで呼び出せるようになります。単にexecするよりもずっと安全で制御が効きます。
 コマンドの元になる配列を組み上げるのはわりと面倒なのでBuilderパターンを用いるのがよいです。図は増補改訂版Java言語で学ぶデザインパターン入門 p.82から引用しました。
 
 増補改訂版Java言語で学ぶデザインパターン入門 | 結城 浩 |本 | 通販 | Amazon
 マンガでわかる Builder – Qiita
 Builderパターン – Qiita

 Builderパターンは簡単に言えば、パラメータなど何かしらを構築するプログラムのよくあるパターンです。PHPに縁のある人はおそらくSQLのクエリビルダが身近です。コマンドの場合、オプション関連があってもなくてもよくて -[a-zA-Z] 対象 の様な気持ち手間な文字列を大量に使うのでBuilderパターンを使う使わないで実装、呼び出しの手間がわりと変わります。例えば次の様な感じです。

class ListSegmentsCommand
{
    protected $command = 'ls';
    protected $options = [];
    public function setAllOption(){
        $this->options[] = '-a';
        return $this;
    }
    public function setLongOption(){
        $this->options[] = '-l';
        return $this;
    }
    // 上記の様なオプションをセットするメソッドを羅列
    public function run(){
        $process = new Process(Arr::flatten([$this->command, $this->options]))
        $process->run();
        return $process;
    }
}
// 呼び出すならば
$lsProcess = (new ListSegmentsCommand())
    ->setAllOption()
    ->setLongOption()
    ->run();
$lsProcess->getOutput();
>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG