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

avatar

正则表达式(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 并测试你的 RegEx。regex101.com 显示使用上述 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(一个漏洞追踪工具)提出后便在更高的版本中得到了修复。

如何规避 RegEx 的漏洞

1. 编写简单的 RegEx

当 RegEx 中包含至少 3 个字符,且包含至少两个彼此接近的 *+} 时,灾难性回溯就会发生。

所以,如果你能简化你的 RegEx 并避免使用以上的样式,那么你便能避免灾难性回溯。

2. 使用验证库

对于常用的验证任务,我们可以使用第三方库,例如 validator.jsexpress-validator

我们可以依赖这些库,因为它们的背后有一个大型社区的支持。

3. 使用 RegEx 分析器

你能通过使用 safe-regexrxxr2 等工具来编写无漏洞的 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 引擎。它确保 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 的原理,以及规避这些问题的方法。

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

感谢您的阅读!

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏