どうすれば邪悪な正規表現を認識できますか?


83

私は最近、正規表現のサービス拒否攻撃に気づき、コードベースで見つけられる場所、または少なくともユーザー入力で使用される、いわゆる「悪」の正規表現パターンを根絶することにしました。上記のOWASPリンクウィキペディアにある例は役に立ちますが、問題を簡単な言葉で説明するのに役立ちません。

ウィキペディアからの邪悪な正規表現の説明:

  • 正規表現は、複雑な部分式に繰り返し( "+"、 "*")を適用します。
  • 繰り返される部分式の場合、別の有効な一致の接尾辞でもある一致が存在します。

例を挙げて、再びウィキペディアから:

  • (a+)+
  • ([a-zA-Z]+)*
  • (a|aa)+
  • (a|a?)+
  • (.*a){x} x> 10の場合

これは簡単な説明がないだけの問題ですか?正規表現を作成するときにこの問題を回避したり、既存のコードベース内で正規表現を見つけたりするのが簡単になるものを探しています。


7
このトピックに関する別のリンクは次のとおりです。regular
expressions.info

1
正規表現で静的分析を実行して、疑わしいReDoSの問題を発見するためのツールは次のとおり
〜hxt

@tripleeeによって提供されたリンクには、RXXRツールへのリンクが壊れているようです。ここではGitHubの鏡だ:github.com/ConradIrwin/rxxr2
マイク・ヒル

3
さらに、好奇心旺盛な人にとっては、元のRXXRツールの作成者がRXXR2に取って代わったようです。彼らの新しいページがここでホストされており、現在RXXR2ソースに取り組んでリンクしていない:cs.bham.ac.uk/~hxt/research/rxxr2
マイク・ヒル

回答:


76

なぜ邪悪な正規表現が問題なのですか?

コンピュータは、たとえそれがあなたが意図したものではなく、まったく不合理であっても、あなたが彼らに指示したことを正確に行うからです。正規表現エンジンに、特定の入力について、特定のパターンに一致するものと一致しないものがあることを証明するように依頼すると、エンジンは、テストする必要のあるさまざまな組み合わせの数に関係なく、それを実行しようとします。

これは、OPの投稿の最初の例に触発された単純なパターンです。

^((ab)*)+$

与えられた入力:

abababababababababababab

正規表現エンジンは次のようなこと(abababababababababababab)を試行し、最初の試行で一致が見つかります。

しかし、次にモンキーレンチを投入します。

abababababababababababab a

エンジンは最初に試行(abababababababababababab)しますが、その余分なために失敗しますa。これは壊滅的なブラックトラッキングを引き起こします。なぜなら、私たちのパターンは(ab)*、誠意を持って、キャプチャの1つを解放し(「バックトラック」し)、外側のパターンを再試行させるからです。正規表現エンジンの場合、次のようになります。

(abababababababababababab)-いいえ
(ababababababababababab)(ab)-いいえ
(abababababababababab)(abab)-いいえ
(abababababababababab)(ab)(ab)-いいえ
(ababababababababab)(ababab)-いいえ
(ababababababababab)(abab)(ab)-いいえ
(ababababababababab)(ab)(abab)-いいえ
(ababababababababab)(ab)(ab)(ab)-いいえ
(abababababababab)(abababab)-いいえ
(abababababababab)(ababab)(ab)-いいえ
(abababababababab)(abab)(abab)-いいえ
(abababababababab)(abab)(ab)(ab)-いいえ
(abababababababab)(ab)(ababab)-いいえ
(abababababababab)(ab)(abab)(ab)-いいえ
(abababababababab)(ab)(ab)(abab)-いいえ
(abababababababab)(ab)(ab)(ab)(ab)-いいえ
(ababababababab)(ababababab)-いいえ
(ababababababab)(abababab)(ab)-いいえ
(ababababababab)(ababab)(abab)-いいえ
(ababababababab)(ababab)(ab)(ab)-いいえ
(ababababababab)(abab)(abab)(ab)-いいえ
(ababababababab)(abab)(ab)(abab)-いいえ
(ababababababab)(abab)(ab)(ab)(ab)-いいえ
(ababababababab)(ab)(abababab)-いいえ
(ababababababab)(ab)(ababab)(ab)-いいえ-いいえ-いいえ-いいえ-いいえ-いいえ-いいえ
(ababababababab)(ab)(abab)(abab)-いや
(ababababababab)(ab)(abab)(ab)(ab)-いや
(ababababababab)(ab)(ab)(ababab)-いや
(ababababababab)(ab)(ab)(abab)(ab)-いや
(ababababababab)(ab)(ab)(ab)(abab)-いや
(ababababababab)(ab)(ab)(ab)(ab)(ab)-いや
                              ...
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abababab) -いや
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ababab)(ab)-いや
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)(abab)-いや
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)(ab)(ab)-いや
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ababab)-いや
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)(ab)-いや
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)-いや
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)-いや

可能な組み合わせの数は入力の長さに応じて指数関数的に増加し、正規表現エンジンは、考えられるすべての用語の組み合わせを使い果たして最終的にあきらめるまで、この問題を解決しようとしてすべてのシステムリソースを使い果たします。 「一致するものはありません」と報告します。その間、サーバーは溶けた金属の燃える山に変わりました。

邪悪な正規表現を見つける方法

それは実際には非常にトリッキーです。私はそれらが何であるか、そして一般的にそれらを避ける方法を知っていますが、私は自分でカップルを書きました。驚くほど長い時間がかかる正規表現を参照してください。アトミックグループで可能なすべてをラップすると、バックトラックの問題を防ぐのに役立ちます。これは基本的に、正規表現エンジンに、指定された式に再度アクセスしないように指示します。「最初の試行で一致したものはすべてロックします」。注意は、しかし、原子式はバックトラックを妨げないことの中に表現なので、^(?>((ab)*)+)$まだ危険ですが、^(?>(ab)*)+$(それが一致するよ安全です(abababababababababababab)し、それがこのように壊滅的なバックトラッキングを防ぐ、文字にマッチしていますのいずれかを放棄することを拒否します)。

残念ながら、一度作成すると、問題の正規表現をすぐにまたはすばやく見つけることは実際には非常に困難です。結局のところ、悪い正規表現を認識することは、他の悪いコードを認識することと同じです。多くの時間と経験、および/または単一の壊滅的なイベントが必要です。


興味深いことに、この回答が最初に書かれて以来、テキサス大学オースティン校のチームは、これらの「邪悪な」パターンを見つけるという明確な目的で正規表現の静的分析を実行できるツールの開発について説明した論文を発表しました。このツールはJavaプログラムを分析するために開発されましたが、特にReDoS攻撃割合が増え続けるにつれて、JavaScriptやその他の言語で問題のあるパターンを分析および検出するために開発されるツールが今後さらに増えると思います。

正規表現を使用するプログラムでのDoS脆弱性の静的検出
ValentinWüstholz、Oswaldo Olivo、Marijn JH Heule、およびIsilDillig
テキサス大学オースティン校


これは、/なぜ/正規表現の例を説明するのに非常に良い答えですが、問題の正規表現を認識するために人が内部化できるいくつかのルールを探しています。
マイクパートリッジ

4
「なぜ」を知ることは、「邪悪な」正規表現を書かないようにするための最も重要なステップです。残念ながら、一度作成すると、問題の正規表現をすぐにまたはすばやく見つけることは実際には非常に困難です。包括的な修正が必要な場合は、通常、アトミックグループ化が最善の方法ですが、正規表現が一致するパターンに大きな影響を与える可能性があります。結局のところ、悪い正規表現を認識することは、他の悪いコードの正規表現と同じです。多くの経験、多くの時間、および/または単一の壊滅的なイベントが必要です。
JDBはまだモニカ覚え

これが、ユーザーが強制せずにバックトラックをサポートしない正規表現エンジンを好む理由です。IE lex / flex。
スペンサーラスブン2013年

@MikePartridgeこれは一般的なITの古典的な理論の問題であり、一部のコードが無限にループするか停止するかを決定することは、NP完全な種類の問題です。正規表現を使用すると、特定のパターン/ルールを検索することでそれらの一部を推測/キャッチできますが、NP完全分析を行わない限り、すべてをキャッチすることはできません。いくつかのオプション:1)ユーザーがサーバーに正規表現を入力できないようにします。2)十分に早く計算を終了するように正規表現エンジンを構成します(ただし、厳しい制限があっても、コードで有効な正規表現をテストしても機能します)。3)CPU / mem制限のある優先度の低いスレッドで正規表現コードを実行します。
ped7g 2017年

1
@ MikePartridge-最近、これらの問題のある正規表現を静的に検出するために開発されているいくつかの新しいツールに関する論文に出くわしました。面白いもの...フォローする価値があると思います。
JDBは、2018

12

あなたが「邪悪な」正規表現と呼ぶものは、壊滅的なバックトラックを示す正規表現です。リンク先のページ(私が書いた)は、概念を詳細に説明しています。基本的に、壊滅的なバックトラッキングは、正規表現が一致せず、同じ正規表現の異なる順列が部分的な一致を見つけることができる場合に発生します。次に、正規表現エンジンはこれらすべての順列を試行します。コードを調べて正規表現を調べたい場合は、次の3つの重要な問題を確認してください。

  1. 代替案は相互に排他的でなければなりません。複数の選択肢が同じテキストに一致する可能性がある場合、正規表現の残りの部分が失敗すると、エンジンは両方を試行します。選択肢が繰り返されるグループに含まれている場合は、壊滅的なバックトラックが発生します。古典的な例は(.|\s)*、正規表現フレーバーに「ドットが改行に一致する」モードがない場合に、任意の量のテキストに一致することです。これがより長い正規表現の一部である場合、十分に長いスペースの実行(との両方.で一致\s)を持つサブジェクト文字列は正規表現を壊します。修正は(.|\n)*、代替を相互に排他的にするために使用するか、[\r\n\t\x20-\x7E]ASCII印刷可能文字、タブ、改行など、実際に許可される文字をより具体的にするために使用することです。

  2. 順番に並んでいる定量化されたトークンは、相互に排他的であるか、それらの間にあるものが相互に排他的である必要があります。それ以外の場合、両方が同じテキストに一致する可能性があり、正規表現の残りの部分が一致しない場合、2つの数量詞のすべての組み合わせが試行されます。古典的な例はa.*?b.*?c、3つのものをそれらの間の「何でも」と一致させることです。ときにc最初に一致させることができない.*?行またはファイルの最後まで文字で文字を拡大していきます。展開ごとに、2番目.*?は行またはファイルの残りの部分と一致するように文字ごとに展開します。修正は、それらの間に「何か」を置くことはできないことを認識することです。最初の実行はで停止する必要がbあり、2番目の実行はで停止する必要がありますc。1文字でa[^b]*+b[^c]*+c簡単な解決策です。区切り文字で停止するので、所有格の数量詞を使用してパフォーマンスをさらに向上させることができます。

  3. 数量詞を含むトークンを含むグループは、グループ内の数量化されたトークンが相互に排他的な他のものとのみ一致する場合を除いて、独自の数量詞を持ってはなりません。これにより、内部数量詞の反復回数が多い外部数量詞の反復回数が少なくなり、内部数量詞の反復回数が少ない外部数量詞の反復回数が多いのと同じテキストに一致する方法がなくなります。これは、JDBの回答に示されている問題です。

私が答えを書いている間、私はこれが私のウェブサイトの完全な記事に値すると決めました。これもオンラインになりました。


10

私はそれを「繰り返しの繰り返し」と要約します。あなたがリストした最初の例は、「文字a、1回以上続けて。これも1回以上続けて起こる可能性がある」と述べているので良い例です。

この場合に探すのは、*や+などの数量詞の組み合わせです。

注意すべきもう少し微妙なことは、3番目と4番目のものです。これらの例にはOR演算が含まれており、両側が真になる可能性があります。これを式の数量詞と組み合わせると、入力文字列に応じて、多くの潜在的な一致が発生する可能性があります。

要約すると、TLDRスタイル:

数量詞を他の演算子と組み合わせて使用​​する方法に注意してください。


3
現在、この答えは私が探しているものに最も近いものです。壊滅的なバックトラックを引き起こす可能性のある正規表現を認識するための経験則です。
マイクパートリッジ

1
あなたが省略したこと、そして問題の重要な部分であると思われることは、グループをキャプチャすることです。
マイクパートリッジ

@MikePartridgeそれも。なるべく煮詰めようとしたので、グループをキャプチャするなど、同じことが起こる原因は他にもあります。
Jarmund 2012年

7

驚くべきことに、ソースコードレビューを実行するReDOSに何度も遭遇しました。私がお勧めすることの1つは、使用している正規表現エンジンでタイムアウトを使用することです。

たとえば、C#では、TimeSpan属性を使用して正規表現を作成できます。

string pattern = @"^<([a-z]+)([^<]+)*(?:>(.*)<\/\1>|\s+\/>)$";
Regex regexTags = new Regex(pattern, RegexOptions.None, TimeSpan.FromSeconds(1.0));
try
{
    string noTags = regexTags.Replace(description, "");
    System.Console.WriteLine(noTags);
} 
catch (RegexMatchTimeoutException ex)
{
    System.Console.WriteLine("RegEx match timeout");
}

この正規表現はサービス拒否に対して脆弱であり、タイムアウトがないとスピンしてリソースを消費します。RegexMatchTimeoutExceptionタイムアウトを使用すると、指定されたタイムアウトの後にがスローされ、リソース使用量がサービス拒否状態につながることはありません。

タイムアウト値を試して、使用法に合っていることを確認することをお勧めします。


7

邪悪な正規表現の検出

  1. NicolaasWeidemanのRegexStaticAnalysisプロジェクトをお試しください。
  2. WeidemanのツールなどのCLIを備えたアンサンブルスタイルのvuln-regex-detectorを試してみてください。

経験則

邪悪な正規表現は常に、対応するNFAのあいまいさが原因であり、regexperなどのツールを使用して視覚化できます。

ここにいくつかの形のあいまいさがあります。これらを正規表現で使用しないでください。

  1. (a+)+(別名「星の高さ> 1」)のような入れ子の数量詞。これにより、指数関数的な爆発が発生する可能性があります。サブスタックのsafe-regexツールを参照してください。
  2. のような定量化された重複論理和(a|a)+。これにより、指数関数的な爆発が発生する可能性があります。
  3. のような定量化された重複する隣接関係は避けてください\d+\d+。これは、多項式の爆発を引き起こす可能性があります。

追加のリソース

この論文は超線形正規表現について書きました。これには、他の正規表現関連の研究への参照が多数含まれています。


4

これは、使用中の正規表現エンジンに関連していると思います。これらのタイプの正規表現を常に回避できるとは限りませんが、正規表現エンジンが正しく構築されていれば、それほど問題にはなりません。このブログシリーズを見る正規表現エンジンのトピックに関する多くの情報については、を。

バックトラックはNP完全問題であるという点で、記事の下部にある警告に注意してください。現在、それらを効率的に処理する方法はありません。入力でそれらを禁止することをお勧めします。


a*a*後方参照を使用しません。さて、正規表現エンジンはバックトラッキングを使用します。これはおそらく、あなたが意味したことですか?その場合、すべての最新のエンジンはバックトラッキングを使用します。を介してバックトラックを簡単に無効にすることができますが(?>...)、それによって式の意味が変わらないことがよくあります(場合によっては回避できます)。
JDBは

@ Cyborgx37おっと!私はバックトラックを意味しました。修繕。
スペンサーラスブン2013年

その場合、エンジンはバックトラッキングを使用するか、使用しません。入力を制限することによってバックトラックを制限する方法は事実上ありません。
JDBは

2
@JDB:「最新のエンジンはすべてバックトラッキングを使用しています。」-たぶんそれは2013年には真実でした、もはやそうではありません
ケビン

@ケビン-確かに。あなたが勝ちます。
JDBはまだモニカ覚えて

3

そのような正規表現は、少なくともすべてではなく、表現力を制限することなく認識できるとは思いません。ReDoSが本当に気になる場合は、サンドボックス化して、タイムアウトで処理を強制終了しようとします。最大バックトラッキング量を制限できるRegEx実装がある可能性もあります。


2
あなたはその質問を誤解していると思います。私がそれを読んでいるとき、OPは文字通り、彼がそうするためのプログラムを書くことができる方法ではなく、彼が邪悪な正規表現を認識することができる方法尋ねています。たとえば、「この正規表現を作成しましたが、それが悪である可能性があるかどうかをどのように判断できますか?」
ruakh 2012年

ええと、あなたは正しいかもしれません。次に、@ DanielHilgarthがコメントですでにリンクしている壊滅的なバックトラックに関する記事のみを推奨できます。
ベルギ2012年

2
@ 0x90:たとえば、a*または\*「脆弱」であるとは考えていないためです。
ruakh 2013年

1
@ 0x90a*はまったく脆弱ではありません。その間、a{0,1000}a{0,1000}壊滅的な正規表現が起こるのを待っています。でも、a?a?右の条件の下で厄介な結果を持つことができます。
JDBは、2013

2
@ 0x90-壊滅的なバックトラッキングは、一方が他方と同一またはサブセットであり、式の長さが可変であり、1つまたは複数の文字を放棄できるように配置されている2つの式がある場合は常に危険です。その他はバックトラックを介して。たとえば、a*b*c*$は安全ですが、存在しない場合に文字を放棄する可能性があり、最初の一致が失敗する可能性があるa*b*[ac]*$ため、危険です(例)。a*[ac]*baaaaaaaaaaaccccccccccd
JDBは

0

小さなテスト入力で実行したり、正規表現の構造を分析したりすることで、いくつかの単純化ルールを実装できると私が考える方法がいくつかあります。

  • (a+)+ 冗長な演算子をただに置き換えるためのある種のルールを使用して減らすことができます (a+)
  • ([a-zA-Z]+)* また、新しい冗長性の組み合わせルールを使用して簡略化することもできます。 ([a-zA-Z]*)

コンピューターは、関連する文字のランダムに生成されたシーケンスまたは文字のシーケンスに対して正規表現の小さな部分式を実行し、それらがすべてどのグループに含まれるかを確認することで、テストを実行できます。最初のコンピューターは、正規表現のようなものです。が欲しいので、で試してみましょう6aaaxaaq。次に、すべてのaと、最初のグループのみが1つのグループに含まれることを確認し、aがいくつ配置されても、+すべてがグループに含まれるため、問題ではないと結論付けます。2つ目は、正規表現が文字の束を必要としているので、で試してみると-fg0uj=、各束がすべて1つのグループに含まれていることがわかり、+、最後にを削除します。

次に、次のルールを処理するための新しいルールが必要です。elimate-irrelevant-optionsルールです。

  • を使用する(a|aa)+と、コンピューターがそれを調べて、2番目の大きなものが好きですが、最初の1つを使用してより多くのギャップを埋め、できるだけ多くのaaを取得して、他に何かを取得できるかどうかを確認できます。終わった後。`eaaa @ a〜aa 'のような別のテスト文字列に対して実行できます。それを決定する。

  • (a|a?)+一致する文字列a?は私たちが探しているドロイドではないことをコンピュータに認識させることで、身を守ることができます。これは、いつでもどこでも一致する可能性があるため、のようなものは好き(a?)+ではないと判断して破棄するためです。

  • (.*a){x}一致する文字aがすでに.*。によって取得されていることを認識させることで、保護します。次に、その部分を破棄し、別のルールを使用して、の冗長な数量詞を置き換えます(.*){x}

このようなシステムの実装は非常に複雑ですが、これは複雑な問題であり、複雑な解決策が必要になる場合があります。また、正規表現が終了しない場合に強制終了する前に、限られた量の実行リソースのみを正規表現に許可するなど、他の人が提起した手法を使用する必要があります。


1
「のように」、「欲しい」ものを認識し、「試行」し、「見て」、結論を導き出す(「実現する」、「決定する」)ことは、コンピューターにアルゴリズムで実装するのが難しい重要な問題です。頼りになるものは何もありません。むしろ、何らかの証明が必要になります。
ベルギ2013年

@Bergiテスト例で意味したのは、完全な正規表現の小さなチャンクを取得し、それがどのように動作するかを判断する簡単な方法として、テスト文字列に対して実行することでした。もちろん、あなたはあなたが調べたチャンクだけをテストしていて、テストケースで奇妙なことをしないことをすでに知っています。
AJMansfield 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.