• home > theory > algorithm > Introduction >

    深入正则表达式(5):正则表达式回溯填坑笔记—回溯与非回溯

    Author:zhoulujun Date:

    正常表达式所涉及的一些术语解释,与基本原理概述。本篇文章是打基础。

    大部分编程语言如如java、JavaScript、.net、Perl、Python、Ruby、PHP等正则表达式使用的引擎实现是 NFA 自动机,这种正则表达式引擎在进行字符匹配时会发生回溯(backtracking)。而一旦发生回溯,那其消耗的时间就会变得很长,有可能是几分钟,也有可能是几个小时,时间长短取决于回溯的次数和复杂度

    回溯攻击

    只用一台机器,发送一个请求,就可以打跨对方的服务器,这简直就是黑客梦寐以求的核武器。与之相比,什么 DDoS 弱爆了,动静大还花钱多。

    这种攻击也有自己的名字:ReDoS (RegEx Denial of Service)

    由于正则表达式应用非常广泛,几乎存在于后端服务的各个部分,所以只要找到其中一个漏洞,就有机可趁。

    试想一个场景,黑客发现了 WAF 中存在 ReDoS 漏洞,发送一个请求打垮了 WAF;你无法在短时间内定位这个问题,甚至意识不到这是一次攻击;为了保证业务的正常,你选择重启或者暂时关闭 WAF;在 WAF 失效期间,黑客利用 SQL 注入,拖走了你的数据库。而你,可能还完全蒙在鼓里。

    回溯情景

    使用“(?>…)”方式进行非回溯声明。由于正则表达式引擎的贪婪特性,导致它在某些情况下,将进行回溯以获得匹配

    text="abbc"
    reg=/ab{1,3}c/

    正则表达式匹配情况

    其实在正则表达式中有这么三种模式:贪婪模式、懒惰模式、独占模式

    • ?:告诉引擎匹配前导字符0次或一次。事实上是表示前导字符是可选的。

    • +: 告诉引擎匹配前导字符1次或多次。

    • *: 告诉引擎匹配前导字符0次或多次。

    • {min, max}:告诉引擎匹配前导字符min次到max次。如果有逗号而max被省略了,则表示max没有限制;如果逗号和max都被省略了,则表示重复min次。

    默认情况下,这个几个特殊字符都是贪婪的,也就是说,它会根据前导字符去匹配尽可能多的内容。

    所以关于数量的匹配中,有 + ? * {min,max} 四种情况,如果只是单独使用,那么它们就是贪婪模式

    如果在他们之后加多一个 ? 符号,那么原先的贪婪模式就会变成懒惰模式,即尽可能少地匹配。但是懒惰模式还是会发生回溯现象的。例如下面这个例子:

    text="abbc"
    regex="ab{1,3}?c"

    正则表达式的第一个操作符 a 与 字符串第一个字符 a 匹配,匹配成功。于是正则表达式的第二个操作符 b{1,3}? 和 字符串第二个字符 b 匹配,匹配成功。因为最小匹配原则,所以拿正则表达式第三个操作符 c 与字符串第三个字符 b 匹配,发现不匹配。于是回溯回去,拿正则表达式第二个操作符 b{1,3}? 和字符串第三个字符 b 匹配,匹配成功。于是再拿正则表达式第三个操作符 c 与字符串第四个字符 c 匹配,匹配成功。于是结束。

    如果在他们之后加多一个 + 符号,那么原先的贪婪模式就会变成独占模式,即尽可能多地匹配,但是不回溯

    于是乎,如果要彻底解决问题,就要在保证功能的同时确保不发生回溯。

    regex=ab{1,3}+bc

    正则匹配示意图

    悲观回溯

    这次我们的正则改为 /^(a*)b$/。但是把要匹配的字符串改为 aaaaa。去掉了结尾的字符 b。

    让我们看看此时的执行流程:

    1. (a*) 首先匹配了所有 aaaaa。

    2. 尝试匹配 b。但是匹配失败。

    3. 回溯 1 个字符。此时剩余字符串为 a。依然无法匹配字符 b。

    4. 回溯一直进行。直到匹配 0 次的情况。此时剩余字符串为 aaaaa。依然无法匹配 b。

    5. 所有的可能性均已尝试过,依然无法匹配。最终导致整体匹配失败。

    可以看到,虽然我们可以一眼看出二者无法匹配。但正则表达式在执行时还要“傻傻的”逐一回溯所有可能性,才能确定最终结果。这个“傻傻的”回溯过程就叫悲观回溯。

    禁止回溯

    既然回溯可能有性能问题,那我们是否可以禁止正则表达式进行回溯呢。

    答案是:可以。

    有两种语法可以防止回溯:

    • 有限量词(Possessive Quantifiers)

    • 原子分组(Atomic Grouping)

    关于这两种语法,感兴趣的同学可以自行 Google。在此不详细解释。因为这两种语法在 JavaScript 中均不被支持

    所以对于我这从java跳到前端的,现在也泪崩逃走算了

    回溯/懒惰/独占

    把以上三种模式的表达式列出如下

    贪婪-默认情况

    懒惰-加个?

    独占-加个+

    X?

    X??

    X?+

    X*

    X*?

    X*+

    X+

    X+?

    X++

    X{n}

    X{n}?

    X{n}+

    X{n,}

    X{n,}?

    X{n,}+

    X{n,m}

    X{n,m}?

    X{n,m}+

    回溯问题

    比如

    ^[允许字符集]+

    该字符集大小约为250,而+号表示至少出现一次。按照上面说到的NFA引擎贪婪模式,在用户输入一个过长字符串进行匹配时,一旦发生回溯,计算量将是巨大的。后来采用了独占模式,CPU 100%的问题也得到了解决。



    参考文章:

    正则表达式中的悲观回溯  https://loveky.github.io/2017/05/31/regular-expressions-catastrophic-backtracking/

    正则表达式回溯漏洞 https://www.cnblogs.com/Eleven-Liu/p/10826488.html

    藏在正则表达式里的陷阱 https://www.cnblogs.com/chanshuyi/p/the_regex_backtracking_trap.html

    一个由正则表达式引发的血案 https://www.cnblogs.com/study-everyday/p/7426862.html

    如何彻底避免正则表达式的灾难性回溯? https://zhuanlan.zhihu.com/p/44425997

    正则表达式里的底层原理是什么 https://www.cnblogs.com/Renyi-Fan/p/9694695.html#_label0_3



    转载本站文章《深入正则表达式(5):正则表达式回溯填坑笔记—回溯与非回溯》,
    请注明出处:https://www.zhoulujun.cn/html/theory/algorithm/IntroductionAlgorithms/8429.html