侧边栏

正则ReDOS攻击

发布于 | 分类于 前端/前端业务

对用户的输入进行校验,是一个系统稳健性最基础的要求,不只前端要对用户提交的输入进行校验,后端也会对输入进行再次校验。

要校验输入是否符合特定要求,正则表达式是比较常用的手段。这里就有一个潜在的安全问题:ReDoS(Regular Expression Denial of Service) 正则表达式拒绝服务攻击。

本文将介绍这种不是很常见的Web安全问题。

ReDoS是一种通过构造恶意输入,使正则表达式匹配过程陷入极端低效状态,从而耗尽系统资源(如 CPU 时间)的安全攻击。

简单来说,就是错误的正则遇到恶意的输入时

  • 在后端可能造成服务器资源打满,拒绝服务的情况。
  • 在前端可能发生在表单验证、输入过滤或 URL 解析等场景中,导致浏览器卡顿甚至崩溃(不属于攻击,但影响用户体验)

原理

正则表达式引擎(如 JavaScript 的引擎)使用 回溯算法 进行匹配。当正则表达式编写不当时,某些输入会触发引擎的指数级回溯,导致匹配时间急剧增加。

攻击者可以通过特意构造的输入,触发正则表达式引擎的回溯机制,使其需要多次尝试不同的匹配路径,进而大幅增加匹配时间。

典型的ReDoS攻击会利用以下两种类型的正则表达式:

  1. 具有重复分支的正则表达式:例如 (a|aa)*,其中有多个分支可能匹配同一输入,会导致出现很多的排列组合。
  2. 贪婪的重复量词:例如 .*, a+ 等,它们会尝试匹配尽可能多的字符,导致正则引擎过度回溯。
  3. 嵌套量词(如 (a+)+)的多重回溯路径。

下面是一个验证的demo

js
// 定义一个低效的正则表达式
const regex = /(?:a+)+b/;

// 定义一个“恶意”输入,用于触发ReDoS
const input = "a".repeat(10); // 可以修改此处的字符串长度

// 测试正则表达式的执行时间
console.time("Regex Execution Time"); // 开始计时
try {
  const result = regex.exec(input); // 执行正则表达式匹配
  if (result) {
    console.log("Match found:", result[0]);
  } else {
    console.log("No match found");
  }
} catch (e) {
  console.error("Error occurred:", e);
}

console.timeEnd("Regex Execution Time"); // 结束计时

下面统计了不同input长度时,上面这段代码运行的时间

10 // 0.11083984375 ms
20 // 56.91796875ms
30 // 53948.12890625ms

可以看出当长度稍微增加,程序运行耗时会大大延长。

正则引擎会尝试大量的组合,寻找是否有可能的匹配。如果出现异常的边界输入(比如连续很多满足条件的字符)就会根据输入的内容出现指数级的回溯,使正则匹配时间大大增加。

由于js是单线程运行的,这段代码的长时间占用CPU,导致页面卡顿;如果这段代码运行在NodeJS中,就会导致服务器在这段时间内无法响应其他请求。

JavaScript 引擎(如 V8)已对部分回溯问题做了优化,例如通过回溯限制快速失败机制,但开发者仍需主动避免危险模式。

在业务中,很少有开发同学会考虑到关于正则的安全性问题。但实际上ReDOS攻击并不少见,比如axios的这个issue中,就提到了关于ReDOS的修复,这个还是最近两年(2023年)才修复的。

防御措施

前端中的常见风险场景

  • 表单验证:邮箱、密码复杂度等正则校验。
  • 输入过滤:过滤用户输入的特定字符(如特殊符号)。
  • 路由解析:单页应用(SPA)中客户端路由的正则匹配。
  • 第三方库依赖:引用的库中可能包含危险正则表达式。

只要是用到正则的地方,都可能出现ReDOS攻击。本身用正则来校验输入就是不相信用户输入,如果这里也出现了攻击漏洞,那就得不偿失了。

ReDoS攻击的核心是通过精心构造的输入让正则引擎陷入性能瓶颈,因此理解正则表达式的效率问题对于防止这类攻击非常关键。

由于回溯是大部分正则表达式引擎的基础工作机制,要修改正则引擎是一件很复杂(甚至不可能达到)的事情,因此,要避免ReDOS攻击,可以遵循下面建议

长度限制

在使用正则校验前,先使用一些通用的、低时间复杂度的判断规则,比如

  • 通过input.length来判断字符串的长度是否符合要求

  • 使用字符串方法(如 startsWithincludes)替代简单正则。

  • 复杂场景使用解析器(如 URL 解析库)

js
// 其他通用校验
if(input.lengt > 50) throw Error()
// 然后再正则校验
re.test(input)

同时还可以

  • 限制输入长度:例如用户名不超过 50 字符。
  • 清理用户输入:移除非常规字符后再匹配。

优化正则表达式

需要避免复杂的正则表达式,特别是那些包含大量可选项、重复或嵌套分支的模式。

  • 避免灾难性回溯:使用具体匹配代替模糊量词。
    • 错误示例:/(a+)+b/
    • 修正示例:/a+b/(明确结构,减少回溯)。
  • 使用原子组(部分引擎支持,如 Node.js 的 (?>))来禁止回溯。
  • 限制量词范围:如 {1,10} 代替 *+

还可以通过一些工具,来检测正则表达式是否存在潜在风险:

使用限时匹配

在一些编程语言中,可以设置正则匹配的超时时间(或者在单独的线程中执行正则匹配),避免正则匹配长时间占用CPU带来的风险(可惜JS貌似并不支持)

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。