1. 引入
很有意思的一种攻击,值得记录一下给自己看。
2. ReDoS(正则表达式拒绝服务攻击)原理
ReDoS 本质是利用正则表达式的灾难性回溯(Catastrophic Backtracking) 实现的拒绝服务攻击。当正则表达式包含大量 "可选路径"(如嵌套的重复量词 */+/?),且输入字符串接近匹配但最终不匹配时,正则引擎会尝试所有可能的路径来验证匹配,导致 CPU 资源被耗尽,服务响应超时甚至崩溃,具体来说:
2.1. 正则回溯匹配
正则引擎(如 JavaScript 的 RegExp、Python 的 re)默认是"贪婪匹配"+"回溯匹配",可以用一个简单例子理解:
正则:/a.*b/(匹配以 a 开头、b 结尾的字符串)
输入:a12345b6789b
引擎的匹配过程:
- 先匹配
a→ 成功; .*是贪婪匹配,会直接匹配到字符串末尾(12345b6789b);- 接下来要匹配
b,但此时已经到字符串末尾,没有字符了 → 匹配失败; - 回溯 :
.*放弃最后一个字符(b),现在.*匹配12345b6789,再尝试匹配b→ 成功; - 最终匹配结果:
a12345b6789b。
这个过程就是回溯:当正则的某部分匹配失败时,引擎会"回退一步",调整之前的匹配范围,重新尝试匹配。
正常情况下,回溯次数很少;但如果正则设计不当,回溯次数会呈指数级增长,这就是"灾难性回溯"。
2.2 ReDoS 的核心条件(缺一不可)
触发 ReDoS 必须满足 3 个条件:
- 正则包含"嵌套/重叠的重复量词" :如
(x+)+、(x*)+、x?y*等(重复量词指*/+/?/{n,}); - 输入字符串"接近匹配但最终不匹配":如果输入完全匹配或完全不匹配,回溯次数都很少;只有"差一点匹配"时,才会触发大量回溯;
- 重复量词的匹配范围"模糊" :如
.(匹配任意字符)、\w(匹配字母/数字/下划线)等,而非明确的字符范围(如[a-z])。
2.3 灾难性回溯的过程(经典案例拆解)
以最典型的漏洞正则 /(a+)+b/ 为例,分析输入 aaaaa(无 b,接近匹配但不匹配)的回溯过程:
| 输入字符 | 正则部分 | 匹配尝试 | 回溯次数 |
|---|---|---|---|
| a | (a+)+ | 匹配 1 个 a → 尝试匹配 b → 失败 | 1 |
| aa | (a+)+ | 先匹配 a+a → 尝试 b → 失败;再回溯为 aa → 尝试 b → 失败 |
2 |
| aaa | (a+)+ | 尝试 a+a+a → 失败;a+aa → 失败;aa+a → 失败;aaa → 失败 |
4 |
| aaaa | (a+)+ | 回溯次数 8 次 | 8 |
| aaaaa | (a+)+ | 回溯次数 16 次 | 16 |
规律:输入有 n 个 a 时,回溯次数是 2(n−1)2^{(n-1)}2(n−1)(指数级增长)。
- 当 n=10 → 512 次回溯(可接受);
- 当 n=20 → 524288 次回溯(轻微卡顿);
- 当 n=30 → 536870912 次回溯(CPU 100%,程序卡死)。
这就是 ReDoS 的核心:输入长度线性增加,回溯次数指数级增加,最终耗尽系统资源。
2.4 为什么普通正则不会触发 ReDoS?
如果正则设计合理,即使有重复量词,也不会触发灾难性回溯。比如:
- 安全正则:
/a+b/(无嵌套重复); - 输入:
aaaaa(无 b); - 匹配过程:
a+匹配所有 a → 尝试匹配 b → 失败 → 直接返回 false,无回溯。
对比漏洞正则 /(a+)+b/,核心差异是:(a+)+ 让引擎认为"a 可以拆分成多个组",而 a+ 只有一种匹配方式。
2.5 总结
- ReDoS 本质:正则引擎的回溯机制在"嵌套重复量词 + 接近匹配的输入"下,产生指数级回溯次数,耗尽 CPU 资源。
- 核心触发条件 :嵌套/重叠的重复量词(如
(x+)+)+ 模糊匹配(如.)+ 接近匹配的输入。 - 关键规律 :输入长度线性增长,回溯次数呈 2n2^n2n 指数增长,这是 ReDoS 能"少量输入搞垮服务"的根本原因。
3. 复现
用如下python代码,演示不同输入长度下的匹配耗时:
python
import re
import time
vulnerable_re = re.compile(r'^(\w+)+$')
for i in range(1,1000,2):
t1 = time.time()
attack_input = 'a' * i + '$'
vulnerable_re.match(attack_input)
t2 = time.time()
print(i, '{0}s'.format(t2-t1) )
实测运行结果输出如下,可以清晰看到:某些情况下,输入长度仅增加 2 倍,耗时却增加上万倍。
1 0.0s
3 0.0s
5 0.0s
7 0.0s
9 0.0s
11 0.0s
13 0.0009975433349609375s
15 0.005021333694458008s
17 0.0189666748046875s
19 0.10775160789489746s
21 0.4058842658996582s
23 1.445460557937622s
25 6.071380138397217s
27 23.70104146003723s
29 90.62589955329895s
31 357.1210618019104s
33 1392.2017726898193s
4. 总结
正常匹配时正则引擎的回溯次数极少,而当正则表达式包含嵌套 / 重叠的重复量词(如(x+)+)且遇到接近匹配但最终不匹配的输入时,回溯次数会呈指数级暴增,这种现象就是正则的 "灾难性回溯"。