我最近意识到Regular expression Denial of Service攻击,并决定在我的代码库中找到所谓的“邪恶”正则表达式模式 - 或者至少是那些用于用户输入的模式。上面OWASP link和wikipedia给出的示例很有帮助,但他们并没有很好地用简单的术语解释问题。
来自wikipedia的邪恶正则表达式的描述:
通过示例,再次来自wikipedia:
(a+)+
([a-zA-Z]+)*
(a|aa)+
(a|a?)+
(.*a){x}
for x> 10 这是一个问题,只是没有更简单的解释?我正在寻找能够在编写正则表达式时更容易避免这个问题,或者在现有代码库中找到它们的东西。
答案 0 :(得分:53)
因为计算机完全按照你的要求去做,即使它不是你的意思,也不完全是不合理的。如果你要求一个Regex引擎证明,对于某些给定的输入,既可以匹配或不匹配给定的模式,那么无论必须测试多少种不同的组合,引擎都会尝试这样做。
这是一个简单的模式,受到OP帖子中第一个例子的启发:
^((ab)*)+$
鉴于输入:
abababababababababababab
正则表达式引擎会尝试类似(abababababababababababab)
的内容,并在第一次尝试时找到匹配项。
然后我们把猴子扳手扔进去:
abababababababababababab a
引擎将首先尝试(abababababababababababab)
,但由于额外的a
而失败。这导致灾难性的包围,因为我们的模式(ab)*
,在善意的展示中,将释放其中一个捕获(它将“回溯”)并让外部模式再次尝试。对于我们的正则表达式引擎,看起来像这样:
(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)
- 不是
可能的组合数量随着输入的长度呈指数级增长,在您知道它之前,正则表达式引擎正在耗尽所有系统资源,试图解决这个问题,直到用尽所有可能的术语组合,它最终放弃并报告“没有匹配”。与此同时,你的服务器变成了一堆燃烧的熔融金属。 (有趣的是,这基本上是密码暴力破解程序的工作方式,因为它位于same class of problems中。)
这实际上非常棘手。我自己写了几个,尽管我知道它们是什么,一般如何避免它们。见Regex taking surprisingly long time。在atomic group中包含所有内容可以帮助防止回溯问题。它基本上告诉正则表达式引擎不要重新访问给定的表达式 - “锁定你在第一次尝试时匹配的内容”。但请注意,原子表达式不会阻止在表达式中,因此^(?>((ab)*)+)$
仍然很危险,但^(?>(ab)*)+$
是安全的(它会匹配(abababababababababababab)
然后拒绝放弃任何匹配的字符,从而防止灾难性的回溯。)
不幸的是,一旦写完,实际上很难立即或快速找到问题正则表达式。最后,认识到一个糟糕的正则表达式就像识别任何其他不良代码一样 - 它需要花费大量的时间和经验和/或一个灾难性的事件。
有趣的是,由于这个答案是第一次写的,德克萨斯大学奥斯汀分校的一个团队发表了一篇论文,描述了一个能够对正则表达式进行静态分析的工具的开发,其明确目的是找到这些“邪恶”模式。该工具是为了分析Java程序而开发的,但我怀疑在未来几年中我们会看到更多的工具是围绕分析和检测JavaScript和其他语言中的问题模式而开发的,尤其是rate of ReDoS attacks continues to climb。
Static Detection of DoS Vulnerabilities in Programs that use Regular Expressions
ValentinWüstholz,Oswaldo Olivo,Marijn J. H. Heule和Isil Dillig 德克萨斯大学奥斯汀分校
答案 1 :(得分:8)
我将其概括为“重复重复”。你列出的第一个例子很好,因为它表示“字母a,连续一次或多次。这可以连续发生一次或多次”。
在这种情况下要查找的是量词的组合,例如*和+。
值得注意的是第三和第四个更微妙的事情。这些示例包含OR操作,其中双方都可以为真。这与表达式的量词结合可能会导致很多潜在的匹配,具体取决于输入字符串。
总结一下,TLDR风格:
小心如何将量词与其他运算符结合使用。
答案 2 :(得分:7)
你所谓的“邪恶”正则表达式是展示catastrophic backtracking的正则表达式。链接页面(我写的)详细解释了这个概念。基本上,当正则表达式无法匹配并且相同正则表达式的不同排列可以找到部分匹配时,会发生灾难性的回溯。然后正则表达式引擎尝试所有这些排列。如果您想查看代码并检查正则表达式,这些是需要关注的3个关键问题:
替代品必须互相排斥。如果多个替代方案可以匹配相同的文本,那么如果正则表达式的其余部分失败,则引擎将尝试两者。如果备选方案属于重复的组,则会发生灾难性的回溯。一个典型的例子是(.|\s)*
,以便在正则表达式没有“点匹配换行符”模式时匹配任何数量的任何文本。如果这是较长正则表达式的一部分,那么具有足够长的空格(由.
和\s
匹配)的主题字符串将打破正则表达式。修复方法是使用(.|\n)*
使备选方案相互排斥,甚至更好地更具体地说明哪些字符是真正允许的,例如[\r\n\t\x20-\x7E]
用于ASCII printables,制表符和换行符。
按顺序排列的量化标记必须相互排斥,或者相互排斥它们之间的标记。否则两者都可以匹配相同的文本,并且当正则表达式的其余部分无法匹配时,将尝试两个量词的所有组合。一个典型的例子是a.*?b.*?c
来匹配3个东西与它们之间的“任何东西”。当c
无法匹配时,第一个.*?
将逐字符扩展,直到行或文件的末尾。对于每个扩展,第二个.*?
将逐字符扩展以匹配行或文件的其余部分。修复是要意识到你不能在它们之间有“任何东西”。第一次运行需要在b
停止,第二次运行需要在c
停止。单个字符a[^b]*+b[^c]*+c
是一个简单的解决方案。由于我们现在停在分隔符处,我们可以使用占有量词来进一步提高性能。
包含带有量词的标记的组必须没有自己的量词,除非组内的量化标记只能与其相互排斥的其他内容匹配。这确保了内部量词的更多迭代的外部量词的更少次迭代不能与外部量词的更多迭代匹配相同的文本而内部量词的迭代更少。这是JDB答案中说明的问题。
在我写答案时,我认为这值得full article on my website。现在也在线。
答案 3 :(得分:3)
我认为你不能认识到这样的正则表达式,至少不是所有的正则表达式都没有限制性地限制它们的表现力。如果您真的关心ReDoS,我会尝试对它们进行沙盒处理,并在超时时终止它们。也有可能有RegEx实现允许您限制其最大回溯量。
答案 4 :(得分:3)
令我惊讶地发现ReDOS很多次执行源代码审查。我建议的一件事是使用你正在使用的正则表达式引擎的超时。
例如,在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
,并且不会导致资源使用导致拒绝服务条件。
您需要尝试使用超时值,以确保它适合您的使用。
答案 5 :(得分:2)
我想说这与正在使用的正则表达式引擎有关。您可能无法始终避免使用这些类型的正则表达式,但如果您的正则表达式引擎是正确构建的,那么它就不是问题了。有关正则表达式引擎主题的大量信息,请参阅this blog series。
请注意文章底部的警告,其中回溯是NP-Complete问题。目前无法有效地处理它们,您可能希望在输入中禁止它们。
答案 6 :(得分:2)
邪恶的正则表达总是由于相应的NFA中的含糊不清,您可以使用regexper等工具进行可视化。
以下是一些含糊不清的形式。不要在你的正则表达中使用它们。
(a+)+
(又名“星高&gt; 1”)。这可能导致指数性爆炸。请参阅substack的safe-regex
工具。(a|a)+
。这可能导致指数性爆炸。\d+\d+
这样的量化重叠邻接。这可能导致多项式爆炸。我在超线性正则表达式上写了paper。它包括对其他正则表达式相关研究的大量参考。
答案 7 :(得分:0)
我可以想到一些方法可以通过在小测试输入上运行它们或分析正则表达式的结构来实现一些简化规则。
(a+)+
可以使用某种规则将冗余运算符替换为(a+)
([a-zA-Z]+)*
也可以通过我们的新冗余合并规则([a-zA-Z]*)
计算机可以通过运行正则表达式的小子表达式来对相关字符或字符序列的随机生成序列运行测试,并查看它们最终所在的组。对于第一个,计算机就像,嘿,正则表达式想要一个,所以让我们尝试6aaaxaaq
。然后它会看到所有的a,只有第一个groupm最终出现在一个组中,并得出结论,无论有多少是什么,它都无关紧要,因为+
全部都在组中。第二个,就像,嘿,正则表达式想要一堆字母,所以让我们用-fg0uj=
尝试,然后它再次看到每一个串都在一个组中,所以它摆脱了{{ 1}}在最后。
现在我们需要一个新规则来处理下一个规则:消除无关选项规则。
使用+
,计算机会看一下它,就像我们喜欢那个大的第二个,但我们可以使用第一个来填补更多的空白,让我们得到很多我们可以,看看我们完成后是否还能得到任何其他东西。它可以在另一个测试字符串上运行它,比如`eaaa @ a~aa'。确定。
通过让计算机意识到(a|aa)+
匹配的字符串不是我们正在寻找的机器人,您可以保护自己免受(a|a?)+
的影响,因为它可以随时随地匹配,我们决定我们不喜欢像a?
这样的东西,然后扔掉它。
我们通过(a?)+
来确保(.*a){x}
匹配的字符已被a
抓取,以防止.*
。然后,我们抛弃该部分,并使用另一条规则替换(.*){x}
中的冗余量词。
虽然实现这样的系统会非常复杂,但这是一个复杂的问题,可能需要复杂的解决方案。你还应该使用其他人提出的技术,比如只允许正则表达式使用一些有限数量的执行资源,如果它没有完成就杀死它。