mb_send_mail(),mail()で第5引数を設定する際の注意点

以下の徳丸さんの記事を読んで、以前にmb_send_mail()の関連で調べたことがあったのを思い出しましたので、少し書きます。

環境は Unix 系の OS で OS コマンドを使用してメールを送信する場合です。メール送信コマンドは php.ini で sendmail_path を設定します(デフォルト: "sendmail -t -i")。
mb_send_mail関数(mail関数も同様)ですが、第5引数(additional_parameter)にユーザの入力を使用する場合は注意が必要です。mb_send_mail関数の第5引数は、内部でescapeshellcmd(内部関数名:php_escape_shell_cmd)によって引数の文字列全体がエスケープされます。

escapeshellcmd() は、以前に徳丸さんから OS コマンドインジェクションの防止には不適切であると指摘されています。ユーザの入力値を十分にチェックしておかないと、sendmailコマンドに任意のコマンドライン引数を渡されてしまうことになります。

mb_send_mail() の第5引数の使用例として、sendmail コマンドに envelope-from を渡す(-f オプション)を考えます。

<?php
$envelope_from = 'from@example.com ;ls -l';
mb_send_mail( "test1@example.com", "subject", "body", "From: test2@example.com", "-f " . $envelope_from );

上記の場合、「-f from@example.com ;ls -l」が php_escape_shell_cmd のエスケープ対象になり、「;」が「\;」にエスケープされます。sendmailコマンドに渡る引数は「sendmail -t -i -f from@example.com \;ls -l」になりますので、ls コマンドは実行されません。ただし、この時、不正なコマンド引数「;ls」と「-l」が sendmail コマンドに渡ることになります。
つまり、$envelope_from の妥当性をチェックせずに、ユーザからの入力を使用していると、sendmailコマンドに任意の引数を設定可能です。

対処方法

対処方法としては、引数の妥当性を確認するのが良いと思います。上記のようにメールアドレスである場合は、メールアドレスとして正しいかどうかをチェックします。
コマンドライン引数に対して escapeshellarg() によるエスケープをする方法は止めておいた方が良いと思います。
例えば、「&」は、メールアドレスにも使用できる文字ですが、escapeshellarg() でエスケープすると、mb_send_mail() の内部でさらに escapeshellcmd() でエスケープされ、別のメールアドレスになってしまいます。

<?php
$envelope_from = escapeshellarg( 'mail&1@example.com' );
mb_send_mail( "test1@example.com", "subject", "body", "From: test2@example.com", "-f " . $envelope_from );
// コマンドライン: sendmail -t -i -f 'mail\&1@example.com'

検証方法

sendmailコマンドで実際に試すのは大変ですし、実際に渡されているコマンドライン引数を確認するのは難しいため、検証用に以下のような PHP スクリプトを用意すると便利です。

<?php
// input_vars.php
// コマンドライン引数と標準入力の結果表示
echo "* argv :\n";
print_r( $_SERVER['argv'] );

echo "* stdin:\n";
echo preg_replace( '/^/mu', '  ', stream_get_contents( STDIN ) );
<?php
// mail.php
// テスト用スクリプト
$envelope_from = 'mail&1@example.com';
$envelope_from = escapeshellarg( $envelope_from );

echo '* $envelope_from: [' . $envelope_from . "]\n";
mb_send_mail( "test1@example.com", "subject", "body", "From: test2@example.com", "-f " . $envelope_from );

実行する時は、PHPCLI版、CGI版では、引数 -d で php.ini の設定を上書きできますので、以下の様に、sendmail コマンドのパスを先程の PHP スクリプトに設定して実行します。

$ php -d "sendmail_path=php input_vars.php" mail.php 

結果は以下のようになります。

* $envelope_from: [''\''mail&1@example.com'\''']
* argv :
Array
(
    [0] => input.php
    [1] => -f
    [2] => mail\&1@example.com
)
* stdin:
  To: test1@example.com
  Subject: subject
  X-PHP-Originating-Script: 1000:mail.php
  From: test2@example.com
  MIME-Version: 1.0
  Content-Type: text/plain; charset=ISO-2022-JP
  Content-Transfer-Encoding: 7bit
  
  body