当我在 Heroku 管理安全团队时,我经常做一个噩梦:
我的 PagerDuty 警报响了,提醒我发生了安全事故。在梦中,我盯着手机并意识到“不,大事不好”——接着,我就被惊醒了。
我仍然不确定梦中的安全事故到底是什么,但它很可能是 DoS 攻击。虽然 DoS 攻击简单,但是它造成的影响却可能是毁灭性的:攻击者以超过服务器负载的方式向你的应用程序发送流量。这虽然不像远程代码执行或数据泄露那样糟糕,但还是相当可怕。如果客户不能使用你的应用程序,你就会失去他们的信任,损失金钱。
通常,我们讨论两种类型的 DoS 攻击:
从事 Web 应用程序工作的工程师经常会遇到可用于 DoS/DDoS 攻击的漏洞。
不幸的是,业界对于如何处理这些漏洞存在广泛分歧。这种风险很难分析:我曾见过开发团队为如何处理一个 DoS 问题争论了好几个星期。
本文将试图理清这些分歧,为工程和应用程序安全团队提供了一个框架来考虑 DoS 风险,将 DoS 漏洞分为高、中、低三级,并在每一级提出了缓解措施的建议。这篇文章的主要关注点是大局,应该适用于任何类型的 Web 应用程序。但为了具体化,我加入了一些 Django 相关的具体示例。(我创建了这个框架,因此我对它非常熟悉。)
在应用层评估 DoS 漏洞的风险可能很难。安全专家间存在着广泛分歧:你经常会看到 2 个不同的应用程序安全团队对相似问题的处理方式是截然不同的。
有人认为:要完全抵御集中式 DDoS 攻击,这几乎是不可能的——一个足够专注的攻击者可以向你投放比你的应用程序能处理的更多的带宽。如果没有上游网络提供商(例如 Cloudflare)提供用来防护机器人程序攻击的特定工具,你永远无法完全缓解 DDoS 攻击。
因此,追踪和修复假设的 DoS 漏洞似乎是在浪费开发人员的时间。这些团队将大部分潜在的 DoS 问题视为可接受的风险,并将精力集中在准备网络级别的缓解措施上。
另外一些团队指出,传统风险模型有三个潜在问题:机密性、完整性和可用性。我们都知道,正常运行时间是一个安全问题。越来越普遍的情况是,攻击者关闭服务,然后要求赎金来停止攻击。最近针对Garmin 的攻击是一个非常明显的例子;攻击者几乎关闭了Garmin 的所有服务,据报道要求100 万美元的赎金。(在这个例子中,攻击是勒索软件,但很容易看出DoS 攻击也会有类似的效果)。因此,DoS 漏洞和其它任何漏洞一样都是风险,它们都应该被缓解。
重要的是,这两个立场都是合理的!将DoS 视为超出应用程序安全范围是合理的;同样,将其纳入范围也是合理的。我经常看到安全团队在这两个立场间争论不休。如果确定不了对错,就不可能找出解决的办法。
对于这个争论,我采用的是攻击者杠杆理念。杠杆会放大力量:在杠杆长的一端施加很小的力量,就会在短的一端被放大数倍。具体到DoS 攻击,如果一个漏洞有高杠杆率,这意味着攻击者只需要很少的资源就能消耗你的大量服务器资源。
例如,如果你的Web 应用程序的一个bug 允许单个 GET请求消耗 100% 的 CPU,那么这就是一个非常高的杠杆率。只需要少量攻击,你的 Web 服务器就会陷入瘫痪。另一方面,一个低杠杆率的漏洞需要花费攻击者的大量资源,最后才会让可用性降低一点点。如果一个攻击者必须花费数千美元才能让一台服务器瘫痪,那么你能比他们更快地进行扩展。
杠杆率越高,风险越高,我就越有可能直接解决这个问题。杠杆率越低,我就越有可能接受这个风险或者依赖网络级别的缓解措施。
当然,具体问题需要具体分析。根据杠杆率,我将 DoS 风险分为高、中、低三个风险级别。对每个风险级别,我将分析如何识别漏洞属于哪个级别,讨论一些示例,并给出一些缓解建议。
典型的高风险 DoS 漏洞是那些攻击者本身只需要很少资源就能造成资源匮乏的漏洞。这可能意味着耗尽任何类型的资源,包括:
在所有这些情况中,共同因素是应用程序的一个 bug 会导致显著的放大效应。
当考虑资源放大 DoS 问题的风险时,一个重要因素是触发该漏洞所需的身份认证级别。
如果一个完全匿名的用户就能轻易触发一个资源匮乏攻击,那么攻击者就很容易利用这个漏洞让你崩溃。无需身份认证的 DoS 问题应该被视为高风险。
另一方面,如果只有经过你公司单点登录服务器验证过的用户才能触发该漏洞,那么,这就是一个非常低的风险。大部分攻击者不是内部人员(尽管有些是!)。而且,如果攻击者出现,很容易确定和阻止。
在大多数情况下,“我们可以确定并阻止攻击”是一种合理的,尽管不完备的缓解策略。大多数漏洞介于这两个极端之间:大多数服务让创建新账户非常简单(例如,你只需要一个邮箱地址)。这确实赋予了一些能力来确定和阻止漏洞,但这往往是不够的。
一般来说,我建议将这类 DoS 漏洞——特别是无需身份验证的漏洞——视为高风险,并且予以消除。如果它被利用,这些漏洞就是灾难性的;它们能让单个攻击者就击溃你的应用程序。我会投入跟其它高风险安全漏洞(例如 XSS 和 CSRF)一样的精力来发现并消除这种 bug。
最后一种资源匮乏的常见例子(并发量限制)是正则表达式拒绝服务(regular expression denial-of-service),又叫 ReDoS。当特定类型的字符串会导致不恰当构建的正则表达式表现非常差时,ReDoS bug 就会发生。
不幸的是,这种漏洞在 Python 中很常见;内置的正则表达式模块 (re) 没有针对这种漏洞的内在保护(不像 re2 库,Go 内置的 regex 模块,因此让语言或多或少对这种攻击免疫)。(Django 本身多年来也存在一些这种漏洞;例如, CVE-2019-14232 和 CVE-2019-14233 都是 ReDoS 漏洞。)在 Django,这些漏洞通常出现在两个地方:基于正则表达式的 URL 解析和自定义验证器,以及应用程序使用正则表达式的其它地方。幸运的是,这种类型的漏洞很容易找到。请参阅以下 r2c 文章:
如果你用 Python,你可以在应用程序中使用 Semgrep 扫描 ReDoS,这个库从 Dlint 移植了 ReDoS 检测功能。检测需要一些使用 Semgrep 强大的 pattern-where-python 子句编写的额外逻辑,这些子句让规则能充分利用 Python 的全部功能,因此你必须使用--dangerously-allow-arbitrary-code-execution-from-rules 标志。
复制代码
$ semgrep --config https://semgrep.dev/r/contrib.dlint.redos --dangerously-allow-arbitrary-code-execution-from-rules
稍微深入研究风险案例,我们发现资源匮乏的一个不同类型:你的应用程序本身就比较慢或者资源比较紧张。例如:
如果一个攻击者发现一个比正常速度慢得多的区域,就可以向该端点发送垃圾信息,造成与上述类似的资源耗尽。但是,这些通常并不是 bug;它们只是应用程序的特性。有些特性总是比较慢或占用资源比较多;对某些事情很少有“修复”方法,只是需要一些时间。有时,性能优化可以降低风险,但是那通常需要大量的投资或不可接受的权衡(例如放弃一致性写入)。
然而,还是有一些缓解因素可以降低这类问题的风险:
综上所述,我认为这意味着将这种类别的潜在漏洞视为可接受的风险是更为合理的。“我们将屏蔽试图让我们崩溃的 API key”似乎是一个合理决定。
换言之,有一个常见架构上的缓解措施值得考虑:速率限制。速率限制对某个特定端点在一段时间窗口内设置了请求数量的阈值。速率限制很容易搭建和应用,通常是一个简单的正向工程实践。只要你设置的限制足够高,不妨碍正常使用,它们就可以防止一系列问题,包括 DoS。
在 Django 中, django-ratelimit 提供了一个简单的基于装饰器的 API,使得为视图增加速率限制非常容易:
复制代码
from ratelimit.decorators import ratelimit @ratelimit(key='user’, rate=’10/s’)def my_view(request): …
或者,如果你在使用 Django REST 框架,它可以通过一系列配置实现内置的速率限制。对一些应用程序来说,广泛应用速率限制是很好的办法——甚至可以在每个视图上应用速率限制。在那些例子中,你可以用Semgrep 来发现并警告未被装饰的视图。
下面是一个例子,Semgrep 配置可以发现没有被 @ratelimit装饰器装饰的视图:
复制代码
rules:- id: my_pattern_id patterns: - pattern-either: - pattern: | def $FUNC(..., request, ...): ... - pattern-not: | @ratelimit.decorators.ratelimit(...) def $FUNC(..., request, ...): ... message: | This view Appears not to have a rate limit applied. Consider applying one with the @ratelimit decorator. fix: | severity: WARNING
你可能想针对你的具体应用程序修改规则集;这只是个起点。迭代开发一个定制规则集的一个好方法是从这个规则集开始在 Semgrep 的交互实验室进行迭代。
最后,我们讨论最后一种 DoS 攻击:真正的 DDoS 攻击,即一个攻击者指挥大量计算机向你的应用程序发送大量流量。这些流量通常不是针对具体应用程序的;它通常是一些没有意义的 TCP 或 UDP 包,设计用来使网络本身崩溃。
一次 DDoS 攻击的规模通常只受限于攻击者的预算。这种类型的攻击通常会使应用程序安全工程师举手投降——包括我自己!实在没有什么措施可以用来缓解这种漏洞。在应用程序级别肯定是没有方法的。我比较同意,真正的 DDoS 攻击是超出了应用程序安全范畴的。
那就是说,你可以在网络层级做一些事情,主要是在准备方面:
如果你想了解更多关于准备和缓解 DDoS 攻击的内容,谷歌的 Building Secure and Reliable Systems 第 10 章是个不错的起点。
DoS 漏洞有各种各样。其中,有一些应该被提高优先级并立即修复,但另外一些被视为“可接受的风险”也算合理。毕竟,没有一种方案可以适用于所有漏洞;你需要在找到合适的响应前考虑漏洞的相对风险。
我发现评估风险的最佳框架是 amplification:考虑到需要多少攻击流量来触发某个等级的服务降级。如果几个零散的请求就能让你的服务器崩溃,那这是一个非常高的风险,应该被妥善处理。另一方面,如果非常多的流量只能导致适度的速度降低,那你将这个问题的优先级排在其它问题之后也是合理的。
下次,你面对 DoS 问题的不确定时,可以试试这个框架。我希望它能避免那些令人沮丧的争吵!
原文链接:
https://r2c.dev/blog/2020/understanding-and-preventing-dos-in-web-apps/