你从 Stack Overflow 复制了一条友好的单行正则来"验证邮箱"。测试里一切正常。六个月后,一位安全研究员给你发邮件:一个精心构造的字符串能让你的 API 卡死九十秒。元凶不是你的服务器,不是你的框架——是正则里三个字符在悄悄做指数级的工作。这就是 ReDoS(正则表达式拒绝服务),生产代码里最被低估的 Bug 之一。
本指南从引擎层面讲清:为什么一个看起来无害的模式会爆炸、如何在上线前识别危险、以及如何重写模式让它保持线性。这里的每个例子都可以在我们的100% 客户端正则测试器里实时验证,不会向服务器发送你数据的任何一个字节。
正则引擎到底怎么匹配
要理解失败,先要有引擎如何匹配的正确心智模型。大多数主流语言(PCRE、Python、Java、无特殊标志的 JavaScript)使用回溯型引擎。它尝试一条路径去匹配,一旦某步失败,就回退到上一个决策点,尝试下一种可能。可以把它想象成对"模式所有可能的贴合方式"做深度优先搜索。
对干净的模式和干净的输入,这又快又不可见。麻烦始于模式给了引擎许多条等价路径去满足同一个约束——每多一条等价路径,工作量就乘一次。
经典炸弹:(a+)+$
考虑用模式 (a+)+$ 匹配字符串 aaaaaaaaaaaaaaaaaaaaaaaaaab——二十六个 a 跟一个 b。由于结尾的 b 和锚定的 $,这个匹配永远不可能成功。但看看引擎有多少种方式给这些 a 分组:
- 外层分组一次:内部
a重复 26 次。 - 外层分组两次:25+1、24+2、23+3……各种切法。
- 外层分组三次:所有切成三段的划分。
- 以此类推,穷尽所有可能的划分。
划分数大致按贝尔数增长——超指数级。在 b 前多加一个 a,运行时间就可能翻倍或三倍。在真实输入上,几个字符之内就会从"瞬间"变成"请求超时"。这就是灾难性回溯。
为什么真实事故会发生
ReDoS 不是纸上谈兵。公开的事故复盘包括:2019 年 Cloudflare 的一次全球故障,源于 WAF 正则里一条灾难性规则在他们边缘节点上吃光了 CPU;以及大量流行库的 CVE——一个邮箱、URL 或 XML 校验器可以被一个恶意字符串冻住。共同形状永远一样:一个本用于"简单校验"的模式,包含嵌套或重叠的量词,并被喂以攻击者可控的输入。
阴险之处在于它能通过代码评审。模式看起来正确,测试在正常数据上全绿,爆炸只出现在反例上——恰恰是攻击者会去探测的那种情况。
实用检测清单
信任一条模式之前,扫一遍这些红旗:
- 嵌套量词:量词(
、+、{n,})作用在一个自身含量词的分组上,如(a+)+、(a)*。 - 重叠的分支选择:两个分支能匹配相同的字符,如
(a|a)+或(.+|.*),让引擎无休止地重排。 - 未锚定的中部:
.*紧挨着另一段贪婪匹配,产生无数切分方式。 - 近似重复的捕获组:多个分组都能吞掉同一段字符。
如果你在处理用户输入的模式里看到任何一条,做基准测试:构造一个几乎匹配但结尾失败的字符串,然后每次加长一个字符并测量耗时。线性模式的耗时是一条直线;脆弱模式的曲线会剧烈上扬。
如何修复不安全的模式
四个动作能把大多数模式拉回线性:
- 使用原子分组或占有量词。PCRE/Java 里
(?>...)或++告诉引擎"一旦吞下,绝不吐回"。这一个改动就能杀死进入分组的回溯。 - 用字符类替换有歧义的分支。
(ab|ac)改成a[bc],匹配方式唯一。 - 两端锚定并写具体。
^[a-z]+$远比漂浮在宽松模式中间的[a-z]+安全。 - 用解析器,别用模式。对 HTML、CSV 或嵌套结构,用真正的解析器。正则只属于扁平、边界清晰的文本。
用纯本地工具安全测试
直觉做法——"把模式和测试字符串粘进一个在线正则测试器"——有和永不粘贴密钥指南里一样的隐私问题:很多此类网站会把你的输入送到服务器。如果你的正则里包着一段真实用户数据样本,那次上传就是一次泄露。
我们的正则测试器完全运行在你的浏览器里。你可以对一个模式投掷对抗性的"差一点就匹配"的字符串来证明它是线性的,最坏结果只是你自己的标签页卡住——数据从不离开页面。这才是做这类对抗性测试的正确场所。
常见问题
问:正则里的灾难性回溯是什么? 答:当一个含有重叠或嵌套量词的模式匹配失败时,引擎在放弃之前会尝试指数多种切分输入的方式。像 (a+)+$ 这样的模式在短字符串上只需毫秒,在稍长一点的、不匹配的字符串上可能需要几秒甚至几分钟。
问:我怎么知道我的正则是否易受 ReDoS 攻击? 答:找嵌套量词、两个分支都能匹配相同字符的分支选择,以及允许引擎用多种等价方式切分字符串的模式。最安全的检验是:用一个比常规输入长一个字符的"差一点匹配"的字符串做基准测试,观察耗时增长。
问:ReDoS 只是服务端的问题吗? 答:不是。任何在用户控制的输入上运行用户提供的或用户触发的模式的代码路径都暴露在风险中,包括浏览器端校验。如果恶意访客能选择输入,他们冻住一个标签页和冻住一个服务器线程一样容易。
问:正则测试器能保护我免受 ReDoS 吗? 答:如果它本地运行并显示耗时,就有帮助。Plobi-kit 的正则测试器 100% 运行在浏览器里,你可以放心地对模式投掷对抗字符串而不发送任何数据。灾难性模式仍会卡住标签页——这正是你学到它不安全的方式——但数据从不离开页面。
问:如何修复一条不安全的正则? 答:优先用原子分组或占有量词,让引擎无法回溯进捕获组。用字符类替换有歧义的分支,两端锚定,避免在重叠内容上嵌套量词。可能的话,用专门的解析库代替一条巨型模式。