别再乱写正则了!一行 regex 可能让你的网站瘫痪 10 分钟

0 阅读2分钟

它不是 bug,是黑客精心设计的“CPU 杀手”。

你是否在项目中写过类似这样的正则?

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;

看起来没问题?
但如果用户输入一个特殊构造的字符串,比如:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!

你的服务可能瞬间 CPU 100%、响应超时、进程卡死——而这一切,只因一行“看似无害”的正则。

这就是 ReDoS(Regular Expression Denial of Service)用正则表达式发起的拒绝服务攻击


什么是 ReDoS?原理揭秘

ReDoS 的核心在于:某些正则表达式在匹配失败时,会触发指数级回溯(backtracking)

来看一个经典例子:

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 风险?

方法一:使用静态分析工具

  • eslint-plugin-security

    npm install --save-dev eslint-plugin-security
    

    配置后可自动警告危险正则。

  • safe-regex(简单检测)

    const safe = require('safe-regex');
    console.log(safe(/^(a+)+$/)); // false → 危险!
    

注意:safe-regex 并非 100% 准确,仅作初步筛查。

方法二:人工审查“回溯陷阱”

检查你的正则是否包含:

  • 两个以上连续量词(+, *, {n,m});
  • 可选部分与重复部分重叠;
  • 使用 .*.+ 匹配长文本。

安全写法:三招规避 ReDoS

第一招:避免嵌套量词

危险:

/^(a+)+$/

安全:

/^a+$/

第二招:用原子组(Atomic Grouping)或占有量词(Possessive Quantifier)

虽然 JavaScript 原生不支持,但可通过限制回溯模拟:

例如,邮箱校验不要自己写复杂正则,改用:

// 简单验证 + 业务层确认
if (!value.includes('@') || value.indexOf('@') !== value.lastIndexOf('@')) {
  throw new Error('Invalid email');
}

第三招:设置匹配超时(Node.js 18+)

Node.js 18 引入了 RegExpdotAll 和实验性超时,但更实用的是手动封装超时

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 也能毁掉整个系统。

转发给你团队里那个“正则高手”吧!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!