☰
Java 正则回溯导致 CPU 100%:ReDoS 的原理与五种防范写法
2026/10/21 7:11:22 网站建设 项目流程

一个接口,CPU 打到 100%。

不是流量上来了。是有人提交了一个 30 个字符的字符串。

问题出在一行正则上。这行正则还是 AI 帮我写的,比我原来那版“严谨”。

先看它长什么样

Pattern.compile("^(\\d+)+$");

看着没问题对吧。

它想干的事很简单:判断一个字符串是不是纯数字。\d+是一到多个数字,外面套一层(...)+表示这段可以重复一到多次,^$是首尾锚定。

我原来写的是:

Pattern.compile("^\\d+$");

同一个意思,写法不同。区别在哪,得先说引擎。

正则不是“扫描”,是“试”

Java 的java.util.regex用的是 NFA 回溯引擎。

它碰到(\d+)+这种嵌套量词时不会一遍过。它会先贪婪地吃掉所有数字,然后如果后面匹配失败,就吐一个字符出来重试。吐一个不行吐两个,每一层+都要重试一遍。

输入是这样的时候:

12345678901234567890123456789X

匹配会失败(结尾是个 X)。但失败之前,引擎要把“这串数字怎么切分”的所有组合都试一遍。

组合数量是 2 的 n-1 次方。n 是数字个数。

30 个数字,就是五亿多次尝试。

这就是 30 个字符能打满一个核的原因。这类问题有个名字叫ReDoS,正则表达式拒绝服务。

注意一点:它不是慢,是指数级慢。输入从 30 个字符加到 40 个字符,耗时不是多三分之一,是一千倍。

AI 为什么容易写出这种

我把它给过的几版正则都翻出来,有个共同点:它在追求“约束更严格”。

邮箱它是这么给的:

^([a-zA-Z0-9]+([._-][a-zA-Z0-9]+)*)@([a-zA-Z0-9]+([.-][a-zA-Z0-9]+)*)\.[a-zA-Z]{2,}$

手机号是这么给的:

^(1[3-9]\d)(\d{4})(\d{4})$|^(1[3-9]\d)-(\d{4})-(\d{4})$

嵌套量词 + 多分支,一个不少。

它的优化目标是“描述完整”,不是“最坏耗时可控”。

网上流传的正则里,被收藏最多的是那种“把格式抠到最死”的版本。那类正则的评判标准是“能不能覆盖所有合法写法”,没人去测它在恶意输入下的耗时。

AI 复现的就是那个形态。

我后来怎么改

一,能简单就别嵌套。

// 有风险^(\d+)+$// 安全^\d+$

嵌套量词在绝大多数校验场景里都是多余的。(\d+)+和\d+表达的是同一件事。

二,用占有量词或原子组。

// 占有量词:吃掉就不吐Pattern.compile("^\\d++$");// 原子组:括号内不回退Pattern.compile("^(?>\\d+)+$");

这两种写法会让引擎在匹配失败时直接放弃,不做全组合枚举。耗时从指数级回到线性。

代价是不再支持回溯,所以只适合“不需要回退”的校验类正则。

三,换线性引擎。

Java 自带的java.util.regex没有超时机制。真要对着不可信输入跑复杂正则,换 RE2J:

<dependency><groupId>com.google.re2j</groupId><artifactId>re2j</artifactId><version>1.7</version></dependency>

它保证线性耗时,代价是不支持反向引用和环视。

四,先把输入长度卡住。

最简单也最有效的一条:

if(input==null||input.length()>100){thrownewIllegalArgumentException("输入过长");}

指数级的东西,只要把 n 摁住就跑不起来。这一条能挡掉绝大多数 ReDoS。

五,有专用解析器的别用正则。

日期、CSV、URL、JSON、HTML,这些都有现成解析器。AI 给的正则看着能跑,但它的最坏耗时你算不出来,专用解析器的最坏耗时文档里写着。

现在我的检查习惯

AI 给正则,我只看两处:

  1. 有没有嵌套量词。(...)+、(...)*、(...){n,}套在量词上,看到就改。
  2. 有没有连续的.*.*。这种也是回溯杀手。

碰到绕不开的复杂正则,我会拿几个构造的输入试一下:一长串“几乎匹配但最后差一个字符”的字符串。接口卡住不动,就是它。

本地两分钟能测出来的东西,没必要等线上出事。

你写过最绕的正则是什么?有没有因为正则把服务拖死过?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询