资讯动态

Java 正则回溯导致 CPU 100%:ReDoS 的原理与五种防范写法

发布时间:2026/10/22 18:36:43 来源:尧图企业网站定制
一个接口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没有超时机制。真要对着不可信输入跑复杂正则换 RE2JdependencygroupIdcom.google.re2j/groupIdartifactIdre2j/artifactIdversion1.7/version/dependency它保证线性耗时代价是不支持反向引用和环视。四先把输入长度卡住。最简单也最有效的一条if(inputnull||input.length()100){thrownewIllegalArgumentException(输入过长);}指数级的东西只要把 n 摁住就跑不起来。这一条能挡掉绝大多数 ReDoS。五有专用解析器的别用正则。日期、CSV、URL、JSON、HTML这些都有现成解析器。AI 给的正则看着能跑但它的最坏耗时你算不出来专用解析器的最坏耗时文档里写着。现在我的检查习惯AI 给正则我只看两处有没有嵌套量词。(...)、(...)*、(...){n,}套在量词上看到就改。有没有连续的.*.*。这种也是回溯杀手。碰到绕不开的复杂正则我会拿几个构造的输入试一下一长串“几乎匹配但最后差一个字符”的字符串。接口卡住不动就是它。本地两分钟能测出来的东西没必要等线上出事。你写过最绕的正则是什么有没有因为正则把服务拖死过

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑