先做一个一分钟实验

打开终端跑这一行 Python(re 是回溯型引擎,大多数语言的默认正则一样):

1
python3 -c "import re; re.match(r'^(a+)+$', 'a'*29+'X')"

29 个 a 加一个 X,看起来毫不起眼。在笔记本上实测:a 的个数是 20 时约 0.09 秒,24 个约 1.3 秒,29 个要跑约 43 秒才报出"不匹配"。原因在于 ^(a+)+$ 面对连续 a 时,外层分组和内层 a+ 对"这些 a 怎么切分"各执一词,引擎会把 29 个字符的全部切分方式穷举一遍——共 2^28 ≈ 2.7 亿次。每多一个 a,工作量翻倍,这就是灾难性回溯(ReDoS):不匹配的输入越长,耗时指数上涨,一个请求就能吃满一个核。

2016 年 Stack Overflow 全站宕机约半小时的著名事故,起因就是一条处理用户输入的正则踩了同样的坑。ReDoS 不需要什么高级攻击技巧,一段恰好够长的普通输入就够了。

四种高危模式,对着清单自查

1. 量词套量词

反例:(a+)+、(\d+)*、([\w-]+)+。内层和外层都能"伸缩",切分方式指数爆炸。正例:合并成一层,a+、\d+。

2. 分支重叠且外带量词

反例:(a|aa)+——在同一个位置,a 和 aa 都能匹配,又是指数级切分。正例:拍平为单一量词 a+;确实需要分支时,让分支在任意位置互斥(比如 [a-m] 和 [n-z] 这类不相交字符类)。

3. 大边界重复堆叠

反例:(a{1,4}){5,20}。单层边界看着温和,两层相乘后等价于"5 到 80 个 a 的模糊区间",切分方式照样指数涨。正例:语义等价的拍平写法 a{5,80}。

4. 相邻可变片段

反例:^(\w+\s?)*$——\w+ 和 \s? 谁吞掉这个空格说不清,这正是 Stack Overflow 事故里那条正则的形状。正例:合并为一个字符类 ^[\w\s]*$,一次吃完,无从回溯。

上线前,先用检测器扫一遍

人工排查靠记性,机器排查靠工具。把正则贴进 ReDoS 风险检测器,它对正则做静态分析,标出灾难性回溯风险的危险片段——问题出在哪个子表达式、是嵌套量词还是分支重叠——并给出修复建议。

建议把它变成 review 习惯:新正则进代码库之前扫一遍,线上出现可疑的正耗时告警之后回来再扫一遍。

修复范式:三招外加两个高级语法

  1. 拍平嵌套、消灭重叠:(a+)+ → a+;(a{1,4}){5,20} → a{5,80};(\w+\s?)* → [\w\s]*。多数危险正则的正确形态是"一层量词 + 一个字符类"。
  2. 给输入限长:服务端先限制待匹配文本长度(比如不超过 200 字符)。指数爆炸需要足够长的触发串,限长等于拆掉引信。
  3. 换线性时间引擎兜底:Go 的 RE2、Rust 的 regex 用自动机实现,天然线性时间,不存在灾难性回溯,重负载场景值得考虑。

另外两个高级语法,用之前先确认运行时支持:原子组 (?>...) 和占有量词 a++、*+,都是让引擎"吃进字符后不许吐回",从根上切断回溯。PCRE、Java、.NET、Python 3.11+ 都支持;JavaScript 至今没有原生支持,写跨端正则别依赖它们,老老实实用第 1 招改写。

改写完别急着上线,用正则表达式测试做实时匹配验证:新旧两条正则跑同一组用例(正常输入、触发串、边界值),高亮对比匹配结果,确认语义没有改坏——修 ReDoS 最怕修出新 bug。

上线前检查清单

  1. 正则里有没有"量词后面跟量词"的形状(+)*、{1,4}){5,20})?
  2. 分支是否有公共前缀,且外面套了量词?
  3. 相邻片段是否都能匹配同一类字符(\w+\s? 式的"抢字符")?
  4. 待匹配文本的长度是否已在服务端限制?
  5. 新正则是否已经在检测器里扫过一遍?

四个问题都答"没有"、最后一项答"扫过",才算过关。

常见问题

只有处理用户输入的正则才有风险吗?

不是。日志行、文件路径、CSV 单元格、AI 模型的输出,只要进入正则的文本长度不可控就可能触发。哪怕是只匹配固定文本的正则,写坏了也会让构建和测试阶段莫名卡死。

开启多行模式或 Unicode 模式能避免吗?

不能。那两个开关只改变匹配语义,不改变回溯行为。结构性的问题要靠改写模式本身,或者换 RE2 这类线性时间引擎。

检测器说没风险,就高枕无忧了吗?

静态分析覆盖的是已知危险结构,给的是风险评估而不是运行时保证。上线后仍建议保留输入限长,并对正则调用的耗时加监控,双保险。

小结

ReDoS 的教训可以浓缩成一句话:量词套量词,必有回溯险。写的时候多看一眼结构,入库前用 ReDoS 风险检测器扫一遍,改写后用正则表达式测试验证语义,三步走完,基本就能和"半夜半个 CPU 的神秘告警"说再见。

这类日常高频的开发小工具,还收录在前端日常工具包专题里,值得一并收藏。