2014年3月4日火曜日

正規表現によるバリデーションでは ^ と $ ではなく \A と \z を使おう

正規表現によるバリデーション等で、完全一致を示す目的で ^ と $ を用いる方法が一般的ですが、正しくは \A と \z を用いる必要があります。Rubyの場合 ^ と $ を使って完全一致のバリデーションを行うと脆弱性が入りやすいワナとなります。PerlやPHPの場合は、Ruby程ではありませんが不具合が生じるので \A と \z を使うようにしましょう。

はじめに

大垣さんのブログエントリ「PHPer向け、Ruby/Railsの落とし穴」には、Rubyの落とし穴として、完全一致検索の指定として、正規表現の ^ と $ を指定する例が、Ruby on Rails Security Guideからの引用として紹介されています。以下の正規表現は、XSS対策として、httpスキームあるいはhttpsスキームのURLのみを許可する正規表現のつもりです。
/^https?:\/\/[^\n]+$/i
しかし、Rubyの場合、以下の入力が上記正規表現にマッチしてしまいます。
javascript:exploit_code();/*
http://hi.com
*/
確かに、これはすごいワナです。なぜ、このようなことが起こるのでしょうか。その理由は下記の2点にあります。
  • メタ文字 ^ と $ は「行」の先頭と末尾を示す
  • Rubyの正規表現機能は、デフォルトで複数行モードである

^ と $ の意味

Rubyに限らずPerlやPHPでもそうですが、正規表現のメタ文字 ^ と $ は「行」の先頭・末尾を指します。文字列の先頭と末尾を指定する場合は、\A と \z を使用します。

デフォルトで複数行モード

こちらはRuby特有の仕様ですが、Rubyの正規表現は、デフォルトでPerlやPHPのm修飾子を指定したような動作となります。PerlやPHPの場合は、文字列の途中に改行があった場合でも、文字列全体を1行と見なします。一方、Rubyの場合や、PerlやPHPで正規表現の m修飾子を指定した場合は、改行を行の区切りと見なし、複数行として処理します。このため、/^xxx$/ (PerlやPHPの場合は/^xxx$/m )という正規表現は、「xxxに一致する行がある場合にマッチ」となります。
ちなみに、Rubyにも m修飾子がありますが、これはPerlやPHPの s修飾子にあたるもので、メタ文字ドット「.」が改行にマッチするようにする指令です。

先のPoCの場合、2行目に http://hi.com というhttpスキームのURLがあるため正規表現がマッチして、「正しい入力」とみなされたことになります。

XSS以外の脆弱性の可能性

この問題はXSS以外の脆弱性になる可能性があります。
  • SQLインジェクション: 数値の妥当性確認を正規表現で行っていて、"1\nOR 1=1"等の入力が来た
  • メールヘッダインジェクション: メールアドレスの妥当性を正規表現で実施していて、"a@example.jp\nSubject: hoge"等の入力が来た
  • HTTPヘッダインジェクション: リダイレクト先のURLを正規表現でチェックしていて、"http://example.jp/\nSet-Cookie: SESSIONID=ABC"等の入力が来た

どうすればよいか(Rubyの場合)

では、どうすればよいかというと、文字列のバリデーションなどに正規表現を用いる場合、^ と $ を使わずに、\A と \zを文字列の先頭・末尾を示すメタ文字として使用します。私の観測範囲では、Ruby界隈では元々よく知られている内容と思いますが、大垣さんの指摘のように、他の言語からRubyに移ってきた人には落とし穴になりそうです。

PerlやPHPの場合も ^ と $ を避けよう

大垣さんは、先に参照したエントリで「PHPのpreg_*()もmb_regex_*()も文字列データの開始と終端はそれぞれ^と$です」と書いておられますが、正確には、PHPの場合も ^ と $ は「行」の先頭と末尾を示します。大垣さんに限らず大半の方が、正規表現でのバリデーションに ^ と $ を使って完全一致マッチングを指定していますが、これは間違いということになります。
それでは、過去のPHP(やPerl等)のスクリプトが、これが原因で脆弱性だらけになるかというと、そうではありません。なぜなら、PHPやPerlの正規表現のデフォルトは単一行モードであり、文字列の途中の改行の前後で ^ や $ がマッチすることはないからです。

しかし、行の末尾に改行がある場合にも $ は(改行の直前に)マッチしてしまいます。すなわち、以下のPHPスクリプトは 1 (マッチした)を返します。
preg_match('/^[0-9]+$/', "123\n")
一方、以下は 0 (マッチしない)を返します。
preg_match('/\A[0-9]+\z/', "1234\n")
このように、^ と $で完全一致のチェックをしているつもりでも、データ末尾に改行が含まれている場合を見逃してしまうという問題があります。

書籍等の対応

私はRuby関連の書籍はあまり持っていませんが、初めてのRubyを確認すると、^ $ \A \z の意味が正しく説明されていました。これは、まぁyuguiさんなら当然でしょうが。
PHP関連の書籍はほぼ全滅のような気がしますが、PHP逆引きレシピ 第2版 (PROGRAMMER’S RECiPE)は正しい記述があります。第1版の方は間違っておりますので、まだ第1版をお使いの方は、この機に第2版を買いましょう(お勧め)。
追記。プロになるための PHPプログラミング入門も正しい記述です。徹底攻略PHP5技術者認定[上級]試験問題集[PJ0-200]対応には、上記の非常に詳しい解説があります。(追記終わり)
ちなみに、拙著体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践もw…と、これは自分で書いているので当然として、実はこの本を書くためにこのあたりを調べていたのでした。

まとめ

正規表現によるバリデーションをする場合、完全一致を示す目的でメタ文字 ^ と $ が多用されますが、これは間違いであり、\A と \z を用いるようにして下さい。^ と $ を用いた場合、Rubyの場合は脆弱性の原因となりやすく、他の言語の場合でも、データ末尾の改行をチェックできないと言う問題が生じます。

謝辞

正規表現の完全一致に \A と \z を使うべしという知識は、小飼弾氏から教えていただきました。私が、自分のブログエントリ『正規表現で「制御文字以外」のチェック』にて、「Perlの場合末尾の\nがうまくチェックできない」と書いたところ、小飼氏が「regexp - ^$でなくて\A\zを使おう」にて回答下さいました。あらためてお礼申し上げます。ありがとうございました。

0 件のコメント:

コメントを投稿

フォロワー

ブログ アーカイブ