DevFormatLab
← 返回博客列表

正则性能与灾难性回溯

作者 DevFormatLab Editorial·9 分钟阅读
正则性能ReDoS安全回溯

多数正则模式在微秒级就完成了。少数要几秒。少数会跑不完。区别很少来自数据、语言或引擎——而是模式本身的结构,更确切地说,是它在对抗性输入上是否允许指数级回溯。能识别并避开这些模式,正则就能放心用在用户输入上;做不到,用户就拿到了一个 60 秒拒绝服务的开关。

回溯怎么工作的,一句话

支持反向引用的正则引擎(除 RE2 外所有主流引擎)都会先尝试最长匹配,失败,回退一个字符,再试,失败,再回退一个,再试……永无止境。模式形状正常时,这种回溯是有界的:最坏 O(n × m),n 是输入长度,m 是模式长度。模式形状糟糕时,最坏 O(2^n)——指数级。一个 10 字符的模式在 60 字符的输入上可能要 60 秒;20 字符的模式在 100 字符的输入上可能比宇宙的年龄还长。

标志性结构:共享字符上的嵌套量词

灾难性回溯的经典形态是 (a+)+(a*)*(a|a)+,或任何让一个量化的组能以多种方式匹配同一段文本。引擎必须尝试把同一段文本在两个量词之间的每一种切分,切分数随输入长度指数增长。

已经在生产代码里见过的具体例子(每一个都会让 Node.js 进程冻结于一段 50 字符的输入):

  • (a+)+$ 配上 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!——末尾阻止匹配的字符让引擎对 a 串在两个量词之间的每一种切分都尝试一遍。
  • ^(\d+)+$ 配上 12345678901234567890123456789012345678901234567890!——同样结构,同样命运,更多数字。
  • ^([a-z]+)+$ 配任何无法完整匹配的字母串——长度上指数。
  • (.*a){20} 配任何以非 a 结尾的串——输入长度上指数。
  • ^(\w+\s?)*$ 配长空白串——指数。

为什么超时不是解药

对失控正则的本能修法是套个超时:匹配超过 N 秒就杀掉。这是纵深防御的最后一根稻草,是必要的兜底,但不是解药。原因如下:

  • 超时在工作做完后拒绝结果。CPU 已经烧完;延迟已经加到调用方的响应时间。60 秒匹配消耗了 60 秒 CPU 才等到超时,而在 60 秒之内匹配器可能还为了回溯栈分配了 GB 级内存。
  • 超时很难设对。太短,真正匹配超大输入(想想 100 MB 日志行)的合法请求被拒。太长,单个恶意请求能卡死一个 worker 线程。
  • 多数语言的正则 API 原生不支持取消。用来加超时的「工具库」(往往靠 setTimeout + setImmediate 之类的技巧)会泄漏 worker 线程,直到原始调用返回或操作系统回收进程。
  • 所以:保留超时,但当作最后一道防线。真正的解药是把模式重写到对指数回溯免疫。

    五种免疫的写法

    1. 互斥字符类代替选择分支。([a-z]+|[0-9]+)+ 也是脆弱的,因为两个分支在「空匹配」上重叠。([a-z]+|[A-Z]+|[0-9]+)+ 不是——分支互斥,引擎没有歧义可以回溯。把共享字符的分支改写成互斥的字符类,或者合并到一个字符类加量词。

    2. 支持的话用原子组。PCRE / Java 里的 (?>...) 告诉引擎:「这个匹配一旦成功,后面的回溯不能再打开它」。这消除了产生指数情形的歧义。ECMAScript 和 Python 不支持原子组,但下面四种写法同样能拿到这个效果。

    3. 支持的话用占有量词。a++a+?(占有语义)、[a-z]{n,m}+。与原子组效果相同,但作用在量词上。PCRE 和 Java 支持;ECMAScript 与 Python 不支持。

    4. 阻止引擎匹配两次。所有灾难性模式的标志都是「同一段文本可以用两种方式匹配」。一旦看到这种情况,重写让第二次匹配不可能。通用做法:a*a* 改成 a*a*a 改成 a+(a*)* 改成 a*;共享原子上嵌套的量词合并成单个量词。

    5. 用不回溯的引擎。RE2(Go),C++ 与 Rust 里 RE2 派生的库,以及任何带 hyperscan 家族绑定的语言。这些引擎保证时间复杂度对输入长度线性。代价:没有反向引用,没有(简单情形的)环视,特性集略有不同。回报:正则可以放心跑在用户输入上而不用超时。

    在发布前发现

    用于检测正则灾难性回溯的静态分析工具已经成熟。任何一条接触用户输入的正则模式都该在 CI 跑这些:

    • regexploit——Python,针对模式分析恶意输入语料。
    • rxxr——JavaScript,ECMAScript 正则的静态分析器。
    • 交互测试——打开本站的 正则测试工具,粘进去你的模式和能构造出的最小失败输入,看引擎转多久。1 KB 输入要超过 100 ms 就值得怀疑。

    手工识别规则:只要看到「量词里包量词」「分支能产生空匹配」「反向引用指向内容与另一组重叠的组」,就把模式按三种写法改,然后在你能构造的最对抗 10 KB 串上分别 benchmark。最慢的那种就是该修的地方。

    收尾

    灾难性回溯是 2026 年生产环境里最常见的正则相关故障,唯一持久的解药是写出引擎无法歧义匹配的模式。上面五种写法都简短,适用每个主流正则风味,把正则从安全负债变回有用的工具。防御:正则测试工具 做 sanity check,正则基础指南 介绍涉及的特性。

    相关工具