我如何识别邪恶的正则表达式?


83

最近,我意识到正则表达式拒绝服务攻击,并决定在我的代码库中可以找到的所谓“邪恶”正则表达式模式中根除-至少是在用户输入中使用的那些。上面的OWASP链接Wikipedia上给出的示例很有帮助,但它们不能很好地用简单的术语解释问题。

邪恶的正则表达式的描述,来自维基百科

  • 正则表达式将重复(“ +”,“ *”)应用于复杂的子表达式;
  • 对于重复的子表达式,存在一个匹配项,它也是另一个有效匹配项的后缀。

再次举例,来自维基百科

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

这是一个没有更简单解释的问题吗?我正在寻找可以简化编写正则表达式或在现有代码库中查找它们的问题。


7
关于这个话题的另一个环节是这个:regular-expressions.info/catastrophic.html
丹尼尔Hilgarth

1
这是一个用于对正则表达式执行静态分析以发现可疑的ReDoS问题的工具:cs.bham.ac.uk/~hxt/research/rxxr.shtml
Tripleee,

@tripleee提供的链接似乎指向RXXR工具的链接断开。这是一个GitHub镜像:github.com/ConradIrwin/rxxr2
Mike Hill

3
此外,对于那些好奇的人来说,原始RXXR工具的作者似乎将其替换为RXXR2。他们的新页面在此处托管,并且当前确实具有RXXR2来源的有效链接:cs.bham.ac.uk/~hxt/research/rxxr2
Mike Hill,

Answers:


76

为什么邪恶的正则表达式有问题?

因为计算机完全按照您的要求去做,即使这不是您的意思或完全不合理。如果您要求Regex引擎证明,对于某些给定的输入,给定模式是否匹配,那么无论必须测试多少种不同的组合,引擎都将尝试这样做。

这是一个简单的模式,灵感来自OP的第一个示例:

^((ab)*)+$

给定输入:

爸爸妈妈

正则表达式引擎尝试类似的操作,(abababababababababababab)并且在第一次尝试中找到匹配项。

但是随后我们将活动扳手投入:

ababababababababababababbab a

引擎将首先尝试,(abababababababababababab)但是由于额外的原因而失败a。这会导致灾难性的跟踪,因为我们的模式出于(ab)*善意的表现,将释放其中一个捕获(它将“回溯”)并让外部模式重试。对于我们的正则表达式引擎,看起来像这样:

(abababababababababababab)-Nope
(ababababababababababab)(ab)-Nope
(abababababababababab)(abab)-Nope
(abababababababababab)(ab)(ab)-Nope
(ababababababababab)(ababab)-Nope
(ababababababababab)(abab)(ab)-Nope
(ababababababababab)(ab)(abab)-Nope
(ababababababababab)(ab)(ab)(ab)-Nope
(abababababababab)(abababab)-Nope
(abababababababab)(ababab)(ab)-Nope
(abababababababab)(abab)(abab)-Nope
(abababababababab)(abab)(ab)(ab)-Nope
(abababababababab)(ab)(ababab)-Nope
(abababababababab)(ab)(abab)(ab)-Nope
(abababababababab)(ab)(ab)(abab)-Nope
(abababababababab)(ab)(ab)(ab)(ab)-Nope
(ababababababab)(ababababab)-Nope
(ababababababab)(abababab)(ab)-Nope
(ababababababab)(ababab)(abab)-Nope
(ababababababab)(ababab)(ab)(ab)-Nope
(ababababababab)(abab)(abab)(ab)-Nope
(ababababababab)(abab)(ab)(abab)-Nope
(ababababababab)(abab)(ab)(ab)(ab)-Nope
(ababababababab)(ab)(abababab)-Nope
(ababababababab)(ab)(ababab)(ab)-Nope
(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)-不可以

可能组合的数量与输入的长度成指数关系,在不知不觉中,正则表达式引擎吞噬了所有系统资源,试图解决此问题,直到用尽所有可能的术语组合,最终放弃并报告“没有匹配项。” 同时,您的服务器已变成一堆燃烧的熔融金属。

如何发现邪恶的正则表达式

这实际上非常棘手。我已经写了一对夫妇,尽管我知道它们是什么,以及通常如何避免它们。看到Regex花了很长时间。将所有内容包装在原子组中有助于防止回溯问题。它基本上告诉正则表达式引擎不要重新访问给定的表达式-“锁定第一次尝试匹配的内容”。但是请注意,原子表达式不会阻止表达式的回溯,因此^(?>((ab)*)+)$仍然很危险,但是^(?>(ab)*)+$很安全(先匹配(abababababababababababab)然后拒绝放弃任何匹配的字符,从而防止灾难性的回溯)。

不幸的是,一旦编写完成,实际上很难立即或快速找到有问题的正则表达式。最后,识别错误的正则表达式就像识别其他任何错误的代码一样,这需要大量的时间和经验,并且/或者只发生一次灾难性事件。


有趣的是,自从这个答案首次被写出来以来,德克萨斯大学奥斯汀分校的一个小组就发表了一篇论文,描述了一种能够执行正则表达式静态分析并明确发现这些“邪恶”模式的工具的开发。该工具是为分析Java程序而开发的,但是我怀疑在未来几年中,我们将看到更多围绕JavaScript和其他语言来分析和检测有问题的模式而开发的工具,尤其是随着ReDoS攻击率的持续攀升

静态检测使用正则表达式的程序中的DoS漏洞
ValentinWüstholz,Oswaldo Olivo,Marijn JH Heule和Isil Dillig
德州大学奥斯汀分校


这在描述/ why /示例正则表达式需要很长时间的过程中是一个很好的答案,但是我正在寻找一个人可以内化以帮助识别问题正则表达式的规则。
Mike Partridge 2012年

4
知道“为什么”是避免编写“邪恶的”正则表达式的最重要步骤。不幸的是,一旦编写完成,实际上很难立即或快速找到有问题的正则表达式。如果您想要一揽子修复程序,原子分组通常是最好的选择,但是这可能会对正则表达式将匹配的模式产生重大影响。最后,识别不良的正则表达式就像其他任何不良代码的正则表达式一样-需要大量的经验,大量的时间和/或一个灾难性的事件。
JDB仍记得Monica

这就是为什么我偏爱正则表达式引擎的原因,该引擎在没有用户强制的情况下不支持回溯。IE lex / flex。
Spencer Rathbun

@MikePartridge是IT常见的经典理论问题,决定某些代码将无限循环还是停止是NP完全问题。使用正则表达式,您可能可以通过搜索某些模式/规则来猜测/捕获其中的一些,但是除非您进行大量的NP完整分析,否则您将永远无法全部捕获它们。一些选项:1)永远不要让用户在您的服务器上输入regexp。2)配置regexp引擎以足够早地终止计算(但是即使在严格的限制下,在代码中测试有效的regex仍然可以工作)。3)在具有CPU /内存限制的低优先级线程中运行正则表达式代码。
Ped7g '17

1
@MikePartridge-最近遇到了一篇有关正在开发的用于静态检测这些有问题的正则表达式的新工具的论文。有趣的东西...我认为值得关注。
JDB仍记得Monica

12

您所说的“邪恶”正则表达式是具有灾难性回溯的正则表达式。链接页面(我写过)详细解释了该概念。基本上,当正则表达式不匹配并且同一正则表达式的不同排列可以找到部分匹配时,就会发生灾难性的回溯。然后,正则表达式引擎将尝试所有这些排列。如果您想查看代码并检查正则表达式,则需要注意以下三个关键问题:

  1. 替代方案必须互斥。如果多个替代项可以匹配相同的文本,则当正则表达式的其余部分失败时,引擎将尝试两者。如果备选方案在重复的组中,则将发生灾难性的回溯。一个经典的例子是(.|\s)*当正则表达式风格没有“点匹配换行符”模式时,匹配任意数量的文本。如果这是较长正则表达式的一部分,则带有足够长空格(由.和匹配\s)的主题字符串将破坏正则表达式。修复方法是(.|\n)*使替代方案互斥,甚至更好地更确切地确定真正允许使用哪些字符,例如[\r\n\t\x20-\x7E]ASCII可打印字符,制表符和换行符。

  2. 顺序的量化令牌必须彼此互斥或彼此之间是互斥的。否则,两者都可以匹配相同的文本,并且当正则表达式的其余部分不匹配时,将尝试使用两个量词的所有组合。一个经典的例子是a.*?b.*?c将3个事物与它们之间的“任何事物”进行匹配。当c无法匹配时,第一个.*?将逐字符扩展,直到行或文件的末尾。对于每个扩展,第二个.*?扩展将逐字符扩展以匹配行或文件的其余部分。解决办法是认识到它们之间不能有任何东西。第一轮需要在处停止,b第二轮需要在处停止c。带有单个字符a[^b]*+b[^c]*+c是一个简单的解决方案。由于我们现在在定界符处停止,因此我们可以使用所有格量词来进一步提高性能。

  3. 包含带有量词的标记的组一定不能拥有自己的量词,除非该组内的已量化标记只能与与其互斥的其他事物进行匹配。这确保了没有办法让外部量词的较少迭代与内部量词的较多迭代可以匹配相同的文本,因为外部量词的更​​多迭代具有内部量词的较少迭代。这是JDB答案中说明的问题。

在编写答案时,我认为这值得在我的网站上发表全文。现在也在线。


10

我将其概括为“重复的重复”。您列出的第一个示例是一个很好的示例,因为它指出“字母a,连续一次或多次。这可能再次连续发生一次或多次”。

在这种情况下,要查找的是量词的组合,例如*和+。

需要注意的是第三点和第四点。这些示例包含一个OR运算,其中双方都是正确的。这与表达式的量词结合可以导致大量潜在的匹配,具体取决于输入字符串。

总结一下,TLDR风格:

请注意如何将量词与其他运算符结合使用。


3
当前,这个答案最接近我要寻找的答案:识别可能导致灾难性回溯的正则表达式的经验法则。
Mike Partridge

1
您遗漏的东西,似乎是问题的重要组成部分,是捕获组。
Mike Partridge 2012年

@MikePartridge那也是。我尝试将其尽可能地简化,因此还有其他一些可能导致相同的事情,例如捕获组。
Jarmund

7

令人惊讶的是,我多次遇到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在给定的超时后抛出a ,并且不会导致资源使用导致拒绝服务的情况。

您将要尝试使用超时值,以确保它适合您的用法。


7

检测邪恶的正则表达式

  1. 尝试Nicolaas Weideman的RegexStaticAnalysis项目。
  2. 试试我的集成风格的vuln-regex-detector,它具有用于Weideman工具和其他工具的CLI。

经验法则

邪恶的正则表达式总是归因于相应NFA中的歧义,您可以使用regexper等工具将其可视化。

这是一些歧义形式。不要在正则表达式中使用它们。

  1. 嵌套量词,例如(a+)+(又称“星高> 1”)。这可能导致指数爆炸。请参阅子堆栈的safe-regex工具。
  2. 量化的重叠析取像(a|a)+。这可能导致指数爆炸。
  3. 避免使用像这样的量化重叠邻接\d+\d+。这可能会导致多项式爆炸。

额外资源

我写了关于超线性正则表达式的这篇论文。它包括对其他正则表达式相关研究的参考。


4

我想说这与使用中的正则表达式引擎有关。您可能并非总是能够避免使用这些类型的正则表达式,但是如果您的正则表达式引擎构建正确,那么问题就不大了。请参阅此博客系列,以获取有关正则表达式引擎主题的大量信息。

请注意本文底部的警告,因为回溯是一个NP-Complete问题。当前无法有效处理它们,您可能要在输入中禁止它们。


a*a*不使用反向引用。现在,正则表达式引擎使用backtracking,这也许就是您的意思?在这种情况下,所有现代引擎都使用回溯。您可以通过轻松禁用回溯(?>...),但这通常不会改变表达式的含义(在某些情况下可以避免)。
JDB仍然记得莫妮卡

@ Cyborgx37糟糕!我的意思是回溯。固定。
Spencer Rathbun

在那种情况下,引擎要么使用回溯,要么不使用。实际上,没有办法通过限制输入来限制回溯。
JDB仍然记得莫妮卡

2
@JDB:“所有现代引擎都使用回溯。” -也许这在2013年是正确的,但现在已经不复存在了
凯文

@Kevin-当然。你赢了。
JDB仍记得

3

我认为您无法识别此类正则表达式,至少不能全部识别或不限制它们的表达。如果您真的很在意ReDoS,我会尝试将其沙盒化并暂停它们的处理。还可能有RegEx实施允许您限制其最大回溯量。


2
我认为您误会了这个问题。在我读到它时,OP实际上是在问如何识别邪恶的正则表达式,而不是在问如何编写程序来识别它。就像,“我已经写了这个正则表达式,但是我怎么知道它可能是邪恶的呢?”
ruakh 2012年

嗯,你可能是对的。然后,我只能推荐@DanielHilgarth已在评论中链接的有关灾难性回溯的文章。
Bergi 2012年

2
@ 0x90:因为我不认为ega*\*“易受攻击”。
ruakh

1
@ 0x90a*一点也不脆弱。同时,a{0,1000}a{0,1000}正在发生灾难性的正则表达式。甚至a?a?在适当的条件下也会产生讨厌的结果。
JDB仍然记得Monica

2
@ 0x90-灾难性的回溯是危险的,只要您有两个表达式彼此相同或为另一个子集,表达式的长度是可变的并且它们的位置使得一个表达式可以放弃一个或多个字符即可其他通过回溯。例如,a*b*c*$安全a*b*[ac]*$是危险的,因为如果不存在且初始匹配失败,a*可能会放弃字符。[ac]*baaaaaaaaaaaccccccccccd
JDB仍然记得Monica

0

我可以想到一些方法,可以通过在小型测试输入上运行它们或分析正则表达式的结构来实现一些简化规则。

  • (a+)+ 可以使用某种规则将冗余运算符替换为 (a+)
  • ([a-zA-Z]+)* 我们的新冗余合并规则还可以简化 ([a-zA-Z]*)

计算机可以通过针对随机生成的相关字符序列或字符序列运行正则表达式的小子表达式来运行测试,并查看它们最终都属于哪些组。对于第一个,计算机就像,嘿,正则表达式想要一个,所以让我们尝试一下6aaaxaaq。然后,它看到所有a以及只有第一个groupm结束在一个组中,并得出结论,无论放了多少a都没关系,因为+将全部都放入了该组。第二个,就像,嘿,正则表达式想要一堆字母,让我们用尝试一下-fg0uj=,然后再看一遍,每个串都在一个组中,因此最后消除了+

现在,我们需要一个新规则来处理下一个规则:消除不相关选项规则。

  • 使用(a|aa)+,计算机可以看到它,就像,我们喜欢那第二个大,但是我们可以使用第一个来填补更多的空白,让我们尽可能多地获得aa,看看是否还能得到其他完成后。它可以针对另一个测试字符串(例如eaaa @ a〜aa)运行它。确定。

  • 您可以(a|a?)+通过使计算机意识到匹配的字符串a?不是我们想要的机器人来保护自己,因为由于它总是可以在任何地方匹配,因此我们决定不喜欢(a?)+,并扔掉它。

  • 我们可以避免(.*a){x}意识到与匹配的字符a已经被抓住了.*。然后,我们丢弃该部分,并使用另一条规则替换中的冗余量词(.*){x}

虽然实现这样的系统将非常复杂,但这是一个复杂的问题,并且可能需要复杂的解决方案。您还应该使用其他人提出的技术,例如只允许regex有限数量的执行资源,如果无法完成则杀死它。


1
“喜欢”,识别“想要”,“尝试”猜测,“看到”和得出结论(“实现”,“确定”)是非平凡的问题,很难通过算法在计算机上实现……测试示例是没什么可依赖的,您宁可需要某种证明。
Bergi

@Bergi测试示例中我的意思是,您需要取一小部分完整的正则表达式,然后将其针对测试字符串运行,以作为确定其行为方式的简单方法。当然,您只测试已经检查过的块,并且已经知道在测试用例中不会做奇怪的事情。
AJMansfield
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.