在 JavaScript 中使用正则表达式的隐患

9/7/2021 JavaScript

正则表达式(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 的原理,以及规避这些问题的方法。

我希望这篇文章能帮助你保护你的应用程序免受此类攻击。别忘了在留言区分享你的看法。

感谢您的阅读!