Categories
程式開發

如何阻止對Web應用程序發起的DoS攻擊?


當我在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佔用——例如,一個漏洞觸發了一個意外二次算法,導致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 標誌。

$ semgrep --config https://semgrep.dev/r/contrib.dlint.redos 
          --dangerously-allow-arbitrary-code-execution-from-rules

中槓桿率DoS風險:複雜的資源匱乏

稍微深入研究風險案例,我們發現資源匱乏的一個不同類型:你的應用程序本身就比較慢或者資源比較緊張。例如:

  • 複雜的報告,需要讀取或計算大量數據。考慮一下,在一個很長的時間內對聚集指標的實時報告,或者一份匯總數百萬筆交易的季度財務報告。
  • 數據庫或搜索引擎寫入,這需要高價的重新索引。典型的Web應用程序是以寫入速度慢為代價來實現讀取速度快的設計的。這對於分佈式數據庫的一致性寫入尤其如此(CAP定理!)
  • 類似GraphQL的API,能生成任意深度的數據庫表連接。這是一個超出這裡深度的話題;如果想了解更多,請參閱Apollo團隊的保護GraphQL API免受惡意查詢

如果一個攻擊者發現一個比正常速度慢得多的區域,就可以向該端點發送垃圾信息,造成與上述類似的資源耗盡。但是,這些通常並不是bug;它們只是應用程序的特性。有些特性總是比較慢或占用資源比較多;對某些事情很少有“修復”方法,只是需要一些時間。有時,性能優化可以降低風險,但是那通常需要大量的投資或不可接受的權衡(例如放棄一致性寫入)。

然而,還是有一些緩解因素可以降低這類問題的風險:

  • 這種端點通常都在一些身份驗證或登錄驗證後。例如,GraphQL API需要一個API key;財務報告只開放給有特殊權限的用戶;數據庫寫入只會被已經登錄的用戶觸發。這能降低上述風險。
  • 這種特性通常比高槓桿率等級的風險需要更多的攻擊流量來擊垮。例如,雖然在典型的應用程序中寫入比讀取慢,但是也不是那麼慢;一個調優得比較好的數據庫能處理每秒上千次寫入。因此,攻擊者必須更費力地投入更多資源來造成資源匱乏。

綜上所述,我認為這意味著將這種類別的潛在漏洞視為可接受的風險是更為合理的。 “我們將屏蔽試圖讓我們崩潰的API key”似乎是一個合理決定。

緩解建議措施:速率限制

換言之,有一個常見架構上的緩解措施值得考慮:速率限制。速率限制對某個特定端點在一段時間窗口內設置了請求數量的閾值。速率限制很容易搭建和應用,通常是一個簡單的正向工程實踐。只要你設置的限制足夠高,不妨礙正常使用,它們就可以防止一系列問題,包括DoS。

在Django中,Django速率限制提供了一個簡單的基於裝飾器的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

最後,我們討論最後一種DoS攻擊:真正的DDoS攻擊,即一個攻擊者指揮大量計算機向你的應用程序發送大量流量。這些流量通常不是針對具體應用程序的;它通常是一些沒有意義的TCP或UDP包,設計用來使網絡本身崩潰。

一次DDoS攻擊的規模通常只受限於攻擊者的預算。這種類型的攻擊通常會使應用程序安全工程師舉手投降——包括我自己!實在沒有什麼措施可以用來緩解這種漏洞。在應用程序級別肯定是沒有方法的。我比較同意,真正的DDoS攻擊是超出了應用程序安全範疇的。

緩解建議:充分準備和網絡級別的安全措施

那就是說,你可以在網絡層級做一些事情,主要是在準備方面:

  • 你應該考慮將應用程序放在可以防護DDoS雲耀斑之類的服務後面。你還能從CloudFlare之類的CDN獲得大量性能好處,因此,這通常是值得的。
  • 你應該理解網絡層級以及網絡規則可以應用到哪個層級。許多DDoS攻擊是可以被識別的(通過IP、源端口、流量類型或某種組合)。知道如何應用網絡規則來阻止惡意流量有助於你快速對攻擊做出響應。
  • 除了自己控制的系統外,你應該了解你的網絡供應商是誰以及它們可以應用哪些緩解措施。通常,你的網絡供應商能比你更有效地阻攔這些攻擊。例如,如果你在AWS上託管主機,作為AWS Shield高級的一部分,你可以得到AWS DDoS響應團隊24×7的支持。這個服務的起始價格是36000美元每年,這看起來可能貴的離奇或者超級便宜,一切取決於你的業務。

如果你想了解更多關於準備和緩解DDoS攻擊的內容,谷歌的構建安全可靠的系統第10章是個不錯的起點。

結論

DoS漏洞有各種各樣。其中,有一些應該被提高優先級並立即修復,但另外一些被視為“可接受的風險”也算合理。畢竟,沒有一種方案可以適用於所有漏洞;你需要在找到合適的響應前考慮漏洞的相對風險。

我發現評估風險的最佳框架是amplification:考慮到需要多少攻擊流量來觸發某個等級的服務降級。如果幾個零散的請求就能讓你的服務器崩潰,那這是一個非常高的風險,應該被妥善處理。另一方面,如果非常多的流量只能導致適度的速度降低,那你將這個問題的優先級排在其它問題之後也是合理的。

下次,你面對DoS問題的不確定時,可以試試這個框架。我希望它能避免那些令人沮喪的爭吵!

原文鏈接:

https://r2c.dev/blog/2020/understanding-and-preventing-dos-in-web-apps/