正規表現チェッカー
正規表現テスターは、パターンを対象テキストにマッチさせます。処理はブラウザ内、Web Workerの中で、2秒のタイムアウト付きで実行されます。これは慎重さのためだけではありません。JavaScriptには実行中の正規表現を中断する手段がなく、(a+)+$のようなパターンを長い入力に適用すると、スレッドを無期限に占有します。Workerを強制終了することだけが、実際に機能する防御です。
正規表現チェッカーはどう動きますか?
ほとんどの正規表現テスターは、パターンをページのメインスレッドで実行します。バックトラッキングするパターンを誰かが貼り付けた瞬間まで、それで問題ありません。タブは応答しなくなり、対処法はタブを閉じるしかなくなります。
破局的バックトラッキングと、捕まえられない理由
JavaScriptの正規表現エンジンはバックトラックします。マッチが失敗すると、すべての代替経路を再試行します。aの連続の末尾が別の文字になっている入力に(a+)+bを適用すると、経路の数は2つの量指定子の間で連続部分を分割するあらゆる方法、つまり指数関数的になります。30文字で約10億回の試行です。タイムアウトオプションも、中断シグナルも、処理を譲る場所も、例外もありません。エンジンは単純に返ってこないのです。
これがReDoS脆弱性のクラスであり、実際のシステムを落とします。Cloudflareの2019年の世界的障害は、1つのWAFルール内の1つの正規表現が原因でした。Stack Overflowの2016年の障害は、長い空白の連続を含む投稿に対する、空白にマッチするパターンでした。どちらもサーバー上で発生したもので、結果は1つのタブではなく、すべてのリクエストがブロックされることになります。
Workerを強制終了できることが設計のすべてです
そこでマッチ処理はWeb Worker内で実行され、2秒後にタイマーが強制終了します。これが使える唯一の仕組みです。実行中の正規表現を止めることはできませんが、スレッドなら破棄できます。タイムアウト時には、凍りついたタブの代わりに、原因と思われるものを明示したメッセージが表示されます。これ自体も有益なシグナルです。ここでタイムアウトするパターンは、サーバーをハングさせるパターンだからです。
静的解析が示すのは形であって判定ではありません
リスクパネルは、ネストした量指定子、分岐が重なり合う量指定子付きの選択、同じ文字に隣接する量指定子を検出します。実際の事故はほぼすべて、このパターンに分類されます。一方で、このパネルが意図的にしないのは「このパターンは安全」と宣言することです。任意の正規表現が指数関数的な最悪時の挙動を持つかどうかの判定は本物の解析問題であり、安全性を保証すると謳うツールは、守れない約束をしていることになります。
長さゼロのマッチにはひと手間が必要です
何にもマッチしえないパターン——a*、(?:)、\b——をグローバルマッチのループにかけると、lastIndexが前進せず、無限ループになります。正しい実装はすべて、空マッチの後に手動でlastIndexを進めており、このツールもそうです。些細なことですが、驚くほど多くのツールがここで壊れています。
分解表示はトークン単位です
パターンはトークンに分割され、それぞれに平易な説明が付きます。他人が書いた正規表現の典型的な問題は、間違っていることではなく読めないことだからです。リテラル文字の連続は、1文字ずつ列挙するのではなくひとまとめに表示します。
パターン
(\w+)@(\w+\.\w+)
マッチ結果
1 [email protected] at 58 $1 ada $2 example.com 2 [email protected] at 271 $1 grace $2 example.org
正規表現チェッカーが対応するオプションとエッジケースは?
| パラメータ | 型 | デフォルト | 動作とエッジケース |
|---|---|---|---|
| g(グローバル) | フラグ | オン | 最初の一致だけでなく、すべての一致を見つけます。このフラグがないと、いくつ一致があっても結果は1つしか返されません。 |
| i(大文字小文字を無視) | フラグ | オフ | 大文字と小文字を区別しません。uフラグと組み合わせると、ASCIIより広い範囲を扱えるUnicodeのケースフォールディングも適用されます。 |
| m(複数行) | フラグ | オン | ^と$は、対象テキストの両端だけでなく、すべての改行位置で一致します。ログ形式の入力では、ほぼ常に有効にしたいフラグです。 |
| s(dotAll) | フラグ | オフ | 「.」が改行にも一致するようになります。このフラグがないと「.*」は行末で止まるため、複数行のマッチが失敗するいちばん一般的な原因になります。 |
| u(Unicode) | フラグ | オフ | \u{…}エスケープと\p{…}プロパティクラスが使えるようになり、エンジンがUTF-16単位ではなくコードポイント単位で動作します。「.」が絵文字1つ全体に一致するのはこのためです。 |
| y(sticky) | フラグ | オフ | lastIndexの位置でしか一致せず、後続を走査しません。トークナイザーでの使用を想定したフラグで、隙間があったら読み飛ばすのではなく構文エラーとして扱います。 |
| タイムアウト | ミリ秒 | 2000 | この時間を超えるとワーカーは強制終了されます。遅いパターンと指数関数的なパターンを事前に見分ける方法はなく、経過時間だけが唯一の判断材料です。 |
| 一致数の上限 | 件数 | 5000 | この件数を超えると結果は切り詰められ、そのことが通知されます。5万行を描画しても誰の役にも立ちません。 |
| 置換 | $1, $<name>, $& | — | 番号付きグループ、名前付きグループ、マッチ全体を参照できます。$$はドル記号そのもので、これがいちばん忘れがちです。 |
よくある質問
パターンがタイムアウトしたのはなぜですか?
ほぼ間違いなく、破局的バックトラッキングが起きています。量指定子付きグループの中の量指定子——(a+)+、(\d*)*——や、分岐が同じテキストにマッチしうる量指定子付きの選択、たとえば(a|ab)+を探してください。エンジンは両者の間で入力を分割するあらゆる方法を試し、入力長に対して指数関数的なコストがかかります。見つけられれば、リスクパネルがその形を名指しして表示します。
バックトラックするパターンはどう直せばよいですか?
どの部分が何にマッチするかの曖昧さを取り除きます。(a+)+をa+に置き換える。選択の分岐を排他的にして、各位置で1つの分岐しかマッチできないようにする。パターンにアンカーを付けて、失敗したときにすべての文字位置から再試行されないようにする。開いたままの繰り返しは、データが実際に含む範囲に制限する。そして、ワイルドカードよりも具体的な文字クラスを優先してください。二重引用符を除外する[^"]*は、.*よりもはるかにバックトラックが少なくなります。
ReDoSは実際のリスクですか、それとも理論上のものですか?
実際のリスクであり、しかも高くつきます。Cloudflareの2019年7月の世界的障害は、WAFルール内の1つの正規表現が原因でした。Stack Overflowは2016年に、末尾の空白にマッチするパターンが、長い空白の連続を含む投稿で発火してダウンしました。サーバー上では、障害の進み方はここより深刻です。すべてのリクエストが、エンジンで立ち往生した1件の後ろに列を作るため、細工された入力1つでサービス全体が落ちます。
.*が行をまたいでマッチしないのはなぜですか?
ドット(.)は、sフラグが有効でない限り改行にマッチしないからです。これはJavaScriptの正規表現で最もよくある驚きであり、mフラグと混同しやすい点でもあります。ただしmが変えるのは^と$の意味であって、.のマッチ対象ではありません。複数行にまたがるマッチにはs、行を基準にしたマッチにはmが必要です。2つは独立しており、両方指定することもよくあります。
メールアドレスの検証に正規表現を使うべきですか?
緩いチェックに限ります。RFC 5322のアドレス文法をパターンにすると数百文字になり、引用符付きのローカル部やコメント、IPリテラルのドメインなど、誰も想定しない形式を受け入れながら、それでもメールボックスが実在するかどうかは分かりません。実用になるパターンは、前後に何かがある@と、ドメイン内のドットを確認する程度のものです。本当の検証は確認メールの送信です。
パターンや対象テキストはアップロードされますか?
いいえ。マッチ処理はブラウザ内のWeb Worker——同じタブの中の別スレッド——で実行され、サーバーではありません。Networkパネルで確認できます。これが重要なのは、テストに使われる対象テキストが本番環境のログであることが多く、本番ログにはメールアドレス、トークン、顧客識別子が含まれているからです。