Wiping Out CSRF
現在是2017年了,關於跨站請求偽造(CSRF)已經沒有多少可以說的了。這是一個被熟知了多年的漏洞,在現在流行的 web 框架上也得到了一定的解決。那麼為什麼我們還在談論呢?有幾個原因:
- 傳統的應用程序缺乏 CSRF 保護機制;
- 一些框架的構建不夠完善;
- 應用程序沒有很好的利用具有保護機制的框架;
- 新的應用程序不使用提供 CSRF 保護機制的現代框架。
CSRF 仍然是 Web 應用程序中普遍漏洞。 這篇文章將深入地分析 CSRF 的工作原理以及當下的一些防禦對策。隨後我們將提供一種解決對策,可以運用在應用程序寫完之後,不需要修改源代碼。最後,我們將檢測 cookie 的一種新擴展,如果成為一種標準,這可能就是大多CSRF 案例的一個終結了。代碼附在這裡,其中包含測試用例的具體實現。GitHub存儲庫
理解攻擊
在最根本的層面上,CSRF 是一個漏洞,攻擊者強制受害者代表攻擊者發出 HTTP 請求。這是一種完全發生在客戶端的攻擊(例如 Web 瀏覽器),其中接收方相信受害者正在發送可信任的應用程序信息。
有三個組件能夠發生攻擊:不正確的使用危險的HTTP請求方式、Web瀏覽器對cookie的處理和跨站腳本攻擊(XSS)。
HTTP 規範標準將請求方式分為安全的和不安全的兩種。安全的請求方式(GET,HEAD 和 OPTIONS)旨在用於只讀操作。使用它們的請求旨在返回有關所請求的資源的信息,並且不會對伺服器產生任何副作用。不安全請求方式(POST,PUT,PATCH 和 DELETE)用於修改,創建或刪除資源。
不幸的是,HTTP 請求的的方式可能被忽略,或者是請求的意圖沒有被嚴格的限制。 不正確的請求方式使用的主要原因是由於 HTTP 規範的瀏覽器支持率歷來較差。直到 XML HTTP 請求(XHR)的流行,除了 GET 或 POST 之外,根本不可能使用不依賴於特定框架和庫黑客的請求方式。這種限制導致區分 HTTP 請求方式變的實際無關緊要。僅僅這樣做不足以創造 CSRF 的條件,但它有助於並使其保護更加困難。
導致 CSRF 漏洞的最大因素是瀏覽器處理 Cookie 。 HTTP 最初被設計為無狀態協議,具有對應於單個響應的單個請求,並且在請求之間不攜帶狀態。為了支持複雜的Web 應用程序,創建 Cookie 作為在相關 HTTP 請求之間保持狀態的解決方案。
Cookie 駐留在瀏覽器的全局級別,並在實例,窗口和標籤之間共享。用戶依賴於網頁瀏覽器,可以自動傳送每個請求的 Cookie 。由於 cookie 可以在瀏覽器中訪問/修改,並且沒有防篡改保護,所以狀態的存儲已經轉移到伺服器管理的 session 中。
在該模型中,在伺服器上生成唯一的標識符並將其存儲在 cookie 中。每個瀏覽器請求都會發送 cookie ,並且伺服器能夠查找該標識符,以確定它是否是一個有效的會話。會話結束時,伺服器就不記得這個標識符了,之前發送的請求也將會失效。
問題在於瀏覽器是如何管理 cookie 的。一個 cookie 由幾個屬性組成,但我們關心的最重要的一個是 Domain 屬性。Domain 屬性的預期功能是將 Cookie 定位到與 cookie的 domain 屬性相匹配的特定主機。這被設計為一種安全機制,以避免敏感信息(如會話標識符)被攻擊者可能執行會話固定攻擊的惡意網站被盜。這裡的缺點是 domain 屬性不依賴於同源策略 (SOP ),它只是將 Cookie 的 domain 的值與請求中的主機進行比較。
這允許源自不同來源的請求也攜帶該主機的任何 cookie 。當且僅當安全和不安全的請求方式被正確使用時,這是安全的行為; 示例:安全請求(GET)不應該改變狀態,但是我們已經看到正確的使用不一定值得信任。如果您不了解 SOP,那麼您應該閱讀更多信息。
最重要的組件是跨站腳本攻擊(XSS)。XSS 是受害者在 DOM 中呈現攻擊者控制JavaScript 或 HTML 的能力。如果 XSS 存在於應用程序中,那麼在阻止 CSRF 攻擊時它也就結束了。如果 XSS 是有效的,我們將在本文中討論的主要對策,大多數應用程序依賴,可以被繞過的。
進行攻擊
現在我們知道這些因素,讓我們深入了解 CSRF 的工作原理。如果您尚未設置,現在將是遵循代碼庫中的說明並獲取示例運行的好時機。README 中提供了基本設置的說明,以及入門指南。 我們將介紹三種傳統的不同類型的 CSRF:
1.資源包含
2.基於表單
3.XMLHttpRequest
資源包含是在大多數介紹 CSRF 概念的演示或基礎課程中可能看到的類型。這種類型歸結為控制 HTML 標籤(例如<image>、< audio>、<video>、<object>、<script>等)所包含的資源的攻擊者。如果攻擊者能夠影響 URL 被載入的話,包含遠程資源的任何標籤都可以完成攻擊。
由於缺少對 Cookie的源點檢查,如上所述,此攻擊不需要 XSS,可以由任何攻擊者控制的站點或站點本身執行。此類型僅限於 GET 請求,因為這些是瀏覽器對資源 URL 唯一的請求類型。這種類型的主要限制是它需要錯誤地使用安全的 HTTP 請求方式。
我們將討論的第二種類型是基於表單的 CSRF,通常在正確使用安全的請求方式時看到。攻擊者創建自己的表單,模仿他們想要受害者提交的表單; 它包含一個 JavaScript 片段,強制受害者的瀏覽器提交表單。該表單可以完全由隱藏的元素組成,並且表單應該迅速地提交,以致受害者不能發現它。由於處理 cookies,攻擊者可以在任何站點上發動攻擊,只要受害者使用有效的 cookie 登錄,攻擊就會成功。如果請求是有目的性的,成功的攻擊將使受害者回到他們平時正常的頁面。該方法對於攻擊者可以將受害者指向特定頁面的網路釣魚攻擊特別有效。
我們將討論的最後一個主要類型是 XMLHttpRequest(XHR)。由於需求的需要,這可能是最不可能看到的。由於許多現代 Web 應用程序依賴 XHR,我們將花費大量的時間來構建和實現這一特定的對策。基於 XHR 的 CSRF 通常由於 SOP 而以 XSS 有效載荷的形式出現。沒有跨域資源共享策略(CORS),XHR 僅限於攻擊者託管自己的有效載荷的原始請求。這種類型的 CSRF 的攻擊有效載荷基本上是一個標準的 XHR,攻擊者已經找到了一些注入受害者瀏覽器 DOM 的方式。
以下解決方案是可以用於實際部署的極大簡化。它主要側重於客戶端和 token 管理。對請求和響應的攔截和修改有許多奇怪的邊緣情況,需要大量關於本身運行平台的知識。理解平台的複雜性有很大的權衡,以避免理解和處理 CSRF 本身的複雜性。理想情況下,最好的解決方案是使用一個框架來提供內置和利用 CSRF 保護的框架。儘管有免責聲明,但仍然存在許多理由,如下所述的解決方案是有道理的。
現代防禦
有許多例子都證明不可能修改應用程序來實現 CSRF 的防護。一方面源代碼無法得到,另一方面修改應用程序的風險太高,或者由於應用程序的限制不容易完成。該解決方案特別適合部署在 RASP、WAF、反向代理或負載平衡器中,並且可用於為單個應用程序提供保護,或者使用相同配置的所有應用程序。當部署平台被理解得很好但是應用程序不被適用時,這是特別有用的。我們來討論通常用於防範 CSRF 的現有解決方案,以及如何根據上述要求構建它們。
首先,正確使用安全和不安全的 HTTP 請求方式很重要。這一點不是一個有效的解決方案,但它會使一切變得更加容易,接下來的兩種方法取決於它。不幸的是,沒有一個可以在事實之後應用的解決方案。這是在構建應用程序時需要做的,需要設計和架構。幸運的是,大多數現代Web 框架都有一個路由器的概念,它強制要求有個與 HTTP 請求方式配對的終端。在現代框架中,對與終端不匹配的請求會導致錯誤。如果這是您的應用程序無法實現的,我們稍後將討論解決方法。
下一個保護是驗證請求的來源。該對策旨在確保進入應用程序的請求源自應用程序內的(或具有 CORS 的其他可信來源)。正確的請求方式很重要,因為只要我們假設只有狀態改變的請求是不安全的,那麼我們只需要驗證不安全請求的來源。由於我們上面討論的問題,驗證安全請求的來源是有問題的。如果需要,那麼一個解決方案是創建一個已知安全網址的排除列表,例如用戶首次訪問時將要訪問的主頁或可能的著陸頁。這將防止外部來源的 CSRF,但允許用戶到達網站的期望行為,並在首次訪問時保持登錄狀態。
這種保護並不是絕對必要的,但它增加了額外的層次,並且可能是您要使用 CORS 的一種解決方案。由於應用程序中的 SOP 和 token 的分配,CORS 使 token 的實現變得格外困難。源驗證還取決於 HTTP 頭的存在,但由於瀏覽器差異,瀏覽器擴展或某些請求條件而可能不存在的 HTTP 頭。如果請求頭缺少,則默認選項應始終是打開失敗,並依賴不同層的解決方案來減輕 CSRF 。
該保護通過將 Origin 或 Referer 頭與請求中的 Host 頭進行比較來起作用。Origin 標頭僅在某些情況下使用,例如 XHR,並且可能不存在於所有請求中。它由完整的主機組成,包括埠。Referer 頭顯得更為常見,是發出請求時瀏覽器地址欄的完整URL。最後,Host 頭是瀏覽器通知主機的伺服器,包括埠(如果不是80),希望與之通信。這需要在單個應用伺服器上支持虛擬主機或多個站點。在這種情況下,我們使用 Host 頭作為比較的真實來源,因為我們知道 Host 頭將對應於我們要強制進行原始檢查的主機。首先檢查 Origin頭,然後檢 Referer。這個順序並不重要,甚至可以交換。需要一些基本的解析,並且確保只比較主機和埠,這一點非常重要。
您可能會想知道的是,鑒於 Referer 欺騙的可能性和易用性,比較 referer 是否可信。有兩個部分使這無關緊要。第一個是 Referer 欺騙的唯一方法是直接來自受害者。如前所述,這完全是客戶端攻擊,所以受害者的瀏覽器必須有意地偽造 Referer 來繞過檢查。這是不太可能故意發生的事情。第二個因素是這些 Headers,Origin 和 Referer 不能被 JavaScript 設置,因為它們受到保護,並且如果攻擊者的 XSS 有效負載嘗試設置它們將導致錯誤。這也限制了任何對受害者瀏覽器上這些頭的修改,假設用戶永遠不會故意去自己攻擊自己、瀏覽器也正常工作,這或許是安全的。
第三個也是最常用的對策是 token。token 有幾個不同的種類,但是每種實現最終都使用同步token。要更完整的了解,您應該閱讀「雙重提交」 tokens 和「加密」 tokens。儘管此處討論的解決方案較簡單,但雙重提交令牌應可用於以下解決方案,而加密令牌通常由於 AES 或其他選擇的加密方案的成本而不太有效。相反,我們將使用同步器和加密的混合,提供最佳的兩種解決方案。
同步器 tokens 通過使用唯一的 token 讓伺服器和瀏覽器同步工作。對一個安全方法的請求伺服器會返回一個 token,瀏覽器會隨著每個不安全的請求一起返回給伺服器,通常在表單正文或請求頭,這具體取決於請求的類型。在允許請求繼續之前,伺服器驗證該 token 是真實的和有效的; 伺服器還將提供一個新的 token,以便令牌不會持續重複使用或打開來重複攻擊。由於 SOP,這將阻止攻擊者控制的主機上的 CSRF 有效載荷。攻擊者將無法得到 token 並將其插入到請求中,因為這樣做將要求攻擊者能夠強制受害者向遠程站點請求並返迴響應 - SOP 恰恰就是被設計來阻止這個的。攻擊者唯一可以利用的就是應用程序里的xss跨站腳本。
token 由四部分組成,必須保持完整性才能有效。任何一個的損失將顯著削弱 token 的保護。這四個部分是隨機數,用戶標識符,期限和真實性驗證:
1.隨機數的關鍵空間大小並不是太重要,只要它足夠大以確保缺少重複。
2.用戶標識符可以是用戶唯一的任何值。在我們的實現中,我們將選擇使用會話標識符。
3.壽命或到期時間是 token 有效的長度。理想情況下,您希望時間足夠短,以至於被盜後不能長時間使用,但長度足以使真實用戶使用它時不會過期,從而導致失敗的請求。在大多數框架實現中,通常將 token 保存在 session 中並且隨著 session 的超期而失效。這樣做只有一個值在任何一個時間都有效,在我們的情況下不是這樣,所以需要離散的到期。在我們的例子中,我們會默認一個小時。
4.一定有辦法保證 token 是真實的,並沒有被篡改或偽造。在框架內實現的解決方案通常可以通過將該值存儲在用戶永遠無法訪問的伺服器端會話存儲中來。在我們的例子中,我們將依靠HMAC-SHA256,並提供一個可以驗證的 token 的簽名。這具體是另一個原因,因為攻擊者還需要獲取 HMAC 的密鑰以偽造令牌。如果密鑰被破壞,整個 token 和密鑰空間是不相關的在這種情況下,隨機性只是為令牌值提供一些額外的熵,以最小化被盜或泄漏令牌的有用性。這也是我們如何避免大多數框架依賴於會話存儲的需求,同時獲得比大多數加密令牌解決方案有更好的性能。
token的實現有兩個方面,伺服器端處理token的生成/驗證以及客戶端,客戶端將token發送到伺服器以獲取需要的請求。除了提供生成和驗證的示例之外,我們不會深入到伺服器端實現中,正如之前在聲明中所說,處理攔截請求/響應的具體細節因平台而異。只需說一下,深入特定平台的中間件API,並使用它來實現接近以下步驟的操作。
1.當前session是否有token?如果沒有,請標記應生成token。
2.請求是否需要驗證?如果是,驗證並標記該token已被使用。
3.如果需要驗證並失敗,那麼短路響應並停止處理。如果驗證成功,則處理請求。
4.如果token不存在或被標記為已使用,則生成新token並將添加其cookie到響應中。
值得注意的是,每次生成一個新的token,即使沒有驗證,也不會增加任何安全性或者打開一個新的攻擊向量。如果更容易,您可以在每個請求上生成一個新的令牌來構建您的實現。您將不會獲得額外的保護,但是性能損失應該可以忽略不計。token被添加到cookie中,作為一種為瀏覽器提供值的方式,javascript 可以訪問到它,瀏覽器也會自動的保存它。確保 HttpOnly標誌永遠不會用於此 cookie 這很重要。這樣做會打破實施,但是沒有任何安全問題,因為唯一的威脅來自 XSS,它提供了必要的條件來繞過 CSRF 的保護。
String generateToken(int userId, int key) { byte[16] data = random() expires = time() + 3600 raw = hex(data) + "-" + userId + "-" + expires signature = hmac(sha256, raw, key) return raw + "-" + signature }
以上是創建新 token 的簡單示例。只是被連字元連接起來的四個部分。HMAC 將前三部分用於加密,以確保每個人的真實性,加密後的結果作為第四部分。選擇連字元作為分隔符,因為冒號不是 Cookie 版本0 Cookie 的有效字元。使用它必須要升級到版本1,這可能會破壞與舊瀏覽器的一些兼容性。
bool validateToken(token, user) { parts = token.split("-") str = parts[0] + "-" + parts[1] + "-" + parts[2] generated = hmac(sha256, str, key) if !constantCompare(generated, parts[3]) { return false } if parts[2] < time() { return false } if parts[1] != user { return false} return true }
上面的代碼塊是一個驗證 token 並計算有效性的簡單示例。token 被分為四個部分,第一步是通過前三個部分重新生成 HMAC 並將其與期望的 HMAC 進行比較來驗證 HMAC 。確保在這裡使用一個恆定的時間來避免引入任何時序攻擊。如果成功,我們驗證token是否過期,用戶是否匹配。從根本上來說這就是生成和檢驗 token 的流程。真正的威脅是用戶的瀏覽器自動提交請求時也帶上了 token。
大多數現代框架在構建應用程序時都會為您考慮到了這一點。他們有庫函數來處理 XHR,將token 插入到請求和模板助手中,以便將當前 token 包含在表單中。這是我們要模仿的功能:不依賴於框架為我們提供。相反,作為我們回應攔截的一部分,我們將在響應中添加或附加一小段 JavaScript。儘管嚴格測試絕對不符合規範,但幾乎每個瀏覽器都將正確處理腳本標籤,JavaScript 中分別添加或附加到打開或關閉 HTML 標籤。我們將專門針對 HTML contentType的響應,以確保我們只將注入到我們不會中斷的響應中,我們只修改非 XHR 的響應。這將避免我們將腳本多次載入到瀏覽器或 JSON 響應中。
實現這一點有兩個部分,一個是處理表單提交,另一個是處理 XHR 。第一個代碼段是附加到onclick 事件的文檔回調最小化版本。將它附加到文檔而不是嘗試附加到單個表單或可點擊元素很重要,因為在附加時,表單或元素很可能不存在於 DOM 中,導致回調未觸發。相反,我們附加到始終存在的文檔,並委託給我們關心的元素。我們還需要使用 onclick 而不是onsubmit,因為 onsubmit 在所有瀏覽器和版本中都不會浮動,這意味著我們無法附加到文檔並被調用。
var target = evt.target; while (target !== null) { if (target.nodeName === A || target.nodeName === INPUT || target.nodeName === BUTTON) { break; } target = target.parentNode; } // We didnt find any of the delegates, bail out if (target === null) { return; }
第一節抓取被觸發事件的目標元素。這是用戶點擊的元素。由於 DOM 的樹結構和事件冒泡系統,這個元素可能不是我們感興趣的元素,而是我們必須走出 DOM 尋找可以提交表單的元素; 在這種情況下,比如:<a>、<input> 或 <button> 標籤。如果我們在發現一個 DOM 之前到達 DOM 的頂端,那麼就輕鬆了,因為它是一個沒有提交表單的元素上的點擊事件。
// If its an input element make sure its of type submit var type = target.getAttribute(type); if (target.nodeName === INPUT && (type === null || !type.match(/^submit$/i))) { return; } // Walk up the DOM to find the form var form; for (var node = target; node !== null; node = node.parentNode) { if (node.nodeName === FORM) { form = node; break; } } if (form === undefined) { return; }
接下來我們檢查標籤是否是 <input> 。如果是,那麼我們要確保它是一個提交按鈕。否則它不會提交表單 , 而只是使瀏覽器關注元素。一旦我們確定目標導致提交事件的發生,那麼繼續從DOM 中尋找一個表單標籤。如果我們到達 DOM 的頂端,但沒有找到一個表單標籤,那麼該元素不會被提交,除非它使用 XHR,這將被 XHR 相關代碼部分處理。
var token = form.querySelector(input[name="csrf_token"]); var tokenValue = getCookieValue(CSRF-TOKEN); if (token !== undefined && token !== null) { if (token.value !== tokenValue) { token.value = tokenValue; } return; } var newToken = document.createElement(input); newToken.setAttribute(type, hidden); newToken.setAttribute(name, csrf_token); newToken.setAttribute(value, tokenValue); form.appendChild(newToken);
一旦找到表單,剩下的唯一步驟就是把這個 token 添加到 form 中作為一個隱藏的輸入元素。第一步是從先前的提交中檢查元素是否已經存在。如果是,請檢查該值,並在必要時進行更改。如果沒有,則創建一個新元素並將其附加到表單中。由於冒泡的作用方式,此處理程序在提交表單之前觸發,並在處理程序返回之前將元素添加到表單中,導致瀏覽器提交的請求帶有表單中的新元素,然後將token添加到正文的請求。 對於非基於表單的請求,需要一種將token存入 XHR 請求的方法。大多數庫都提供抽象方法,包括 jQuery,這使得這更容易,因為它們提供了可以修改請求的回調函數,允許不同的請求。不幸的是,我們不能假設一個特定的庫將會出現,並且需要為標準的 XHR API 創建我們自己的 hook。為了做到這一點,我們將包裝和修補對象本身以添加額外的功能。
XMLHttpRequest.prototype._send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(){ if(!this.isRequestHeaderSet(X-Requested-With)){ this.setRequestHeader(X-Requested-With,XMLHttpRequest); } var tokenValue = getCookieValue(CSRF-TOKEN); if(tokenValue!== null){ this.setRequestHeader(X-CSRF-Header,tokenValue);} this._send.apply(this,arguments); };
通過利用 JavaScript 的原型繼承和動態性質,我們將原始發送方法的副本保存到對象上,以便我們可以保留對其的引用以供稍後使用。然後,我們創建一個附加到發送原型的新函數,該原型從 cookie 中提取 token,並向請求中添加一個帶有值的 header 。真正的發送方法通過保存的 referer 來調用,原始參數通過它們按預期工作。
就瀏覽器中的代碼而言,認為 API 沒有改變,XHR 對象也並沒有不同,但是我們現在強制所有請求都在伺服器可以讀取的 header 中提交一個 CSRF token。 這個實現的一個特別的注意事項是,由於原型支持和 XHR 可用性,它只能用於 Internet Explorer(IE)8。XHR 被引入 Internet Explorer 7,但是不存在正確的原型支持,需要額外的解決方案來完善此功能。至少 IE6 可以使用基於表單的解決方案。
可能有一種方法可以通過自定義的 ActiveX 控制項為舊版本的IE版本添加額外的支持,但不在本文的範圍之內。處理舊版本瀏覽器缺乏支持的另一個解決方案是簡單地不執行 CSRF 檢查,而是檢查用戶代理頭。
儘管如此,這些可能並不總是存在並且可能被偽造,這樣做將需要受害者主動地忽略安全性問題或已經被惡意軟體或 XSS 攻擊而泄密。所有其他瀏覽器似乎都有很好的支持,如下圖所示。上面的代碼可能會被比以下版本更老的瀏覽器支持,但是,查找測試副本很難,這些版本的瀏覽器也覆蓋了大多數人常用的。
關於未來
現在我們已經根據當前的實踐構建了涵蓋一個適用於舊版本瀏覽器到現代瀏覽器的解決方案,現在是時候來看看一個新的解決方案,這可能是大多數CSRF案例的完結了。
這是一種擴展名為 Same-Site 的 Cookie 的擴展形式,它增加了對 Cookie 源的檢查。Same-Site 允許瀏覽器限制只發送來自與域匹配的主機的請求發送的 cookie,大大地取代了對同步器令牌的需求。有兩種形式,strict 和 lax。strict 會檢查所有安全和不安全的請求,而 lax 只支持檢查不安全的請求。大多數應用程序將需要配置為 lax,因為保護安全請求就不允許將會話 cookie 與原始 GET 請求一起發送到站點。
在撰寫本文時,瀏覽器支持非常之少,主要是 Chrome 支持該功能。下表列出了從這裡得到的信息。但是,作為擴展程序,Same-Site不會破壞不支持舊瀏覽器對 Cookie 的兼容性。較老的瀏覽器將會自動忽略此項 功能。
在撰寫本文時,Same-Site 還只是以草案存在,我不知道有任何可以支持這種功能的 Cookie庫。只有這種方式變得更穩定和被多數人接受,才可能像token一樣地使用。它可以與同步器token 結合使用,以支持較舊和較舊的瀏覽器。
Same-Site 的一個缺點是缺乏 CORS 支持。在撰寫本文時,沒有提及添加對白名單特定來源的支持,以安全地發送 cookies。這將會破壞依賴於向伺服器提供狀態信息的 Cookie 的 CORS 請求。一個潛在的解決方法是刪除僅用於不使用 Same-Site 並執行源驗證的外部站點的第二個 cookie
參考:https://medium.com/@jrozner/wiping-out-csrf-ded97ae7e83f
推薦閱讀:
※存儲型XSS與反射型XSS有什麼區別?
※XSS introduction
※仿:Pentester中的XSS詳解
※記一次沒什麼技術含量的XSS注入
※XSS 攻擊有哪些黑魔法?