28 个字符的正则,让 Node 卡了 2.7 秒

举报
yd_293143657 发表于 2026/11/11 00:47:03 2026/11/11
【摘要】 线上有个校验接口,正常请求 2 ms 返回。有人传了一个 28 字符的字符串,单个请求耗掉 2.7 秒,期间整个 Node 进程不响应——因为正则跑在主线程上,阻塞就是全阻塞。 一条能复现的正则const VULN = /^(\w+\s?)*$/;const SAFE = /^\w+(?:\s\w+)*$/;喂一个「合法字符加末尾一个感叹号」的串,逼引擎把所有可能的分组方式试一遍:输入长度易...

线上有个校验接口,正常请求 2 ms 返回。有人传了一个 28 字符的字符串,单个请求耗掉 2.7 秒,期间整个 Node 进程不响应——因为正则跑在主线程上,阻塞就是全阻塞。

一条能复现的正则

const VULN = /^(\w+\s?)*$/;
const SAFE = /^\w+(?:\s\w+)*$/;

喂一个「合法字符加末尾一个感叹号」的串,逼引擎把所有可能的分组方式试一遍:

输入长度 易受害模式 安全模式 倍数
16 8.12 ms 0.041 ms 198x
18 3.44 ms 0.034 ms 101x
20 13.50 ms 0.003 ms 4500x
22 56.33 ms 0.003 ms 18777x
24 176.31 ms 0.008 ms 22039x
26 687.72 ms 0.003 ms 229240x
28 2771.88 ms 0.002 ms 1385940x

每多两个字符,耗时翻四倍。按这个斜率,32 个字符约 44 秒,40 个字符就是两小时——一个 HTTP 请求,一行 40 字节的字符串。

为什么是指数

(\w+\s?)* 里有两层量词:外层 * 重复整个分组,内层 + 重复 \w。关键在于两层匹配的字符集重叠——aaaa 这一串,可以整体算一个 \w+,也可以拆成 aa+aa、a+aaa,共 2 的 n-1 次方种切法。

引擎用回溯实现:先贪心地一次吃光,失败后吐回一个字符重试。末尾的 ! 让整个匹配注定失败,于是它必须把所有切法都走一遍才能确认「不匹配」。这就是灾难性回溯(catastrophic backtracking)。

安全版的 ^\w+(?:\s\w+)*$ 没有这个歧义:\w+ 只在开头出现一次,后面的重复单元必须以空白开头,而空白不属于 \w,两层之间边界确定,退化为线性扫描。实测在 28 字符上 2 微秒。

怎么一眼认出来

危险形状只有一个:同一个字符集被内外两层量词重复。

  • (a+)+、(a*)*、(\w+\s?)* —— 分组内是量词,分组外还是量词
  • (\d+|[a-z]+)+ —— 分组内是选择分支,分支之间能匹配同样的字符
  • ^(\S+\.)+\S+$ 这类「点号分隔」的老写法,末尾补个非法字符就炸

反过来,(?:\w+\s)+ 因为分隔符不属于 \w,就不危险。判断标准不是「有没有嵌套量词」,而是「内层能匹配的字符,外层还能不能再匹配一次」。

修法有优先级

一,改写成无歧义的形状。 这是唯一治本的做法,成本和风险都最低。把 (\w+\s?)* 换成 \w+(?:\s\w+)* 即可。改完先跑一遍原来的用例:hello world、a b c、a<TAB>b 依旧匹配,两个连续空格的 a b 依旧不匹配,语义没变。

二,先卡长度。 指数爆炸的前提是输入足够长。校验前先判断 s.length > 64 直接返回 false,等于给所有正则加了一层硬顶。代价是零,收益是把「几十秒」变成「不可能发生」。

三,换线性引擎。 Go 的 regexp、Rust 的 regex crate 用的是 NFA 模拟,最坏 O(n·m),永远不回溯。如果你的校验逻辑能挪到这类服务里,等于从根上免疫。反过来,Java 的 java.util.regex、Python 的 re、PCRE、JS 的 RegExp 都是回溯实现,写法该谨慎还是一样谨慎。

四,别指望超时。 JS 没有原子分组,也没有占有量词,正则一旦开始回溯,没有任何 API 能在中途掐断。setTimeout 在主线程阻塞时同样不会触发。

一个容易漏的点

不是只有 test() 会中招。replace() 用 g 标志全局匹配时,每个位置都要试一次;split() 带捕获组的分隔符同理;matchAll() 更明显。前面那张表的耗时是按单次 test() 算的,换成 s.replace(VULN, '') 只会更慢。

所以排查时不要只盯 test,把项目里所有正则统一过一遍量词结构。绝大多数线上 ReDoS 不是有人恶意构造,而是某个字段校验规则从 2019 年写下来就没人再看第二眼。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。