正则ReDOS攻击
对用户的输入进行校验,是一个系统稳健性最基础的要求,不只前端要对用户提交的输入进行校验,后端也会对输入进行再次校验。
要校验输入是否符合特定要求,正则表达式是比较常用的手段。这里就有一个潜在的安全问题:ReDoS(Regular Expression Denial of Service) 正则表达式拒绝服务攻击。
本文将介绍这种不是很常见的Web安全问题。
ReDoS
是一种通过构造恶意输入,使正则表达式匹配过程陷入极端低效状态,从而耗尽系统资源(如 CPU 时间)的安全攻击。
简单来说,就是错误的正则遇到恶意的输入时
- 在后端可能造成服务器资源打满,拒绝服务的情况。
- 在前端可能发生在表单验证、输入过滤或 URL 解析等场景中,导致浏览器卡顿甚至崩溃(不属于攻击,但影响用户体验)
原理
正则表达式引擎(如 JavaScript 的引擎)使用 回溯算法 进行匹配。当正则表达式编写不当时,某些输入会触发引擎的指数级回溯,导致匹配时间急剧增加。
攻击者可以通过特意构造的输入,触发正则表达式引擎的回溯机制,使其需要多次尝试不同的匹配路径,进而大幅增加匹配时间。
典型的ReDoS攻击会利用以下两种类型的正则表达式:
- 具有重复分支的正则表达式:例如
(a|aa)*
,其中有多个分支可能匹配同一输入,会导致出现很多的排列组合。 - 贪婪的重复量词:例如
.*
,a+
等,它们会尝试匹配尽可能多的字符,导致正则引擎过度回溯。 - 嵌套量词(如
(a+)+
)的多重回溯路径。
下面是一个验证的demo
// 定义一个低效的正则表达式
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
来判断字符串的长度是否符合要求使用字符串方法(如
startsWith
、includes
)替代简单正则。复杂场景使用解析器(如 URL 解析库)
// 其他通用校验
if(input.lengt > 50) throw Error()
// 然后再正则校验
re.test(input)
同时还可以
- 限制输入长度:例如用户名不超过 50 字符。
- 清理用户输入:移除非常规字符后再匹配。
优化正则表达式
需要避免复杂的正则表达式,特别是那些包含大量可选项、重复或嵌套分支的模式。
- 避免灾难性回溯:使用具体匹配代替模糊量词。
- 错误示例:
/(a+)+b/
- 修正示例:
/a+b/
(明确结构,减少回溯)。
- 错误示例:
- 使用原子组(部分引擎支持,如 Node.js 的
(?>)
)来禁止回溯。 - 限制量词范围:如
{1,10}
代替*
或+
。
还可以通过一些工具,来检测正则表达式是否存在潜在风险:
- regexploit:检测危险正则模式。
- safe-regex:评估正则复杂度。
使用限时匹配
在一些编程语言中,可以设置正则匹配的超时时间(或者在单独的线程中执行正则匹配),避免正则匹配长时间占用CPU带来的风险(可惜JS貌似并不支持)
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。
