它不是 bug,是黑客精心设计的"CPU 杀手"。
你是否在项目中写过类似这样的正则?
js
const emailRegex = /^([a-zA-Z0-9._%-]+)+@([a-zA-Z0-9.-]+\.)+[a-zA-Z]{2,}$/;
const urlRegex = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
const tagRegex = /<(\w+)(\s[^>]*)?>.*?<\/\1>/g;
看起来没问题?
但如果用户输入一个特殊构造的字符串,比如:
text
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
你的服务可能瞬间 CPU 100%、响应超时、进程卡死------而这一切,只因一行"看似无害"的正则。
这就是 ReDoS(Regular Expression Denial of Service) :用正则表达式发起的拒绝服务攻击。
什么是 ReDoS?原理揭秘
ReDoS 的核心在于:某些正则表达式在匹配失败时,会触发指数级回溯(backtracking)。
来看一个经典例子:
js
const evilRegex = /^(a+)+$/;
console.time('match');
evilRegex.test('aaaaaaaaaaaaaaaaaaaa!'); // 注意结尾的 !
console.timeEnd('match');
在普通电脑上,这段代码可能耗时:
20 个 a + !→ 几十毫秒30 个 a + !→ 几秒50 个 a + !→ 几分钟甚至永不结束!
为什么?
因为 (a+)+ 存在重复嵌套量词(catastrophic backtracking):
- 引擎尝试所有可能的
a+分组方式; - 当遇到
!匹配失败时,它要回溯所有组合; - 组合数呈指数爆炸(2ⁿ 级别)。
黑客只需提交一个几十字符的字符串,就能让你的服务器"思考到死"。
哪些正则容易中招?
以下模式高危:
| 危险结构 | 示例 |
|---|---|
| 嵌套量词 | (a+)+, (a*)*, (a+)* |
| 模糊重复 | .*.*, .+.+ |
| 可选重叠 | (a/aa)+, (a/a?)+` |
| 不明确分隔 | /^([a-z]+)*$/ |
尤其常见于:
- 邮箱/URL/手机号校验;
- 富文本标签提取(如
<div>...<div>); - 用户输入过滤(如关键词屏蔽);
- 日志解析(自定义格式匹配)。
真实案例:知名 npm 包因 ReDoS 被下架
moment:旧版本日期解析正则存在 ReDoS 风险;lodash:_.template曾因模板正则被曝 ReDoS;validator.js:多个校验函数(如isEmail)历史上多次修复 ReDoS。
你的项目如果依赖了这些库的旧版本,也可能"躺枪"。
如何检测 ReDoS 风险?
方法一:使用静态分析工具
-
bash
npm install --save-dev eslint-plugin-security配置后可自动警告危险正则。
-
safe-regex(简单检测)
jsconst safe = require('safe-regex'); console.log(safe(/^(a+)+$/)); // false → 危险!
注意:
safe-regex并非 100% 准确,仅作初步筛查。
方法二:人工审查"回溯陷阱"
检查你的正则是否包含:
- 两个以上连续量词(
+,*,{n,m}); - 可选部分与重复部分重叠;
- 使用
.*或.+匹配长文本。
安全写法:三招规避 ReDoS
第一招:避免嵌套量词
危险:
js
/^(a+)+$/
安全:
js
/^a+$/
第二招:用原子组(Atomic Grouping)或占有量词(Possessive Quantifier)
虽然 JavaScript 原生不支持,但可通过限制回溯模拟:
例如,邮箱校验不要自己写复杂正则,改用:
js
// 简单验证 + 业务层确认
if (!value.includes('@') || value.indexOf('@') !== value.lastIndexOf('@')) {
throw new Error('Invalid email');
}
第三招:设置匹配超时(Node.js 18+)
Node.js 18 引入了 RegExp 的 dotAll 和实验性超时,但更实用的是手动封装超时:
js
function testRegexWithTimeout(regex, str, timeoutMs = 100) {
return new Promise((resolve) => {
const timer = setTimeout(() => resolve(false), timeoutMs);
const result = regex.test(str);
clearTimeout(timer);
resolve(result);
});
}
// 使用
const isSafe = await testRegexWithTimeout(/^(a+)+$/, 'aaaa...!', 50);
if (!isSafe) throw new Error('Possible ReDoS attack');
终极建议:能不用正则,就不用
对于复杂格式(如邮箱、URL、HTML),优先考虑:
- 使用专用库(如
validator.js的最新版); - 用解析器代替正则(如 DOMParser 解析 HTML);
- 先做长度限制(如
if (input.length > 255) return false); - 在沙箱或 Worker 中执行高风险正则。
结语
正则表达式是强大的工具,
但不当使用,它就是埋在你代码里的"逻辑炸弹"。
记住:
用户输入 + 复杂正则 = 潜在 DoS 攻击面。
下次写 /.../ 之前,请先问自己:
"这个正则,会被恶意字符串卡死吗?"
安全无小事,一行 regex 也能毁掉整个系统。
转发给你团队里那个"正则高手"吧!
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!