正規表現は危険です。様々なリスクが正規表現にはあります。簡単に正規表現リスクとより安全に使う方法を紹介します。
なぜ正規表現が危険なのか?
理由は単純です。
- 間違った正規表現を簡単に書ける
当たり前ですが正規表現は、正規表現に合った文字列を検索します。しかし、これが理由で簡単に間違え易いです。
随分少くなりましたが、正規表現マッチの最初と最後、の間違いは今でも多いです。正規表現APIの仕様により、 “^”/”$” が文字列の先端/終端であったり、”\A”/”\z”が文字列の先端/終端であったりします。これを間違えると、簡単にセキュリティ用のバリデーター/フィルターとして役立たなくなります。
- 仕様として危険
正規表現は仕様として危険です。柔軟かつ複雑な文字列マッチ処理を行う”仕様”が危険です。この為に簡単にDoS攻撃に脆弱になります。
例えば、PCREは柔軟かつ複雑な仕様によって可能になる多すぎるバックリファレンス、多すぎる再帰によるDoS攻撃を回数制限することによって防御できるようになっています。これで完璧かというとそうでもありません。その実例が次のブログです。
- 複雑な処理系
「仕様として危険」と関連しますが正規表現は複雑な処理を行います。セキュアコーディングでは複雑な処理系にデータを送る場合、その処理系が期待する妥当なデータを送信すること、を要求しています。
例えば、PCREはマルチバイト文字列のマッチを行う場合、UTF-8のみをサポートしています。文字エンコーディングが1つだけなので単純と言えるでしょう。しかし、それでも単純とは言いきれません。
PCREはUTF-8文字列としてマッチさせる場合、内部的に文字エンコーディングのバリデーションを行うようになっています。このバリデーションが非常に重たい処理になっています。ちょっと遅くなる、といったレベルの問題ではなく数百倍遅くなる、という場合もあります。数秒で完了可能な処理が数十分もかかってしまう場合があるのです。
この為、PCREにはこのバリデーション処理を省略する機能が付いています。HaskelのPCRE APIでは文字エンコーディングバリデーションを省略できます。PHPもPCREを多用しているので、Haskelのように文字エンコーディングバリデーションを省略できるようにしよう、と提案があったのですが「テストとしてLinuxのバイナリファイル全て(当然ですがUTF-8ととして妥当でないデータ)でマッチさせてみたらクラッシュするのでオプション導入はやめましょう」となりました。
※ 正規表現APIに文字列を渡す前に「文字エンコーディングのバリデーション」を行ってから文字列データを渡せば済むことです。Rubyは文字エンコーディング妥当性検証済みのデータを正規表現APIに渡すようになっています。PHPもそうすべきですが、PCRE(preg_*関数)の文字エンコーディングバリデーション機能に頼る、Oniguruma(mb_regex_*関数)にパッチを当てて不正エンコーディングを無視する、といった実装になっています。preg/mb_regexのどちらも不正文字エンコーディングでは”正しく動作できない”ので、そもそも文字エンコーディングをバリデーションしておくべきです。
正規表現をより安全に使う方法
- 間違った正規表現を簡単に書ける
- 仕様として危険
- 複雑な処理系
これらにより正規表現の利用が危険になっています。複雑な問題があるなら、問題を個別に分解して対応するのがエンジニアでしょう。(と書いてセキュアコーディングを誤解する理由に思い当たりました。これは別の機会に書きます)
- 間違った正規表現を簡単に書ける
代表例が文字列の先端/終端を指定の間違いです。これの対策は結構簡単です。Scalaの正規表現は明示的に指定しない限り正規表現でマッチする文字列は文字列全体になります。例えば、”\d”とするだけで”\A\d\z”と同じ意味になります。間違えようがありません。
- 仕様として危険
バックリファレンス/再帰の回数を制限するのは勿論ですが、それだけでは足りません。簡易な正規表現/リスクを考えた正規表現を使うことも必要ですが、リスクを回避/緩和するには入力データにおかしたデータ(=攻撃用データ)が含まれないことを保証する対策が有効です。これには正規表現APIを利用する”前”に厳格な入力バリデーションを行います。
- 複雑な処理系
正規表現の処理は複雑です。”正規表現データ”を処理する為には、まず”正規表現”を処理できるように”正規表現”自体をコンパイル(パースして効率的に処理できる形に変換)してから利用します。このコンパイル処理にも結構なCPUリソースを利用するのでPCREなどの正規表現ライブラリはコンパイル結果をキャッシュして再利用するようになっています。こういった最適化は正規表現ライブラリ任せ、で構わないでしょう。
しかし、どうしようもない問題もあります。例えば、PCREはUnicode文字列をマッチさせる場合のバリデーションを省略し出鱈目なデータ、バイナリデータなど、を与えるとクラッシュすることが判っています。比較的最近でもOnigurumaのメモリ管理が問題となった事例もあります。
メモリ問題が無かったとしても、大きすぎるデータを与えられると正規表現の実行に多大なCPU時間が必要となる状況を回避することが困難になります。
「仕様として危険」と同じく、正規表現APIを利用する”前”に厳格な入力バリデーションを行うと対策になります。文字エンコーディング処理は複雑です。正規表現APIの内部で問題となり得る文字エンコーディング処理のバリデーション処理を任せるとリスクが発生します。これを避ける為には、入力データとなる文字列の”文字種”および”文字エンコーディング”を予め限定/バリデーションするとリスクを回避/緩和できます。
対策: 正規表現APIを最初のバリデーターとして使わない
入力データを正規表現でバリデーションしているコードは多いと思います。置換処理を除けば、”多い”というより大半の正規表現処理はバリデーション処理でしょう。正規表現の入力データとして”自由に入力データを設定することを許可”すると思いもよらない問題が発生したり、間違えたりします。
”自由に入力データを設定することを許可”すると思いもよらない問題が発生するなら、”自由に入力データを設定させない”様にすれば良いです。つまり、正規表現APIを最初のバリデーターとして利用するのではなく
- 明示的に許可した場合に限り、改行文字(\n、\r)を許可する
- 明示的に許可した長さの入力データのみ許可する。長過る(短過ぎる、も含む)入力データを許可しない
- 明示的に許可し文字種のみ許可する
- 明示的に許可した文字エンコーディングのみ許可する
このような対策を行えば良いことが分かります。
これらの処理を正規表現APIを利用する前に適用するのは結構面倒です。簡単に処理できなければ正規表現だけで済ませたくなります。こういった場合にはライブラリ化でしょう。
PHP用の入力バリデーションライブラリのValidate for PHPはこれらの要件を満すライブラリになっています。バリデーターとして足りない機能はUnicodeのスクリプト(文字種)指定ができない点です。Unicodeの制御文字などもデフォルトで許可しないようになっています。Unicode文字だけでなくASCII英数字も、明示的に指定しないと利用できません。
Leave a Comment