ReDoS(Regular Expression Denial of Service,正则表达式拒绝服务)指的是:一条语法完全正确、平时跑得好好的正则,在某种特定输入下触发「灾难性回溯」,让 CPU 在一个核上跑到 100%,把服务拖垮。
回溯是怎么变成爆炸的
多数编程语言用的正则引擎是 NFA(非确定性有限自动机)实现,靠回溯(backtracking)来匹配:从左往右走,遇到分支先试第一条路,走不通就退回来试下一条。
打个比方:你在核对一叠账单,每笔钱都有「算进去」和「不算进去」两种可能。如果最后总和不对,你只能把每一种组合都重算一遍——n 笔账单就是 2ⁿ 种组合。回溯引擎就是这么干的。
看这条几乎人人写过的正则:
```
^(a+)+$
```
它匹配 aaaa...a(一长串 a)没问题。但如果结尾多一个其他字符,比如 aaaa...a!,引擎必须先认定「不匹配」,而 a+ 外面还套了 +,同一段 a 有指数多种切分方式(内层吃几个、外层吃几个)。结尾失败后,它得把所有切分方式全试一遍才敢下结论。输入每多一个 a,耗时翻一倍——二十几个字符就能卡住好几秒。
危险的不是正则写错了,而是它只在长输入 + 结尾失败时引爆。日常测试用小字符串,永远测不出来。
和相邻概念的区别
| 维度 | 普通慢正则 | ReDoS |
|---|---|---|
| 变慢的原因 | 单次扫描常数大,如全表扫 | 回溯次数随输入长度指数增长 |
| 输入变长后的表现 | 大致线性增长 | 长度每加 1,耗时约翻倍 |
| 什么时候暴露 | 一直慢,压测就能发现 | 小输入正常,恶意输入瞬间雪崩 |
它和 SQL 注入、XSS 的区别在于:ReDoS 不窃取数据、不篡改内容,只是把计算资源烧光,属于可用性攻击。也和「算法复杂度写错」不同——攻击者能主动构造那条最坏输入。
对实际工作的意义
凡是有用户可控的字符串进入正则,就要假设对方会构造最坏输入。几个立即可用的动作:
- 限制输入长度。正则之前先截断,几十个字符的输入造不出指数爆炸。
- 设置超时或中断机制,别让一次匹配无限拖住线程。
- 换引擎。基于自动机的引擎(如 RE2、Go 标准库的 regexp)保证线性时间,代价是不支持反向引用和环视。
- 改写法。避免嵌套量词
(a+)+、(.*)*、(\w+\s?)*这类结构;用字符类替代宽泛的.;能让分支互斥就别让它们重叠;部分引擎支持占有量词或原子组,可以关掉回溯。
对不写代码的人来说,这件事的体感是:某个接口突然全部超时,CPU 打满,重启能好一阵,过一会儿又复发——很可能就是日志过滤、表单校验或者一个爬虫解析规则里藏了这么一条正则。
顺带提醒:在 LLM 应用里,正则常被用来解析模型输出、脱敏、校验工具参数,如果这些环节的输入来自外部,风险完全一样。具体引擎的能力边界,以官方文档为准。
