[譯] 跨站請求偽造已死!

Xat_MassacrE發表於2017-03-13

跨站請求偽造已死!

在連續不斷的被跨站請求偽造折磨了這麼多年後,我們現在終於有了一個合理的解決方案。一個對網站擁有者沒有技術負擔、實施起來沒有難度、部署又非常簡單的方案,它就是 Same-Site Cookies。

和網際網路歷史一樣悠久的跨站請求偽造

跨站請求偽造(又被稱為 CSRF 或者 XSRF )似乎一直都存在著。它源自一個網站必須向另一個網站發出請求的簡單功能。比如像在頁面中嵌入下面的表單程式碼。

<form action="https://your-bank.com/transfer" method="POST" id="stealMoney">  
<input type="hidden" name="to" value="Scott Helme">  
<input type="hidden" name="account" value="14278935">  
<input type="hidden" name="amount" value="£1,000">複製程式碼

當你的瀏覽器載入這個頁面之後,上面的表單將會由一個簡單的 JS 片段來實現提交。

document.getElementById("stealMoney").submit();複製程式碼

這就是被稱作 CSRF 的來歷。我偽造了一個跨站到你的銀行網站的請求。這個問題的關鍵不是我傳送了請求,而是你的瀏覽器通過這個請求傳送了你的 cookies。此時,你當前擁有的全部驗證資訊也會通過這個請求傳送,這就意味著你登入你的銀行賬戶並且捐助了我 £1,000 。謝謝啊!那麼當你沒有登入的時候,這個請求對你就沒有什麼影響了,因為你不登入是無法轉賬的。不過對於銀行來說,他們現在採用的下面幾種辦法可以在一定程度上防禦 CSRF 攻擊。

緩解 CSRF

關於緩解 CSRF 這裡就不詳細展開講了,因為網上關於這個話題已經有大量的資訊了,但是我仍然會快速的過一遍順便展示一下實現他們都需要哪些技術。

檢查 origin

當我們收到一個請求時,關於這個請求的來源有兩個地方的資訊對我們來說是有用的。一個是 Origin header,另一個是 Referer header。你可以檢查他們中的一個或者兩個的值來判定對於你的網站來說他們是不是來自一個不同的域。如果這個請求是跨域的,那麼你把它丟掉就可以了。Origin 和 Referer header 會在瀏覽器端做一些保護措施來阻止被纂改,但是這並不總是有效的。

accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8  
accept-encoding: gzip, deflate, br  
cache-control: max-age=0  
content-length: 166  
content-type: application/x-www-form-urlencoded  
dnt: 1  
origin: https://report-uri.io  
referer: https://report-uri.io/login  
upgrade-insecure-requests: 1  
user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36複製程式碼
Anti-CSRF tokens

通常情況下你可以通過兩種方法來實現 Anti-CSRF tokens,但是它們的原理是一樣的。當一個遊客請求一個頁面時,類似於上面提到的轉賬頁面,你可以在表單中嵌入一個隨機的token。當真正的使用者提交表單的時,你就會收到表單的隨機 token,這樣你就可以通過之前嵌入的那個隨機 token 來校驗了。在 CSRF 攻擊場景中,攻擊者永遠都不可能拿到這個值甚至在攻擊者可以請求到頁面的情況也無法拿到,因為同源策略(SOP)會阻止攻擊者從包含 token 的響應中讀取內容。這個方法在實際運用中很不錯,但是它需要網站追蹤每一個請求並且返回 Anti-CSRF tokens。還有一個類似的在表單中嵌入 token 的方法是給瀏覽器一個包含相同值的 cookie 來實現的。當網站收到真正的使用者提交他們的表單時,cookie 中的值和表單中的值將會相匹配。攻擊者通過沒有 CSRF cookie 的瀏覽器傳送偽造的請求將會失敗。

<form action="https://report-uri.io/login/auth" method="POST">  
    <input type="hidden" name="csrf_token" value="d82c90fc4a14b01224gde6ddebc23bf0">
    <input type="email" id="email" name="email">
    <input type="password" id="password" name="password">
    <button type="submit" class="btn btn-primary">Login</button>
</form>複製程式碼

存在的問題

在很長一段時間,上面的這些方法在面對 CSRF 時給我們提供了強勁的保護。檢查 Origin 和 Referer header 並不是 100% 有效的,大部分網站也會通過一些高階的 Anti-CSRF token 方式來防禦。問題是,這兩種方法都需要網站有一些必要的條件才能實施和維護。雖然這些條件並不是世界上最複雜的技術,但是我們仍然需要建立一個解決辦法來讓瀏覽器做一些我們不想讓它做的事情。既然這樣的話,那麼我們為什麼不直接告訴瀏覽器不要做那些我們不想讓它們做的事情呢?現在,我們可以了!

Same-Site Cookies

你或許已經在我最近的部落格( Tough Cookies)上看到了一些關於 Same-Site Cookies 的內容,但是在這裡我將會用一些例子來深入的講解。從本質上來講,Same-Site Cookies 可以完全有效的阻止 CSRF 攻擊,是的,CSRF 一點機會都沒有。我們在網際網路上真正需求的本質就是贏得網路安全的戰爭,Same-Site Cookies 非常容易部署,是真的非常容易。找到你原來的 cookie :

Set-Cookie: sess=abc123; path=/複製程式碼

新增 SameSite 這個屬性。

Set-Cookie: sess=abc123; path=/; SameSite複製程式碼

你已經完成了。嚴格來講,就是這樣!在 cookie 上啟用這個屬性將會告訴瀏覽器給予這個 cookie 確切的保護。你可以通過 Strict 和 Lax 這兩種模式來啟用這個保護,具體用哪種模式取決於你想要的嚴格程度。如果在你的 cookie 設定中沒有指定模式的話預設將會使用 Strict 模式,但是如果你想的話你可以明確的指定是 Strict 還是 Lax。

SameSite=Strict  
SameSite=Lax複製程式碼
Strict

很顯然,將你的 SameSite 保護設定為 Strcit 模式是一個更好的選擇,但是我們之所以有兩個選項的原因是因為不是所有的網站都是一樣的並且不是所有的網站都有同樣的需求。當我們在 Strict 模式下操作時,瀏覽器在任何跨域請求中都不會攜帶 cookie,所以說 CSRF 一點機會都沒有。但是問題是,頂級導航(直接在位址列改變 URL )的請求都不會攜帶 cookie。比如說有一個連結地址 facebook.com 並且 Facebook 的 SameSite cookies 的模式為 Strict,當你點選連結開啟 Facebook 之後你會發現你無法登入。無論你之前是否登入,在新標籤中開啟,無論你怎麼做,當你從那個連結過來時你都無法登入到 Facebook。這就很煩人了,並且我們的使用者也不希望我們提供如此蛋疼的保護。這時候 Facebook 要做的就是向 Amazon 學習,使用兩個 cookie。一種是用來驗證使用者資訊和登入操作的 '基礎的' cookie,當你想進行一些類似於支付,改變賬戶資訊的敏感操作時就需要第二種 cookie 了,'真正的' cookie 就可以允許你進行一些重要的操作。在這個案例中第一種 cookie 就是一種 '方便的' 不會設定 SameSite 的 cookie,它真的不回允許你進行任何敏感性的操作,即使攻擊者通過它來進行跨站請求,什麼都不會發生。第二種 cookie 是一種設定了 SameSite 屬性的 '敏感的' cookie,攻擊者在跨站請求中不會獲取它的許可權。這對於使用者和安全來說就是一種理想的解決方案。然和這種方式的實施性並不強,因為我們希望 SameSite cookies 可以簡單的部署,那麼我們就需要第二個選項了。

Lax

將 SameSite 保護設定為 Lax 模式將會解決上面提到的在 Strict 模式下的使用者在已經登入的前提下點選連結仍然無法在目標網站登入的問題。在 Lax 模式下有一個例外,就是在頂級導航中使用一個安全的 HTTP 方法傳送的請求可以攜帶 cookie。所謂 "安全的" 的 HTTP 方法在 Section 4.2.1 of RFC 7321 定義為 GET、HEAD、OPTIONS 和 TRACE,在這裡我們只關心 GET 方法,就是我們連結到 facebook.com 的頂級導航就是一個 GET 方法。現在當使用者點選一個設定了 SameSite 的連結之後,瀏覽器就會傳送攜帶 cookie 和一些我們希望的使用者資訊的請求。同時,我們也防範了基於 POST 方法的 CSRF 攻擊。在 Lax 模式下,最開始提到的例子中的攻擊手段也無法成功。

<form action="https://your-bank.com/transfer" method="POST" id="stealMoney">  
<input type="hidden" name="to" value="Scott Helme">  
<input type="hidden" name="account" value="14278935">  
<input type="hidden" name="amount" value="£1,000">複製程式碼

因為 POST 方法被認為是一種不安全的方法,瀏覽器在請求中是不會攜帶 cookie 的。那麼攻擊者當然會想到使用一種 '安全的' 方法來完成同樣的請求。

<form action="https://your-bank.com/transfer" method="GET" id="stealMoney">  
<input type="hidden" name="to" value="Scott Helme">  
<input type="hidden" name="account" value="14278935">  
<input type="hidden" name="amount" value="£1,000">複製程式碼

其實只要我們在接收 POST 請求的地方不接受 GET 請求那麼這種攻擊方法就會失效,但是在 Lax 模式下還有一些需要注意的點。比如,如果一個攻擊者觸發一個頂級導航或者彈出一個新的視窗,那麼他們就可以讓瀏覽器傳送一個攜帶 cookies 的 GET 請求。這就是在 Lax 模式下需要取捨的地方,我們在保證完整的使用者體驗的前提下不得不承擔一些小的風險。

額外的用途

這篇部落格的目標是通過 SameSite Cookies 來緩解 CSRF 攻擊,但是,你可能已經猜到了,這種機制還有一些其他的用途。第一個就是跨站指令碼包含(XSSI),它是指當瀏覽器向類似於指令碼的資原始檔傳送請求的時候將會根據使用者是否登入而做出改變。在跨站請求的場景中,一個攻擊者無法使用 SameSite Cookie 的一些驗證資訊來造成不同的響應。這裡還有一些有趣的定時攻擊的詳細資訊。

還有一個有趣的用途(不是很詳細)是用來對抗在面對渾水猛獸般的攻擊手段下 (CRIME), BREACH), HEIST, TIME) 造成的會話 cookie 的洩露。這些確實是很高階的攻擊手段,但是基礎的場景是一個 MiTM (中間人攻擊) 可以通過任何他們喜歡或監視的機制來強行讓瀏覽器傳送跨域請求。通過使用請求載荷的大小的變化,攻擊者可以變更瀏覽器請求並觀察每次變更之後的大小就可以猜出一位 session ID 的值。而使用 SameSite Cookies 的話,瀏覽器在傳送這些請求的時候就不會攜帶 cookies,那麼攻擊者業就無法猜到他們的值了。

瀏覽器支援情況

和很多新的瀏覽器安全特性一樣,我們總是希望 Firefox 和 Chrome 能夠引領這些新特性,但是這次情況不一樣了。Chrome 自從 v51 就開始支援 SameSite Cookie 了,這意味著 Opera,安卓瀏覽器和安卓上的 Chrome 都支援這一特性。你可以在 caniuse.com 上看到當前所有支援該屬性的詳細資訊,Firefox 還有一個開放的 bug 需要新增支援。雖然目前來看支援並不是很全面,但是我們應該給我們的 cookies 新增 SameSite 這個屬性。支援這一特性的瀏覽器將會按照協議為我們的 cookie 提供額外的保護,而不支援的瀏覽器會直接無視它。這不但對我們沒什麼影響,還會提供一種不錯的具有深度的防禦手段。雖然離我們完全移除傳統的反 CSRF 機制還有很長的一段時間,但是新增 SameSite 仍然可以為我們提供一個足夠健壯的保護。

相關文章