正则表达式(RegEx)被广泛地运用于 Web 开发中,用作模式匹配及验证等用途。然而,在实际使用中它们会带来一些安全和性能上的风险,并向攻击者敞开大门。
因此,在这篇文章中,我将讨论使用正则表达式前所需注意的两个基本问题。
# 灾难性回溯
正则表达式的算法有两种:
- 确定性有限状态自动机(DFA) —— 对于给定字符串,每个字符只检查一次。
- 非确定性有限状态自动机(NFA) —— 多次检查同一个字符,直到找到最佳匹配。
JavaScript 的 RegEx 引擎使用的是 NFA 算法,这会导致灾难性回溯。
为了更好地理解这个问题,让我们考虑以下的 RegEx:
/(g|i+)+t/;
这个 RegEx 看起来很简单。但是,请别低估它让你付出的代价 😯。首先,让我们了解这个 RegEx 背后的含义:
(g|i+)
—— 这个组检查给定字符串是否由g
或至少一个i
开头。- 接下来的
+
将匹配前面的组一次或多次。 - 字符串应由字母
t
结尾。
根据上方的 RegEx,以下的文本被判定为匹配:
git
giit
gggt
gigiggt
igggt
现在,让我们以一个匹配的字符串作为输入,测试上方的 RegEx。我将使用 console.time()
方法:
我们可以看到执行速度非常快,即使字符串有点长。
但是,当你看到验证不匹配的文本所花费的时间时,你会感到惊讶。
在下方的示例中,字符串以 v
结尾,因此与 RegEx 不匹配。然而,它花了大约 429 毫秒,差不多是验证匹配字符串的运行时间的 400 倍。
这个性能上的差异来源于 JavaScript 所使用的 NFA 算法。
在第一次验证成功后,JavaScript 的 RegEx 引擎仍会尝试继续。当它在特定位置失败时,它将回溯到上一个位置并寻找替代路径。
当回溯变得太复杂时,算法就会消耗更多计算能力,造成灾难性回溯。
备注:欲了解回溯的复杂度,你可以访问 regex101.com (opens new window) 并测试你的 RegEx。regex101.com (opens new window) 显示使用上述 RegEx 验证
giiiit
只需要 10 个步骤,而验证giiiiv
则需要 189 个步骤。
# Node.js 环境上的 ReDoS 攻击
攻击者能利用灾难性回溯来攻击 Node.js 服务器。
由于 JavaScript 是单线程的,ReDoS 攻击能耗尽事件循环,造成服务器无响应,直到请求完成为止。
我将使用 Moment.js 库来演示这一点,因为在低于 2.15.2 的 Moment.js 的版本中存在一个著名的 ReDoS 漏洞。
var moment = require("moment");
moment.locale("be");
moment().format("D MMN MMMM");
在这个示例中,日期格式有 40 个字符,其中包括 31 个附加空格。由于灾难性回溯,这些空格将使运行时间增加一倍。在我的本地环境中,它耗时超过 4 分钟。
/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/
中 +
运算符的过度使用造成了这个漏洞。幸运的是,该问题由 Snyk (opens new window)(一个漏洞追踪工具)提出后便在更高的版本中得到了修复。
# 如何规避 RegEx 的漏洞
# 1. 编写简单的 RegEx
当 RegEx 中包含至少 3 个字符,且包含至少两个彼此接近的 *
、+
和 }
时,灾难性回溯就会发生。
所以,如果你能简化你的 RegEx 并避免使用以上的样式,那么你便能避免灾难性回溯。
# 2. 使用验证库
对于常用的验证任务,我们可以使用第三方库,例如 validator.js (opens new window) 或 express-validator (opens new window)。
我们可以依赖这些库,因为它们的背后有一个大型社区的支持。
# 3. 使用 RegEx 分析器
你能通过使用 safe-regex (opens new window)、rxxr2 (opens new window) 等工具来编写无漏洞的 RegEx。它们将检查你的 RegEx 是否存在漏洞并返回其合法性。
var safe = require("safe-regex");
var regex = /(g|i+)+t/;
console.log(safe(regex)); // false
这将被判定为 false
,因为这个正则表达式容易受到灾难性回溯的影响。
# 4. 避免使用 Node.js 默认的 RegEx 引擎
由于 Node.js 默认的 RegEx 引擎容易受到 ReDoS 攻击,我们可以避免使用它,并以其他引擎作为替代,例如:Google 的 re2 (opens new window) 引擎。它确保 RegEx 可以安全地抵御 ReDoS 攻击,用法也与 Node.js 默认的 RegEx 引擎相似。
var RE2 = require("re2");
var re = new RE2(/(g|i+)+t/);
var result = "giiiiiiiiiiiiiiiiiiit".search(re);
console.log(result); // 0
# 主要收获
灾难性回溯是正则表达式中最常见的问题。它不仅影响应用程序的性能,也向 ReDoS 攻击者敞开大门,导致 Node.js 服务器被攻击。
在这篇文章中,我们讨论了灾难性回溯和 ReDoS 的原理,以及规避这些问题的方法。
我希望这篇文章能帮助你保护你的应用程序免受此类攻击。别忘了在留言区分享你的看法。
感谢您的阅读!
- 原文地址:Threats of Using Regular Expressions in JavaScript (opens new window)
- 原文作者:Dulanka Karunasena (opens new window)
- 译文出自:掘金翻译计划 (opens new window)
- 本文永久链接:https://github.com/xitu/gold-miner/blob/master/article/2021/threats-of-using-regular-expressions-in-javascript.md (opens new window)
- 译者:jaredliw (opens new window)
- 校对者:KimYangOfCat (opens new window)、greycodee (opens new window)