なぜRubyと違い、PHPの正規表現で^$の利用は致命的な問題ではないのか?

(Last Updated On: 2018年8月13日)

Rubyデフォルトの正規表現では^は行の先頭、$は改行を含む行末にマッチします。PHPのPCREとmbregexでは^はデータの先頭、$は改行を含む行末にマッチします。

この仕様の違いはデータのバリデーションに大きく影響します。

参考: PHPer向け、Ruby/Railsの落とし穴 の続きの解説になります。こちらのエントリもどうぞ。

 

なぜ^と$が行の先頭と行の末尾にマッチするのか?

そもそも正規表現はテキスト検索を行うステートマシーンとして設計されました。通常テキストには改行があります。特定の行に一致するかどうかテストするように設計するのが自然です。この為、正規表現の^と$は行の先頭と末尾にマッチするように設計されたと考えられます。

正規表現をバリデーションに利用することは可能です。しかし、そもそもは正規表現に一致する「テキスト」を検索する為に作られた物なので、バリデーションに利用するには正規表現の仕様を理解する必要があります。

Rubyの場合、数値のみであることをバリデーションしようとして

^\d+$

を正規表現として利用した場合

Foo Bar\n
12345\n
<script>alert('XSS')</script>\n

のような入力を数値のみと判断してしまいます。

 

^、$を使うPHPの正規表現はほぼ安全

なぜ安全なのかは明らかです。PHPの正規表現(注:POSIX正規表現を除く)^はデータの先頭にマッチします。$は改行を含むデータの終端にマッチします。

^\d+$

を正規表現として利用し、次のデータが渡された場合

Foo Bar\n
12345\n
<script>alert('XSS')</script>\n

データの先頭は”Foo Bar”になり数値としてマッチしません。

12345\n
<script>alert('XSS')</script>\n

がデータとして渡された場合、終端となる行に

<script>alert('XSS')</script>\n

が含まれているので数値としてマッチしません。PHPで ^\d+$を利用した場合、マッチする文字列は

数字

または

数字\n

になります。(他の種類の改行文字は省略)つまり、PHPの場合は

正規表現にマッチするデータ

または

正規表現にマッチするデータ\n

にマッチします。PHPの場合はほぼ安全である事が分かります。Rubyの場合は

任意のデータ\n
正規表現にマッチするデータ\n
任意のデータ

にマッチします。「正規表現にマッチするデータ」の前後に自由に「任意のデータ」が 置けてしまいます。「任意のデータ」はJavaScriptインジェクション、SQLインジェクションなど、全てのインジェクション攻撃に利用可能である可能性があります。攻撃者にとっては非常に便利な状況です。

 

終端に改行が含まれるリスク − PHPとRuby

「PHPで^$を利用しても”ほぼ”安全」と書いている理由の解説になります。

誤って終端に改行が含まれてしまった場合、セキュリティ上のリスクはゼロではありません。HTTPヘッダーインジェクション(HTTPレスポンススプリッティング)やメールヘッダーインジェクションは不正な改行文字の挿入により攻撃が行われます。

しかし、PHPの場合、HTTPヘッダーは直接出力できず必ずheader関数を利用して出力しなければなりません。そしてheader関数にはHTTPヘッダーインジェクション対策が行われているので安全に出力されます。

mail/mb_send_mail関数も同様にメールヘッダーインジェクション対策が行われています。メール送信にパイプなどを利用していない限り、メールヘッダーインジェクションからも保護されています。

Rubyの場合、HTTP/メールヘッダーが保護されるかどうかはフレームワーク/プログラマ任せになります。

この他に出力データの形式が改行で壊される問題が発生するケースも考えられます。出力先の入力形式に合った出力を行わなかった場合、セキュリティ問題ではなくてもバグと言えます。

 

^$をバリデーション用の正規表現に使う間違い − PHPとRubyの違い

PHPの場合、^$をバリデーション用の正規表現に使ってしまう間違いは「単純なバグ」と言えまっす。しかし、Rubyの場合は「致命的なセキュリティバグ」です。

実は古いmbregex(忘れるくらい古い4.xの時代の話です)はRubyと全く同じ動作をしていました。この動作はセキュリティ上の問題であるとして、現在の^は\A(データの先頭)と同じ、$は\Z(改行を含む終端にマッチ)に変更されました。

昔の話なので議論の内容を記憶違いしているかも知れませんが、\z(改行を含まない終端にマッチ)を利用しなかった理由は以下だったと思います。

  • PCREの非マルチラインモードと同じ動作である(シングルラインモードがデフォルト)
  • 終端の改行のセキュリティリスクは低い

PCREの非マルチラインモードと同じ動作であることは、mbregexをPCREの代わりに利用するためには重要です。例えば、

<?php
$data = file('data_file.txt');
foreach($data as $line) {
  if (preg_match('/SOME_REGEX/', $data)) {
    // Some useful code here
  }
}

のようなコード中のpreg_match関数をmb_ereg関数で置き換えようとした場合、行末の改行を含む終端にマッチしないと、単純にmb_eregに置き換えれない問題が生まれます。この為、mb_ereg/eregi関数の$は\zではなく\Zにマッチする仕様になっています。

 

まとめ

  • Rubyでは^、$を使ったバリデーションは致命的なセキュリティバグ
  • PHPでは^、$を使ったバリデーションは単純なバグ 

バリデーションしたデータを例外的な使い方をしない限り、PHPではセキュリティ問題になりません。しかし、バリデーション後のデータに改行が含まれてはならない場合、バグであることには変わりません。他のプラットフォームで同じ正規表現を使う可能性なども考えると^、$ではなく\A、\zを利用した方が良いでしょう。

正規表現の^、$、\A、\z、\Zの仕様はどちらかというと常識的な知識かと思っていました。正規表現の解説文などで説明されていないことが多いことが原因で、あまり広くは知られていないようでした。出力先の入力(エスケープ)仕様と同様に、セキュリティに関連する機能の仕様は良く理解しておかないとセキュリティ上の問題になる、という良い例かも知れません。

最後に実際の動作例を書いておきます。

php > var_dump(mb_ereg('^\d+

 
, "1234\n"));
int(1)
php > var_dump(mb_ereg('^\d+

 
, "1234"));
int(1)
php > var_dump(mb_ereg('^\d+

 
, "1234\r\n"));
bool(false)
php > var_dump(mb_ereg('^\d+

 
, "1234\r"));
bool(false)
php > var_dump(mb_ereg('^\d+

 
, "1234a"));
bool(false)
php > var_dump(preg_match('/^\d+$/', "1234\n"));
int(1)
php > var_dump(preg_match('/^\d+$/', "1234"));
int(1)
php > var_dump(preg_match('/^\d+$/', "1234\r\n"));
int(0)
php > var_dump(preg_match('/^\d+$/', "1234\r"));
int(0)
php > var_dump(preg_match('/^\d+$/', "1234a"));
int(0)

 

投稿者: yohgaki