占有优先模式在防ReDoS攻击中的作用,实际开发中很有用
ReDoS攻击的本质:灾难性回溯
核心问题: 某些正则表达式在匹配特定构造的恶意字符串时,引擎的回溯次数会呈指数级或阶乘级增长,导致CPU长时间满载(甚至永久阻塞)。
罪魁祸首: 嵌套量词 + 允许回溯的组合。
贪婪模式 (, +, {n,m}) 和 非贪婪模式 (?, +?) 都允许回溯,这是问题的根源。
当正则引擎在尝试匹配一个复杂模式且存在歧义路径时,它会尝试所有可能的回溯路径,导致计算量爆炸。
为什么会导致指数级增长的回溯:
正则表达式:^(a+)+$
目标:匹配由1个或多个a组成的字符串(如"aaa")
结构:外层( )+表示"一组或多组",内层a+表示"一个或多个a"
攻击字符串:"aaaaaaaaX"(8个a + 无效字符X)
匹配过程详解(关键回溯步骤):
第一轮尝试(贪婪匹配):
内层a+吃光所有8个a → 匹配"aaaaaaaa"
外层( )+记录第一组:[aaaaaaaa]
尝试匹配结束符$ → 失败(遇到X)
❗ 开始回溯
回溯:外层尝试第二组:
让第一组吐出1个a → 第一组变为[aaaaaaa](7个a)
剩余字符串:"aX"
外层尝试匹配第二组:
内层a+匹配剩余"a" → 第二组[a]
尝试匹配$ → 失败(遇到X)
❗ 继续回溯
更深层回溯:
让第二组吐出它的a → 第二组变为空
尝试匹配$ → 失败(剩余"aX")
回溯到第一组:再吐出1个a → 第一组[aaaaaa](6个a)
剩余字符串:"aaX"
外层尝试第二组:
选项1:a+匹配"aa" → 第二组[aa] → 匹配$失败(遇到X)
选项2:a+匹配"a" → 剩余"aX" → 匹配$失败
指数级回溯爆发:
引擎必须尝试所有可能的a分组组合:
text
分组方案1: [aaaaaaaa] (1组)
分组方案2: [aaaaaaa] [a] (2组)
分组方案3: [aaaaaa] [aa] (2组)
分组方案4: [aaaaaa] [a] [a] (3组)
分组方案5: [aaaaa] [aaa] (2组)
分组方案6: [aaaaa] [aa] [a] (3组)
分组方案7: [aaaaa] [a] [aa] (3组)
分组方案8: [aaaaa] [a] [a] [a] (4组)
...(继续所有组合)...
每组都要尝试匹配结束符$ → 全部失败
为什么是指数级增长?(O(2^n))
8个a的分组可能 = 2⁷ = 128种组合(相当于7个间隔点每个都有"分/不分"两种选择)
text
示例:aaa aaaa a
↑ ↑ ↑ ↑ (7个可能的分割点)
n个a的分组可能 = 2<sup>n-1</sup>种组合
当n=30时:2<sup>29</sup> = 536,870,912次尝试(5亿多次!)
可视化回溯过程(以4个a + X为例):
text
字符串: "aaaaX"
尝试的分组方案:
解决方案:用占有优先模式 ^(a++)+$
内层a++匹配所有a后立即锁定,拒绝回溯
匹配过程:
a++吃光8个a → 锁定
尝试匹配$ → 失败(遇到X)
立即失败,不再尝试其他分组
时间复杂度:O(n)(只扫描一次字符串)
这个例子展示了为什么看似无害的正则表达式可能成为性能炸弹,而占有优先模式是关键的防御手段。