当我在 Heroku 管理安全团队时,我经常做一个噩梦:
我的 PagerDuty 警报响了,提醒我发生了安全事故。在梦中,我盯着手机并意识到“不,大事不好”——接着,我就被惊醒了。
我仍然不确定梦中的安全事故到底是什么,但它很可能是 DoS 攻击。虽然 DoS 攻击简单,但是它造成的影响却可能是毁灭性的:攻击者以超过服务器负载的方式向你的应用程序发送流量。这虽然不像远程代码执行或数据泄露那样糟糕,但还是相当可怕。如果客户不能使用你的应用程序,你就会失去他们的信任,损失金钱。
通常,我们讨论两种类型的 DoS 攻击:
“一般的”DoS 攻击,即单台机器就足以导致宕机。这种攻击的一个经典老派的版本是zip炸弹:攻击者诱使你的服务器打开一个精心编制的 ZIP 文件,这个文件被压缩得很小,但是解压后,它却会把你的整个磁盘空间给塞满。
DDoS(分布式拒绝服务)攻击。这种攻击需要攻击者从多台机器向你的站点发送超大流量。通常,这些攻击来自僵尸网络——被攻击者控制的大量机器。这些僵尸网络可以从互联网的特定角落购买,让任何一个有信用卡的人都能进行一次不错的 DDoS 攻击。
从事 Web 应用程序工作的工程师经常会遇到可用于 DoS/DDoS 攻击的漏洞。
不幸的是,业界对于如何处理这些漏洞存在广泛分歧。这种风险很难分析:我曾见过开发团队为如何处理一个 DoS 问题争论了好几个星期。
本文将试图理清这些分歧,为工程和应用程序安全团队提供了一个框架来考虑 DoS 风险,将 DoS 漏洞分为高、中、低三级,并在每一级提出了缓解措施的建议。这篇文章的主要关注点是大局,应该适用于任何类型的 Web 应用程序。但为了具体化,我加入了一些 Django 相关的具体示例。(我创建了这个框架,因此我对它非常熟悉。)
评估 DoS 风险
在应用层评估 DoS 漏洞的风险可能很难。安全专家间存在着广泛分歧:你经常会看到 2 个不同的应用程序安全团队对相似问题的处理方式是截然不同的。
有人认为:要完全抵御集中式 DDoS 攻击,这几乎是不可能的——一个足够专注的攻击者可以向你投放比你的应用程序能处理的更多的带宽。如果没有上游网络提供商(例如 Cloudflare)提供用来防护机器人程序攻击的特定工具,你永远无法完全缓解 DDoS 攻击。
因此,追踪和修复假设的 DoS 漏洞似乎是在浪费开发人员的时间。这些团队将大部分潜在的 DoS 问题视为可接受的风险,并将精力集中在准备网络级别的缓解措施上。
另外一些团队指出,传统风险模型有三个潜在问题:机密性、完整性和可用性。我们都知道,正常运行时间是一个安全问题。越来越普遍的情况是,攻击者关闭服务,然后要求赎金来停止攻击。最近针对Garmin的攻击是一个非常明显的例子;攻击者几乎关闭了 Garmin 的所有服务,据报道要求 100 万美元的赎金。(在这个例子中,攻击是勒索软件,但很容易看出 DoS 攻击也会有类似的效果)。因此,DoS 漏洞和其它任何漏洞一样都是风险,它们都应该被缓解。
重要的是,这两个立场都是合理的!将 DoS 视为超出应用程序安全范围是合理的;同样,将其纳入范围也是合理的。我经常看到安全团队在这两个立场间争论不休。如果确定不了对错,就不可能找出解决的办法。
攻击者杠杆理论
对于这个争论,我采用的是攻击者杠杆理念。杠杆会放大力量:在杠杆长的一端施加很小的力量,就会在短的一端被放大数倍。具体到 DoS 攻击,如果一个漏洞有高杠杆率,这意味着攻击者只需要很少的资源就能消耗你的大量服务器资源。
例如,如果你的 Web 应用程序的一个 bug 允许单个GET
请求消耗 100%的 CPU,那么这就是一个非常高的杠杆率。只需要少量攻击,你的 Web 服务器就会陷入瘫痪。另一方面,一个低杠杆率的漏洞需要花费攻击者的大量资源,最后才会让可用性降低一点点。如果一个攻击者必须花费数千美元才能让一台服务器瘫痪,那么你能比他们更快地进行扩展。
杠杆率越高,风险越高,我就越有可能直接解决这个问题。杠杆率越低,我就越有可能接受这个风险或者依赖网络级别的缓解措施。
当然,具体问题需要具体分析。根据杠杆率,我将 DoS 风险分为高、中、低三个风险级别。对每个风险级别,我将分析如何识别漏洞属于哪个级别,讨论一些示例,并给出一些缓解建议。
高杠杆率 DoS 漏洞:容易放大的资源匮乏
典型的高风险 DoS 漏洞是那些攻击者本身只需要很少资源就能造成资源匮乏的漏洞。这可能意味着耗尽任何类型的资源,包括:
磁盘空间——例如,一个漏洞放大上传数据并塞满磁盘,比如典型的zip炸弹。
网络带宽——例如,一个漏洞放大输入流量,而单个输入请求会消耗大量带宽,导致网络堵塞。我在微服务系统中看到过这样的 bug,单个输入请求触发大量的内部 API 请求(包括在网络上传送相当大的文件),并阻塞了内部网络带宽。
CPU 占用——例如,一个漏洞触发了一个accidentally quadratic算法,导致 Web 服务陷入瘫痪。
并发限制——大部分服务器都有一个最大并发量限制(例如最大线程数或最大进程数,或数据库最大连接数);一个漏洞导致进程运行非常慢(甚至永不退出),则会导致服务器达到这些限制并开始拒绝请求。
在所有这些情况中,共同因素是应用程序的一个 bug 会导致显著的放大效应。
身份认证影响风险
当考虑资源放大 DoS 问题的风险时,一个重要因素是触发该漏洞所需的身份认证级别。
如果一个完全匿名的用户就能轻易触发一个资源匮乏攻击,那么攻击者就很容易利用这个漏洞让你崩溃。无需身份认证的 DoS 问题应该被视为高风险。
另一方面,如果只有经过你公司单点登录服务器验证过的用户才能触发该漏洞,那么,这就是一个非常低的风险。大部分攻击者不是内部人员(尽管有些是!)。而且,如果攻击者出现,很容易确定和阻止。
在大多数情况下,“我们可以确定并阻止攻击”是一种合理的,尽管不完备的缓解策略。大多数漏洞介于这两个极端之间:大多数服务让创建新账户非常简单(例如,你只需要一个邮箱地址)。这确实赋予了一些能力来确定和阻止漏洞,但这往往是不够的。
缓解建议:消除
一般来说,我建议将这类 DoS 漏洞——特别是无需身份验证的漏洞——视为高风险,并且予以消除。如果它被利用,这些漏洞就是灾难性的;它们能让单个攻击者就击溃你的应用程序。我会投入跟其它高风险安全漏洞(例如 XSS 和 CSRF)一样的精力来发现并消除这种 bug。
一个高杠杆率漏洞示例:ReDoS
最后一种资源匮乏的常见例子(并发量限制)是正则表达式拒绝服务(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
标志。
中杠杆率 DoS 风险:复杂的资源匮乏
稍微深入研究风险案例,我们发现资源匮乏的一个不同类型:你的应用程序本身就比较慢或者资源比较紧张。例如:
复杂的报告,需要读取或计算大量数据。考虑一下,在一个很长的时间内对聚集指标的实时报告,或者一份汇总数百万笔交易的季度财务报告。
数据库或搜索引擎写入,这需要高价的重新索引。典型的 Web 应用程序是以写入速度慢为代价来实现读取速度快的设计的。这对于分布式数据库的一致性写入尤其如此(CAP定理!)
类似 GraphQL 的 API,能生成任意深度的数据库表连接。这是一个超出这里深度的话题;如果想了解更多,请参阅 Apollo 团队的Securing Your GraphQL API from Malicious Queries。
如果一个攻击者发现一个比正常速度慢得多的区域,就可以向该端点发送垃圾信息,造成与上述类似的资源耗尽。但是,这些通常并不是 bug;它们只是应用程序的特性。有些特性总是比较慢或占用资源比较多;对某些事情很少有“修复”方法,只是需要一些时间。有时,性能优化可以降低风险,但是那通常需要大量的投资或不可接受的权衡(例如放弃一致性写入)。
然而,还是有一些缓解因素可以降低这类问题的风险:
这种端点通常都在一些身份验证或登录验证后。例如,GraphQL API 需要一个 API key;财务报告只开放给有特殊权限的用户;数据库写入只会被已经登录的用户触发。这能降低上述风险。
这种特性通常比高杠杆率等级的风险需要更多的攻击流量来击垮。例如,虽然在典型的应用程序中写入比读取慢,但是也不是那么慢;一个调优得比较好的数据库能处理每秒上千次写入。因此,攻击者必须更费力地投入更多资源来造成资源匮乏。
综上所述,我认为这意味着将这种类别的潜在漏洞视为可接受的风险是更为合理的。“我们将屏蔽试图让我们崩溃的 API key”似乎是一个合理决定。
缓解建议措施:速率限制
换言之,有一个常见架构上的缓解措施值得考虑:速率限制。速率限制对某个特定端点在一段时间窗口内设置了请求数量的阈值。速率限制很容易搭建和应用,通常是一个简单的正向工程实践。只要你设置的限制足够高,不妨碍正常使用,它们就可以防止一系列问题,包括 DoS。
在 Django 中,django-ratelimit提供了一个简单的基于装饰器的 API,使得为视图增加速率限制非常容易:
或者,如果你在使用 Django REST 框架,它可以通过一系列配置实现内置的速率限制。对一些应用程序来说,广泛应用速率限制是很好的办法——甚至可以在每个视图上应用速率限制。在那些例子中,你可以用 Semgrep 来发现并警告未被装饰的视图。
下面是一个例子,Semgrep 配置可以发现没有被@ratelimit
装饰器装饰的视图:
你可能想针对你的具体应用程序修改规则集;这只是个起点。迭代开发一个定制规则集的一个好方法是从这个规则集开始在Semgrep的交互实验室进行迭代。
低杠杆率 DoS 风险:DDoS
最后,我们讨论最后一种 DoS 攻击:真正的 DDoS 攻击,即一个攻击者指挥大量计算机向你的应用程序发送大量流量。这些流量通常不是针对具体应用程序的;它通常是一些没有意义的 TCP 或 UDP 包,设计用来使网络本身崩溃。
一次 DDoS 攻击的规模通常只受限于攻击者的预算。这种类型的攻击通常会使应用程序安全工程师举手投降——包括我自己!实在没有什么措施可以用来缓解这种漏洞。在应用程序级别肯定是没有方法的。我比较同意,真正的 DDoS 攻击是超出了应用程序安全范畴的。
缓解建议:充分准备和网络级别的安全措施
那就是说,你可以在网络层级做一些事情,主要是在准备方面:
你应该考虑将应用程序放在可以防护DDoS的Cloudflare之类的服务后面。你还能从 CloudFlare 之类的 CDN 获得大量性能好处,因此,这通常是值得的。
你应该理解网络层级以及网络规则可以应用到哪个层级。许多 DDoS 攻击是可以被识别的(通过 IP、源端口、流量类型或某种组合)。知道如何应用网络规则来阻止恶意流量有助于你快速对攻击做出响应。
除了自己控制的系统外,你应该了解你的网络供应商是谁以及它们可以应用哪些缓解措施。通常,你的网络供应商能比你更有效地阻拦这些攻击。例如,如果你在 AWS 上托管主机,作为AWS Shield Advanced的一部分,你可以得到 AWS DDoS 响应团队 24x7 的支持。这个服务的起始价格是 36000 美元每年,这看起来可能贵的离奇或者超级便宜,一切取决于你的业务。
如果你想了解更多关于准备和缓解 DDoS 攻击的内容,谷歌的Building Secure and Reliable Systems第 10 章是个不错的起点。
结论
DoS 漏洞有各种各样。其中,有一些应该被提高优先级并立即修复,但另外一些被视为“可接受的风险”也算合理。毕竟,没有一种方案可以适用于所有漏洞;你需要在找到合适的响应前考虑漏洞的相对风险。
我发现评估风险的最佳框架是 amplification:考虑到需要多少攻击流量来触发某个等级的服务降级。如果几个零散的请求就能让你的服务器崩溃,那这是一个非常高的风险,应该被妥善处理。另一方面,如果非常多的流量只能导致适度的速度降低,那你将这个问题的优先级排在其它问题之后也是合理的。
下次,你面对 DoS 问题的不确定时,可以试试这个框架。我希望它能避免那些令人沮丧的争吵!
原文链接:
https://r2c.dev/blog/2020/understanding-and-preventing-dos-in-web-apps/
评论 2 条评论