ReDoS 是什么 - 正则表达式灾难性回溯与检测方法
🌟 一个正则怎么会”卡死”程序
有些正则,在正常输入下瞬间完成,但换一段特定文本后,会慢到让程序假死。这不是 bug,而是灾难性回溯(Catastrophic Backtracking)——正则引擎在尝试所有可能的分支组合,耗时随输入长度指数级增长。
攻击者可以利用这一点,用一小段恶意文本让你服务器 CPU 打满,这就是 ReDoS(Regular Expression Denial of Service,正则表达式拒绝服务)。
🧠 为什么会产生灾难性回溯
正则引擎(回溯型 NFA)在匹配失败时会”回溯”——退回去尝试其他分支。当表达式里存在量词嵌套量词、相邻的歧义量词或可选的重复结构时,回溯的分支数量会爆炸式增长。
经典危险写法:
^(a+)+$对输入 aaaaaaaaaaaaaaaaaaaaaaaaX,引擎会穷举”如何把这段 a 分组”的所有方式,数量随 a 的个数指数增长。
危险结构清单
| 结构 | 例子 | 风险 |
|---|---|---|
| 嵌套量词 | (a+)+、(a*)* | 高 |
| 相邻歧义量词 | a*a*、\d+\d+ | 高 |
| 可选重复 | (a|aa)+ | 高 |
| 回溯量词 + 长尾失败 | (.*)*、(\w+\s?)* | 高 |
| 嵌套可选 | (a?)* | 中 |
🔍 如何检测
- 静态判断:看正则里是否有”量词套量词""相邻歧义量词”。有就要警惕。
- 实测计时:用一段”几乎匹配但最后不匹配”的长文本测试,观察耗时是否随长度急剧上升。
- 工具辅助:星点性能与安全工具提供复杂度分析、回溯分析与 ReDoS 启发式检测,并可对大文本做可中止的压力测试。
说明:ReDoS 检测多为启发式判断,结论应结合具体输入与引擎,不能当作绝对定论。
自测小实验
在正则测试器里输入正则 ^(a+)+$,测试文本写一长串 a 后面跟一个 X(不匹配)。如果文本越长页面越卡,就说明存在灾难性回溯。
🛠 如何修复
| 问题写法 | 改进方向 |
|---|---|
(a+)+ | 去掉多余嵌套,写成 a+ |
a*a* | 合并为 a*,或让两部分互斥 |
(.*)* | 去掉外层量词 |
(\w+\s?)* | 改为不重叠的写法,或使用原子组/占有量词(部分引擎支持) |
| 复杂长正则 | 拆成多步,或先做简单校验再细化 |
核心原则:让引擎没有”歧义分支”可回溯——各部分尽可能互斥、不重叠。
其他缓解手段
- 加锚点:
^$限定位置,减少无效尝试; - 限定字符集:用
[0-9]代替.; - 限制输入长度:对用户输入先做长度上限;
- 用非回溯引擎:如 RE2(Go)不支持反向引用与部分断言,天然规避;
- 原子组/占有量词:
(?>...)、a++(部分引擎支持)。
🌐 各引擎差异
| 引擎 | 回溯型 | 原子组/占有量词 |
|---|---|---|
| JavaScript | 是 | 部分(现代) |
Python re | 是 | 否(regex 库支持) |
| Java | 是 | 支持 |
| PCRE | 是 | 支持 |
| Go(RE2) | 否 | —(无回溯) |
用多语言引擎核对特性支持。
🛡 工程实践
- 对用户可控的正则(如自定义规则)做白名单/长度限制;
- 上线前用性能与安全做一次检测;
- 大文本处理放在可中止的流程里,避免页面/服务卡死;
- 关键服务可设置超时。
❓ 常见问题(FAQ)
所有慢正则都是 ReDoS 吗?
不一定。处理超长文本本身也会慢。ReDoS 特指"回溯分支指数增长"导致的急剧变慢。后端语言也有这个问题吗?
有。只要使用回溯型正则引擎(如 Java、Python、PHP 的默认引擎),都可能触发。换成 DFA 引擎就没问题了吗?
DFA/RE2 不做回溯,天然规避这类问题,但功能(如反向引用、部分断言)受限。怎么快速排查现有正则?
把正则贴进[性能与安全](https://www.xingdian.net/app/regex/performance/),看复杂度与 ReDoS 提示。懒惰匹配能避免回溯吗?
不一定。懒惰减少匹配范围,但某些结构仍会回溯,需实测。正则会卡死浏览器吗?
可能。所以大文本测试要用"可中止"的方式,或用[性能与安全](https://www.xingdian.net/app/regex/performance/)分块测试。输入长度有限制就没问题吗?
长度限制能缓解,但指数增长下短输入也可能很慢,仍应优化结构。怎么衡量正则复杂度?
看分组数、嵌套层级、分支数、量词数、潜在回溯结构,[性能与安全](https://www.xingdian.net/app/regex/performance/)会给出报告。🧪 更多危险模式与修复对照
| 危险写法 | 危险原因 | 安全替代 |
|---|---|---|
^(a+)+$ | 分组内量词 + 外量词嵌套 | ^a+$ |
^(a|aa)+$ | 分支前缀重叠 | ^a+$ |
^(\d+)*$ | 嵌套量词 | ^\d+$ |
(\w+\s?)* | 可选 + 重复,产生歧义 | 明确分隔,如 (\w+\s)*\w+ |
.*.*.* | 多个歧义 .* | 合并为 .* |
([a-zA-Z]+)* | 嵌套量词 | [a-zA-Z]+ |
^([^,]+)*$ | 字符类量词嵌套 | 明确分隔符 |
判断口诀:如果一个量词作用在一个”本身可重复”的分组上((...+)*、(...*)*、(...+)+),就要警惕。
🧠 回溯原理详解
以 (a+)+$ 匹配 aaaaX 为例(X 不匹配):
- 外层
+先让内层a+吃尽量多的 a; - 遇到 X 无法匹配
$,引擎回溯; - 内层退一个 a,外层再试;
- 由于外层和内层都能”分配”这些 a,组合方式多达 2^(n-1) 种;
- n 稍大,尝试次数就爆炸。
关键问题:外层和内层对同一段字符都有”解释权”,导致重复尝试。消除方法是让它们不重叠(如直接 a+)。
🛡 服务端防护清单
- 对用户可控的正则做长度上限与白名单;
- 对输入文本做长度上限;
- 关键正则做超时/可中止处理;
- 上线前用性能与安全扫描;
- 高并发场景优先使用 RE2/DFA 引擎;
- 代码评审时检查
(...+)*类结构。
📊 复杂度与回溯的量化
| 结构 | 分组数 | 嵌套 | 回溯风险 |
|---|---|---|---|
\d+ | 0 | 0 | 无 |
(\d+) | 1 | 1 | 低 |
(\d+)+ | 1 | 2 | 高 |
(\d+)* | 1 | 2 | 高 |
| `(a | ab)+` | 1 | 2 |
(\w+\s?)* | 1 | 2 | 高 |
判断要点:量词是否作用在”本身可重复”的元素上。是,则有回溯风险。
🧰 相关工具
🧭 更多常见问题
检测结果一定准确吗?
不一定。多为启发式判断,需结合具体输入与引擎;建议配合实测。为什么我的正则平时很快?
回溯爆炸只在特定"接近匹配但最终失败"的输入上触发,平时可能没遇到。原子组怎么用?
`(?>...)` 表示匹配后不回溯,能消除部分歧义(部分引擎支持)。大文本测试会不会把页面卡住?
用[性能与安全](https://www.xingdian.net/app/regex/performance/)的"可中止"压力测试,避免真的卡死。怎么给正则加超时?
多数语言的正则库不支持超时,需放到独立线程/进程并设限,或改用 RE2。ReDoS 只在正则里发生吗?
主要发生在回溯型正则引擎;其他回溯型解析(如某些模板引擎)也可能有类似问题。💬 总结
ReDoS 的本质是”歧义结构 + 回溯爆炸”。写正则时避免量词嵌套、相邻歧义量词,必要时拆分或改互斥写法;上线前用性能与安全做一次检测。
立即体验:访问 性能与安全
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
部分内容可能已过时