【问题标题】:How can I recognize an evil regex?我如何识别邪恶的正则表达式?
【发布时间】:2012-10-11 14:30:33
【问题描述】:

我最近意识到Regular expression Denial of Service 攻击,并决定在我的代码库中找到所谓的“邪恶”正则表达式模式——或者至少是那些在用户输入中使用的模式。上面OWASP linkwikipedia 给出的示例很有帮助,但它们不能很好地简单地解释问题。

邪恶正则表达式的描述,来自wikipedia

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

举个例子,同样来自wikipedia

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

这是一个没有更简单解释的问题吗?我正在寻找可以在编写正则表达式或在现有代码库中找到它们时更容易避免此问题的方法。

【问题讨论】:

标签: regex


【解决方案1】:

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

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

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

^((ab)*)+$

给定输入:

abababababababababababab

正则表达式引擎尝试类似(abababababababababababab) 的内容,并在第一次尝试时找到匹配项。

然后我们把活动扳手扔进去:

abababababababababababab 一个

引擎将首先尝试(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) - 没有
@ 987654343@ - 没有
(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) - 没有

可能的组合数量随着输入的长度呈指数增长,在您知道之前,正则表达式引擎正在耗尽您所有的系统资源,试图解决这个问题,直到用尽所有可能的术语组合,它终于放弃并报告“没有匹配项”。与此同时,你的服务器变成了一堆燃烧的熔融金属。

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

这实际上非常棘手。现代正则表达式引擎中的灾难性回溯本质上类似于halting problem,艾伦·图灵证明这是不可能解决的。 I have written problematic regexes myself,尽管我知道它们是什么以及通常如何避免它们。将您可以使用的所有内容包装在 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
Valentin Wüstholz、Oswaldo Olivo、Marijn J. H. Heule 和 Isil Dillig
德克萨斯大学奥斯汀分校

【讨论】:

  • 这是描述/为什么/示例正则表达式需要很长时间的一个很好的答案,但我正在寻找一些人们可以内化以帮助识别问题正则表达式的规则。跨度>
  • 了解“为什么”是避免编写“邪恶”正则表达式的最重要一步。不幸的是,一旦编写完成,实际上很难立即或快速找到有问题的正则表达式。如果您想要一揽子修复,原子分组通常是最好的方法,但这会对正则表达式匹配的模式产生重大影响。最后,识别错误的正则表达式就像正则表达式任何其他错误代码一样 - 它需要大量经验、大量时间和/或单个灾难性事件。
  • 这就是为什么我更喜欢不支持回溯的正则表达式引擎,而无需用户强制。 IE。 lex/flex.
  • @MikePartridge 这是常见的 IT 经典理论问题,决定某些代码是无限循环还是停止是一个 NP 完全问题。使用正则表达式,您可能可以通过搜索某些模式/规则来猜测/捕捉其中一些,但除非您进行一些繁重的 NP 完全分析,否则您永远无法捕捉到它们。一些选项:1)永远不要让用户输入正则表达式到您的服务器。 2)配置正则表达式引擎以尽早终止计算(但在代码中测试您的有效正则表达式仍然有效,即使有严格的限制)。 3) 在具有 cpu/mem 限制的低优先级线程中运行正则表达式代码。
  • @MikePartridge - 最近看到一篇关于正在开发的用于静态检测这些有问题的正则表达式的新工具的论文。有趣的东西……我认为值得关注。
【解决方案2】:

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

  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 的回答中说明的问题。

在我写答案时,我认为这值得full article on my website。现在也上线了。

【讨论】:

    【解决方案3】:

    检测邪恶的正则表达式

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

    经验法则

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

    这里有一些歧义。不要在你的正则表达式中使用这些。

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

    其他资源

    我在超线性正则表达式上写了这个paper。它包括对其他正则表达式相关研究的大量参考。

    【讨论】:

      【解决方案4】:

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

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

      需要注意的是第三个和第四个。这些示例包含一个 OR 操作,其中双方都可以为真。这与表达式的量词相结合可以根据输入字符串产生很多潜在的匹配。

      总而言之,TLDR 风格:

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

      【讨论】:

      • 目前,这个答案最接近我正在寻找的:识别可能导致灾难性回溯的正则表达式的经验法则。
      • 您遗漏的,似乎是问题的重要部分,是捕获组。
      • @MikePartridge 那也是。我试图尽可能地简化它,因此还有其他可能导致相同事情的事情,例如捕获组。
      【解决方案5】:

      在执行源代码审查时,我惊讶地遇到了很多次 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,并且不会导致资源使用导致拒绝服务条件。

      您需要对超时值进行试验,以确保它适合您的使用。

      【讨论】:

        【解决方案6】:

        我会说这与使用的正则表达式引擎有关。您可能并不总是能够避免这些类型的正则表达式,但如果您的正则表达式引擎构建正确,那么问题就不大了。有关正则表达式引擎主题的大量信息,请参阅this blog series

        请注意文章底部的警告,因为回溯是一个 NP 完全问题。目前没有办法有效地处理它们,您可能希望在输入中禁止它们。

        【讨论】:

        • a*a* 不使用反向引用。现在,正则表达式引擎使用回溯,这也许就是您的意思?在这种情况下,所有现代引擎都使用回溯。您可以通过(?&gt;...) 轻松禁用回溯,但这通常不会改变您表达的含义(在某些情况下可以规避)。
        • @Cyborgx37 哎呀!我的意思是回溯。固定。
        • 在这种情况下,引擎要么使用回溯,要么不使用。几乎没有办法通过限制输入来限制回溯。
        • @JDB:“所有现代引擎都使用回溯。” - 也许这在 2013 年是真的,但 not anymore.
        • @Kevin - 当然。你赢了。
        【解决方案7】:

        我不认为你可以识别这样的正则表达式,至少不是全部,或者在没有限制性地限制它们的表达性的情况下。如果您真的关心 ReDoS,我会尝试将它们沙箱化并在超时时终止它们的处理。也可能存在允许您限制其最大回溯量的 RegEx 实现。

        【讨论】:

        • 我认为你误解了这个问题。当我读到它时,OP 从字面上询问 he 如何识别邪恶的正则表达式,而不是他如何编写程序来做到这一点。比如,“我已经写了这个正则表达式,但我怎么知道它是否是邪恶的?”
        • 呃,你可能是对的。然后,我只能推荐 @DanielHilgarth 已在 cmets 中链接的有关灾难性回溯的文章。
        • @0x90:因为我不考虑例如a*\* 是“易受攻击的”。
        • @0x90 a* 完全没有漏洞。同时,a{0,1000}a{0,1000} 是一个等待发生的灾难性正则表达式。即使a?a? 在适当的条件下也会产生令人讨厌的结果。
        • @0x90 - 灾难性回溯是危险的,只要您有两个表达式,其中一个相同或另一个的子集,表达式的长度是可变的,并且它们的位置使得一个可以通过回溯放弃一个或多个字符。例如,a*b*c*$ 是安全的,但 a*b*[ac]*$ 是危险的,因为如果 b 不存在并且初始匹配失败(例如 aaaaaaaaaaaccccccccccd),a* 可能会将字符放弃给 [ac]*
        【解决方案8】:

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

        • (a+)+ 可以使用某种规则来减少,将冗余运算符替换为 (a+)
        • ([a-zA-Z]+)* 也可以通过我们新的冗余组合规则简化为([a-zA-Z]*)

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

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

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

        • 1234563不喜欢(a?)+之类的东西,扔掉。
        • 我们通过让a 匹配的字符已经被.* 抓取来保护(.*a){x}。然后我们把那部分扔掉,用另一个规则替换(.*){x}中的冗余量词。

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

        【讨论】:

        • “像”,识别“想要”,“尝试”猜测,“看到”和得出结论(“实现”,“确定”)是难以实现的非平凡问题计算机算法……而且测试示例没有什么可依赖的,你宁愿需要某种证明。
        • @Bergi 我所说的测试示例的意思是,您取一小块完整的正则表达式,然后针对测试字符串运行它,作为确定其行为方式的简单方法。当然,您只是在测试您已经检查过并且已经知道不会在测试用例中做奇怪事情的块。
        猜你喜欢
        • 1970-01-01
        • 2012-02-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-01-06
        • 1970-01-01
        • 1970-01-01
        • 2023-01-16
        相关资源
        最近更新 更多