PHP文字列をテキストとして出力したい場合もあります。PHPの文字列型はバイナリセーフなのでどのようなデータでも保存可能ですが、テキストとして出力するにはエスケープ処理が必要です。
エスケープ処理をしないでPHP文字列を出力すると不正なコードを実行される可能性があります。例えば、ユーザー入力を利用して設定ファイルを作成する場合にエスケープ処理をしないと脆弱になります。
0 1 2 3 4 5 6 7 8 9 |
<?php // ユーザー設定を設定ファイルに書き出す $config_filename = '../config.php'; $config = '<?php $admin_user = \''. $_GET['admin_user'] . '\''; file_put_contents($config_filename, $config); |
$_GET['admin_user']に
0 1 2 |
'; echo file_get_contents('/etc/passwd'); // |
が設定されていた場合、config.phpは以下のようになります。
0 1 2 3 |
<?php $admin_user = ''; echo file_get_contents('/etc/passwd'); // '; |
これをPHPスクリプトとして読み込むと、/etc/passwdファイルの中身が送信されてしまいます。
PHP文字列をテキストとして出力する場合、addslashes関数を利用します。addslashes関数はPHP文字列として出力する為にエスケープが必要な文字を \ (バックスラッシュ)でエスケープします。
addslashes関数がエスケープする文字
- ‘(シングルクォート)
- “(ダブルクォート)
- \(バックスラッシュ)
- NUL (ヌル文字)
PHPの任意コードが実行できる脆弱性を修正したスクリプトは以下の様になります。
0 1 2 3 4 5 6 7 8 9 |
<?php // ユーザー設定を設定ファイルに書き出す $config_filename = '../config.php'; $config = '<?php $admin_user = \''. addslashes($_GET['admin_user']) . '\''; file_put_contents($config_filename, $config); |
先ほどの攻撃用文字列をaddslashes関数を利用した場合の出力は
0 1 2 3 |
<?php $admin_user = '\'; echo file_get_contents(\'/etc/passwd\'); // '; |
となり、ユーザー入力の$_GET['admin_user']がプログラムの一部として解釈されず文字列として保存されます。
addslashes関数はPHP文字列が問題なく処理される為に必要な最低限の文字をエスケープします。PHPの文字列は以下のエスケープもサポートしています。
\n | linefeed (LF or 0x0A (10) in ASCII) |
\r | carriage return (CR or 0x0D (13) in ASCII) |
\t | horizontal tab (HT or 0×09 (9) in ASCII) |
\v | vertical tab (VT or 0x0B (11) in ASCII) (since PHP 5.2.5) |
\e | escape (ESC or 0x1B (27) in ASCII) (since PHP 5.4.0) |
\f | form feed (FF or 0x0C (12) in ASCII) (since PHP 5.2.5) |
\\ | backslash |
\$ | dollar sign |
\” | double-quote |
\[0-7]{1,3} | the sequence of characters matching the regular expression is a character in octal notation |
\x[0-9A-Fa-f]{1,2} | the sequence of characters matching the regular expression is a character in hexadecimal notation |
“(ダブルクォート)で囲まれた文字列とHeredoc、では$varが現れた場合、$varは変数の内容に置換されます。PHPは ‘ (シングルクォート)で囲まれた文字列とNowdocは文字列中に$が現れても変数の開始として処理しません。 $が現れてもエスケープする必要はありません。
0 1 2 3 4 5 6 7 8 |
<?php $var = 'XYZ'; echo '$var'; // $varを出力 echo '\$var'; // \$varを出力 echo "$var"; // XYZを出力 echo "\$var"; // $varを出力 |
エスケープ文字でない文字をエスケープした場合、エスケープ文字も出力されます。この仕様はPythonと同じです。参考:Ruby, Perlはエスケープ文字が削除されて出力されます。
0 1 2 3 |
<?php echo "\a\b\c"; |
は
0 1 2 |
\a\b\c |
を出力します。警告エラーなどは発生しません。
他のエスケープ方法
addslashes関数でエスケープする以外に、addcslash関数でエスケープしたり、バイナリをテキストに変換するbase64_encode関数、rawurlencode関数などを利用する事もできます。
PHP文字列となる変数をaddslashes関数でエスケープした場合、デコード処理は必要ありません。他の方法でエスケープした場合はデコード処理が必要になります。
var_export関数
PHP変数をテキストとして出力するvar_export関数を利用すれば、エスケープ処理されて出力されます。addslashes関数の代わりにvar_export関数を使用したコードも安全です。
0 1 2 3 4 5 6 7 8 9 |
<?php // ユーザー設定を設定ファイルに書き出す $config_filename = '../config.php'; $config = '<?php $admin_user = \''. var_export($_GET['admin_user'], true) . '\''; file_put_contents($config_filename, $config); |
$_GET['admin_user']には配列も設定できます。
例: http://example.com/update_config.php?admin_user[]=abc&admin_user[]=xyz
var_export関数はPHP変数であれば、配列・オブジェクトをPHPスクリプトとして読み込んでも安全な形でエクスポートします。
まとめ
変数をPHP文字列として保存する場合、エスケープ処理は必須です。エスケープ処理を行わないと、任意のPHPコードを実行される可能性があります。PHP文字列を保存する際には、エスケープ処理の有無に関わらず常にaddslashes関数などでエスケープしてから保存します。
エスケープ処理を自動的に行うvar_exportは便利ですが、配列やオブジェクトも問題なく出力します。この動作は意図しない場合もあるので、データ型をチェックするなどの注意が必要です。
追記
@kenji_s さんよりSJISを使った場合は
http://localhost/test.php?admin_user=%95′;%20$a=%3C%3C%3CEOL%0D%0A/etc/passwd%0D%0AEOL;%0D%0Aecho%20file_get_contents($a);%20//
な形で攻撃可能ですね、と指摘を頂きました。文字エンコーディングバリデーションは入力バリデーションで扱うつもりだったのですが、このエントリだけを見て脆弱なコードを書いてしまう方が出ては困ります。
どの文字エンコーディングを使っていても入力時にバリデーション(文字エンコーディングが正しいかを確認)しなければなりませんが、SJISなどの場合は特に注意が必要です。@kinji_sさん、良い指摘をありがとうございました。