mb_ereg_replace関数でe修飾子を使う際の注意点
mb_ereg_replace関数でe修飾子を使ってevalする場合の注意点をまとめてみました。
概要
マニュアルの下記引用部にもあるとおり、mb_ereg_replace関数はe修飾子の指定があっても特にエスケープなどを行いません。
信頼できない入力に対しては、絶対に e 修正子を使用してはいけません。 (preg_replace() と同様、) 自動的なエスケープは行いません。このことを忘れていると、自分の書いたアプリケーションにリモートコード実行の脆弱性を作りこんでしまうことになります。
preg_replaceでe修飾子をつけた場合、後方参照について自動でaddslashes相当のエスケープを行ってくれます(「例えばPHPのpreg_replace関数でe修飾子を避ける」参照)ので、この点において両者は挙動が異なっているわけです。
"\0"、"'"、'\'の3文字に注意
このような挙動のため、preg_replaceでは可能なことでもmb_ereg_replaceにはできないことがあります。以前の記事と同じ例題「空白文字以外が連続する部分文字列の後ろに、その部分文字列の長さを括弧付きの数字で追記する」をmb_ereg_replaceで実装すると次のようになります。
<?php $str = 'abc 12345'; $str = mb_ereg_replace('(\S+)', "'\\1('.strlen('\\1').')'", $str, 'e'); echo $str, "\n"; // abc(3) 12345(5)
しかし、上のプログラムが"\0"(ヌル文字)、"'"(シングルクォート)、'\'の3文字のいずれかがマッチした場合には期待通りには動きません。エラーになるか、期待と異なる結果になってしまいます。
Parse error: syntax error, unexpected T_ENCAPSED_AND_WHITESPACE in /tmp/hoge.php(3) : mbregex replace on line 1 Fatal error: mb_ereg_replace(): Failed evaluating code: ' in /tmp/hoge.php on line 3
上の例はヌル文字をマッチさせた場合の例です。これを防ぐには、これらの文字をマッチさせないように工夫するしかありません。たとえば事前にエスケープしておくなどです。
mb_ereg_replace_callback関数を自作してみる
以前の記事では、preg_replace関数に関して、e修飾子を使うかわりにpreg_replace_callback関数を使うことをお勧めしました。今回はmb_ereg_replace関数のe修飾子についても問題となる状況があることを指摘しましたが、mb_ereg_replace関数についてはpreg系のときのような代替の関数が存在しません。
そこで、preg_replace_callback関数のmb_ereg版を自作してみました。
<?php function mb_ereg_replace_callback($pattern, $callback, $subject, $limit = -1, $option = null) { if ($option === null) { $option = mb_regex_set_options(); } mb_ereg_search_init($subject, $pattern, $option); $unmatched_start_pos = 0; // マッチしなかった部分とマッチした部分を交互に格納する配列 $replacement_substrings = array(); while ($limit !== 0 && $pos_info = mb_ereg_search_pos()) { list($matched_start_pos, $matched_length) = $pos_info; // 正規表現にマッチしなかった部分を取得する $unmatched_length = $matched_start_pos - $unmatched_start_pos; $unmatched = mb_strcut($subject, $unmatched_start_pos, $unmatched_length); $replacement_substrings[] = $unmatched; // 正規表現マッチした部分について置換用関数を呼び出す $matches = mb_ereg_search_getregs(); $replacement = call_user_func($callback, $matches); $replacement_substrings[] = $replacement; // 次にマッチングを開始する位置を記録 $unmatched_start_pos = mb_ereg_search_getpos(); $limit--; // 0になったらループ脱出 } $unmatched = mb_strcut($subject, $unmatched_start_pos); $replacement_substrings[] = $unmatched; return implode('', $replacement_substrings); }
このプログラムを書いていて気づいたことですが、mb_ereg_search_pos関数およびmb_ereg_search_getpos関数はマルチバイトの文字数でなくバイト数を返します。マニュアルにも明記してあるのですが、ちょっと使いにくい気がしますね。おそらく鬼車*1の制約なんでしょう。プログラム中でmb_substr関数でなくmb_strcut関数を使っているのはそのためです。
この関数はpreg_replace_callback関数と全く同じように動作します。
<?php require_once('mb_ereg_replace_callback.php') mb_internal_encoding("UTF-8"); mb_regex_encoding("UTF-8"); $str = 'abc 12345 あいう #",; $foo'; $str = mb_ereg_replace_callback('(\S+)', function($m){return $m[1].'('.strlen($m[1]).')';}, $str); echo $str, "\n"; // abc(3) 12345(5) あいう(9) #",;(4) $foo(4)
一通りこれを作ってから気づいたのですが、id:rskyさんが5年以上前に同じものを作っていました(「mb_ereg_replace_callback() - 讃容日記」)。仕様が少し違いますけど、実装も限りなく一緒です。悲しい…。
まあ、僕の実装を使うかどうかはさておき、このような関数を作れば一件落着というわけです。そもそも内部エンコーディングがUTF-8ならpreg_replace_callback関数を使った方がいいと思いますが…。