前言
各位要懂得保護自己。今日前端早讀課文章由騰訊@camdyzeng分享。
@camdyzeng:騰訊CSIG前端開發高級工程師。團隊主要致力於前端相關技術的研究和在騰訊業務的應用,團隊內部每周有內部分享會,有興趣的讀者可以加入我們或者參與一起討論。郵箱:camdyzeng@qq.com。
正文從這開始~~
關於 CSRF 你想知道的都在這裡了。
CSRF(Cross-site request forgery)簡稱:跨站請求偽造,學習 CSRF 攻擊原理和防護方法是我們團隊新成員的必修課,通常我都是先讓新同學自己研究自己講,然後我再通過其中細節再給他們講一遍,講的次數多了,也慢慢總結出一種比較容易理解的講法。這裡我整理成文分享給有需要的人。
引言:必修課為什麼選擇 CSRF?
CSRF 涉及到的前端知識點比較多,全面理解需要系統的學習 Cookie,前端跨域,HTTP 協議,web 瀏覽器等知識。
理解 CSRF 攻擊和防護方法是前端進階要去,我也希望大家都能夠掌握。
個人興趣,我個人對前端性能優化和前端安全比較感興趣,也算參與和推動了公司內的 CSRF 防護方案從無到有的過程。
一、CSRF 攻擊原理1、Cookie 的使用
HTTP 是無狀態協議,服務器只能根據當前請求的參數(包括 Head 和 Body 的數據)來判斷本次請求需要達到的目的(Get 或者 Post 都一樣),服務器並不知道這個請求之前幹了什麼事,所以這就是無狀態協議。
但是我們現實很多情況需要有狀態,比如登錄態的身份信息,網頁上很多操作需要登錄之後才能操作,比如相冊的上傳照片和刪除照片等功能都需要登錄之後才能操作。那麼怎麼判斷登錄態?(通常我會問如果你遇到這個問題你回怎麼做?),其實解決方案都是類似的,只能是每次 HTTP 請求都要把登錄態信息(這裡用 Key 表示)傳給後台服務器,後台通過 Key 字段是判斷用戶合法性之後再處理這個請求要處理的敏感操作。
怎麼方便的讓每次 HTTP 請求都帶上 Key?所以就設計出了 Cookie,這裡列舉 Cookie 主要的一些特性。
瀏覽器默認自動攜帶本次 HTTP 請求域名的 Cookie(不管是通過什麼方式,在什麼頁面發送的 HTTP 請求)。
讀寫 Cookie 有跨域限制(作用域,Domain,Path)。
生命周期(會話 or 持久)。
2、CSRF 的攻擊過程
根據上面介紹,登錄態 Cookie 的 Key 是瀏覽器默認自動攜帶的,Key 通常是會話 Cookie,只要瀏覽器不關閉,Key 一直存在。所以只要用戶 A 曾經登錄過相冊網站(這裡用 http://www.photo.com 舉例),瀏覽器沒有關閉,用戶在沒有關閉的瀏覽器打開一個黑客網頁(這裡用 http://www.hacker.com),黑客頁面發送 HTTP 請求到 http://www.photo.com 的後台會默認帶上 http://www.photo.com 的登錄態 Cookie,也就能模擬用戶 A 做一些增刪改等敏感操作。Get 和 Post 都一樣,這就是 CSRF 攻擊原理。這種攻擊過程也是最常見的攻擊過程,後面還會介紹另外一種少見的攻擊過程。
3、讀操作能否被攻擊到?
上面說的增刪改都是寫操作,會對後台數據產生負面影響,所以是能被攻擊的。另外一種讀操作,是具有冪等性,不會對後台數據殘生負面影響,能否被攻擊到?讀操作也可能是敏感數據,舉個例子,比如www.photo.com上的私密相冊數據能否被www.hacker.com頁面拿到?這就涉及到前端跨域知識點了,默認大部分情況是拿不到,這裡列舉兩種特殊情是可以拿到的:
如果後台返回的數據是 JSONP 格式的,這種只能是 Get 操作,是能被黑客頁面拿到的。
如果後台是通過 CORS 處理跨域,沒有對請求頭的 Origin 做白名單限制,ACAO 響應頭是*或者包括黑客頁面,包括 Get/Post/Del 等操作,也是能被黑客頁面拿到的。
除了這兩種特殊情況,讀操作都是不能被攻擊到的,因為瀏覽器跨域限制是天然的安全的。
二、CSRF 防護方法
知道攻擊原理,防護方法也很簡單,找到能夠區分請求發送的頁面是自己的頁面還是黑客的頁面的方法就可以了。
1、Referrer
HTTP 請求頭 Referrer 字段是瀏覽器默認帶上,含義是發送請求的頁面地址,比如同樣是刪除相冊的操作http://www.photo.com/del?id=xxx;如果是從相冊自己頁面發送出來,Referrre的值是http://www.photo.com/index.html(以首頁舉例),如果是從黑客頁面發送出來的Referrer的值是http://www.hacker.com/index.html(以首頁舉例),所以後端只要通過Referrrer做白名單判斷就能防這種常見的CSRF攻擊。下面探討幾個容易被忽悠的問題。
1.1 Referrer 會不會被偽造或者篡改?
在瀏覽器環境下,Referrer 是瀏覽器自己帶上的,js 是改不了 Rerferrer,所以是不能被偽造和篡改的。
瀏覽器插件能改 Referrer 麼?答案:能改,但是瀏覽器插件攻擊不屬於 CSRF 攻擊範疇,如果用戶瀏覽器都已經被安裝了黑客插件了就有更方便的攻擊方法,但是不可能在所有用戶瀏覽器都安裝上黑客的插件。
通過網關或者抓包修改 Referrer?答案:能改,這是中間人攻擊,也不屬於 CSRF 攻擊範疇。防中間人攻擊用 HTTPS。
黑客通過自己後台代理,請求發到黑客自己的後台,黑客後台修改 Referrer 再轉發到相冊後台,可以改麼?可行麼?答案:能改,但不可行,請求發送到黑客自己後台不會帶上相冊的 Cookie,登錄態校驗通不過,敏感操作做不了。
1.2 用 Referrer 防 CSRF 安全麼?
處理了下面幾種特殊情況,用 Referrer 防 CSRF 是安全
讀操作不能有上面提到提到的兩種特殊情況,不能用 JSONP,CORS 要白名單,所以讀操作是安全的。
寫操作 Referrer 為空的時候不能放過,使用白名單機制,Referrer 在白名單內才放過。什麼時候會為空?
地址欄直接輸入 url 的時候,第一個請求 Referrer 為空,一般是 html 頁面,這種讀操作不用防 CSRF
使用 Referrer-Policy 策略設置 no-referrer 是,Referrer 為空,自己的頁面不要這樣設置,為了防黑客的頁面設置了,所以為空的時候不能放過。
還有一些 iframe 的特殊使用(以前用來繞過圖片防盜鏈的)也會導致 Referrer 為空,這些情況都不能放過過。
寫操作不能用 Get,如果寫操作可以用 Get,由於 Img 標籤,A 標籤能發 Get 請求。所以在一些 UGC 網站,比如用戶寫日誌可以插入自定義圖片,能插入自定義連接,圖片 img 標籤 src 或者 A 標籤的 href 就指向寫操作的 URL,這樣只要打開這篇日誌就會發送這個 Get 請求,或者點擊了日誌上的連接,就幫用戶做了寫操作。並且 Referrer 還是合法的。這就是一種少見的 CSRF 攻擊過程,其實也是最早期的 CSRF 攻擊。這種攻擊一旦成功,很方便做成蠕蟲病毒,危害性極大。_PS 有人覺得這種少見的攻擊過程不算 CSRF,應該算 XSS,好像也有點道理,但是常規的防 XSS 的方法貌似不好防這種特殊情況,下面要講的 CSRF 的 Token 的防護方式是能防這種特殊情況的。
1.3 用 Origin 可以麼?
可以,原理跟 Referrer 一樣,Origin 請求頭是 XHR2.0 里增加的,含義是發送請求頁面的域名,主要目的是解決跨域問題。如果用來校驗 CSRF 請求,就有一些細節要處理好,後台判斷 Origin 時也要使用白名單,並且不能為空,不在白名單內的請求都直接返回失敗,不能執行請求里的寫操作(有一些 web server 是請求執行了,也返回了數據,只是沒有配 ACAO 響應頭,瀏覽器收不到,這種情況能限制跨域請求,但是不能防 CSRF 的寫操作)。另外一種做法就是自定義 HTTP 請求頭,把 HTTP 請求升級為複雜請求,這樣在跨域的情況就會先發一個 Option 預檢請求,預檢請求通不過也就不會執行後面真實請求了。
1.4 既然能做到安全,為什麼現在很少見用這種方案?
因為有更簡單的方案,就是下面要講的 Token 方案。
2、Token
上面講到 Cookie 的一些特性的第二條,讀寫 Cookie 有跨域限制(作用域,Domain,Path),所以我們可以用這個特性來區分是自己頁面還是黑客頁面。只要頁面能讀(或者寫)www.photo.com域名 Cookie,就證明是自己的頁面。懂了原理,方案就很簡單,比如服務器通過 cookie 下發一個 token,token 值是隨機數,頁面發請求的時候從 cookie 取出 token 通過 HTTP 請求參數傳給後台,後台比對參數裡的 token 和 cookie 里的 token 是否一致,如果一致就證明是自己頁面發的請求,如果不一致就返回失敗。防 CSRF 的方案就是這麼簡單,這種方法能夠 100%防 CSRF,但是可能會有幾個變種,下面探討幾種情況。
2.1 Token 是前台生產還是後台生產?
我上面舉例例子是後台生成傳到前台的,大家發現其實後台並沒有存這個 token,所以原理上前後台生成都可以,只要保證隨機性。如果前端生成 token 然後寫到 Cookie 里,然後 HTTP 請求參數也帶上 token,後端邏輯一樣比對參數裡的 token 和 cookie 里的 token 是否一致,如果一致就證明是自己頁面發的請求,如果不一致就返回失敗。這就是 Cookie 讀和寫的差別,只要能讀寫自己域名的 Cookie 就是自己頁面。
2.2 推薦的最佳實踐方案
由於登錄態已經下發了一個登錄態 key,防 CSRF 的 token 就復用這個 key,由於登錄態 key 比較重要,儘量少明文暴露,所以前端拿到 key 後做了一次 Hash 放到 http 請求參數裡,後端通過同樣的 Hash 算法對 Cookie 里的 key 做 Hash 後跟參數裡的 token 比對是否一致,如果一致就證明是自己頁面發的請求,如果不一致就返回失敗。這裡對 Hash 算法要求不高,簡單高效就可以。
2.3 Token 放在 HTTP 參數裡的哪裡?
放在 URL 的 querystring 里,Post 請求的 Data 里或者 HTTP 請求頭裡,這三種方式都可以,只是有一點點細微的差別,如果 querystring 里,可能會影響 Get 請求的緩存效果,因為重新登錄之後 token 會變,url 也就變了,之前的緩存就失效了。如果放在 HTTP 請求頭裡,就需要使用 fetch 或者 XHR 發請求,這樣會變成複雜請求,跨域時需要多一次 Option 預檢請求,對性能多少有一點點影響。
2.4 用 Token 方案後寫操作可以用 Get 麼?
可以,從安全角度考慮是可以的,用了 Token 之後,Get 和 Post 的安全等級是一樣的,上面討論的那種少見的 CSRF 攻擊過程也攻擊不到了。但是從語義化考慮建議 Get 是還是處理讀操作方便理解。
2.5 用 Token 方案後讀操作可以用 JSONP 跨域麼?
可以,可以使用 JSONP 跨域了,另外如果使用 CORS 處理跨域,建議還是需要對請求頭的 Origin 做白名單限制,防止不同子域名相互影響。
2.6 如果頁面有 XSS 漏洞,黑客拿到 Cookie 怎麼辦?
這個方法防不了 XSS,防 XSS 需要其他方法,比如 CSP,用戶輸入/輸出做轉義等。
3、是否還有其他方案3.1 Cookie 的 SameSite 屬性可以麼?
不好用,SameSite 設計的目的貌似就是防 CSRF,但是我覺得不好用,SameSite 有三個值 Strict/Lax/None,設置的太嚴格,會影響自己業務的體驗,設置的太松沒有效果,就算最嚴格 Strict 模式,也防不了我上面提到寫操作用 Get 請求,UGC 頁面有自定義照片的情況。並且還有小部分老瀏覽器不支持,最終其實還是 Token 方案好用。
3.2 Cookie 的 HTTPOnly 屬性可以麼?
不行,HTTPOnly 表示這個 Cookie 只能是 HTTP 請求可以讀寫,js 沒有讀寫權限,瀏覽器還是會默認帶上,所以登錄態校驗是通過的。如果設置了 HTTPOnly 還有副作用,上面說的 Token 方案就用不了了。
3.3 驗證碼可以麼?
不行,驗證碼是用來防機器暴力攻擊的,驗證碼是用來確認敏感操作是自然人發送還是機器自動發送。這裡舉個圖片驗證碼的例子,大概原理是前端通過 img 標籤展示圖片驗證碼給用戶看(圖片字母經過噪音處理的),這個圖片 HTTP 請求也會設置一個 cookie 如 codeID=xxx(加密的),用戶在輸入框輸入圖片中展示的字母,敏感操作的 HTTP 請求通過參數把用戶輸入的 code 傳給後台,後台拿到用戶輸入的 code 和 cookie 里的 codeID(通常需要通過 id 查數據庫)做比較,如果一致就通過。這種驗證碼系統能夠防機器攻擊,但是防不了 CSRF,黑客同樣可以在黑客的頁面展示驗證碼給用戶,通過誘導用戶輸入驗證碼完成攻擊操作,只能是提高了 CSRF 攻擊成功的門檻,但是只要黑客頁面誘導信息勁爆還是有很大部分用戶會上當的。因為用戶不知道輸入驗證碼後會產生什麼影響。
驗證碼我在一些資料上看到說可以用來防 CSRF,我個人覺得是不行,包括手機驗證碼都不行,詳細情況大家可以研究各種驗證碼的實現原理。我猜測有些人可能有不同意見,但是如果非要構造一種能防 CSRF 的驗證碼技術上也是可行的。我這裡推演一下防護過程。就拿我上面舉得驗證碼舉例。驗證碼圖片的 url 是固定格式的http://code.photo.com/codeImg.jpg?v=123。v是隨機數,換一張時防止緩存用的。驗證碼每次請求會種一個Cookie,codeID=xxx,後台會存儲這個codeID對應的真實code,用戶輸入圖片看到驗證碼,要校驗驗證碼的請求參數會帶上用戶輸入的code,後台拿到參數code和Cookie里的CodeID查數據庫後對比來判斷是否輸入正確。攻擊方式,黑客可以在黑客頁面用img標籤展示這個驗證碼,因為驗證碼url是固定格式的,後面的流程是一樣的。你可能的改進方案:
驗證碼圖片做防盜鏈。黑客破解方案:可以用 Referrer-Policy 的 no-referrer
no-referrer 不給通過。黑客破解方案:可以用 iframe 嵌入你自己的頁面,只把你自己頁面種的驗證碼區域展示出來
我的頁面不給 iframe 嵌入。黑客說,你成功的防住了
所以需要對驗證碼做(1)(2)(3)的改造才能防 CSRF 攻擊。其實加的(1)(2)(3)措施都不是驗證碼的本意,驗證碼是本意用來的防機器攻擊的,不加(1)(2)(3)措施也一樣可以防機器攻擊。這裡就有三種觀點了。觀點一:我只要找到一個反例,找到驗證碼不能防 CSRF 的一個例子,我就證明了驗證碼不能防 CSRF。觀點二:我只要構造一個能防的例子,對驗證碼做一系列額外的改造來防 CSRF,我就證明了驗證碼能防 CSRF。觀點三:驗證碼提高了攻擊門檻,攻防就是魔高一丈道高一尺的過程,加上驗證碼更安全。我個人贊成觀點一。讀者們你們覺得呢?
3.4 HTTPS 可以麼?
不行,HTTPS 是防中間人攻擊的,不是防 CSRF 的
3.5 不用 Cookie 可以麼?
可以。個人覺得非常不好用,這裡討論兩種方案。
方案一:登錄態 key 不放在 Cookie。所以 HTTP 請求也不會自動攜帶 key,也就不存在 CSRF 漏洞,也就不用防了。但是這種設計我個人覺得在一些大型複雜網站是非常棘手難搞的,因為涉及到新開頁面,多個頁面之間登錄態需要同步(其中一個頁面退出登錄,登錄另外賬戶,或者登錄態過期續期等都需要同步給其他頁面),跨頁面通訊也有好多方案,如果你使用 localstorage 等本地緩存的話,關閉頁面還要清理緩存,緩存滿了要清理,瀏覽器兼容問題等。這種大型系統還會遇到其他的一系列問題,也會有一些列的解決方案,系統會比較複雜,最終還不如用 Cookie 方便。
方案二:登錄態 key 放 Cookie。CSRF 的 Token 不放 Cookie,後台生成 Token 藏在 HTML 頁面里,後台也存了這個 Token。HTTP 請求通過參數帶上這個 Token,後台拿到參數裡的 Token 跟自己後台存的 Token 做校驗。這個方案也是在一些資料里看到的。但是這個方案也是相當複雜,比如這個方案需要處理好幾個關鍵問題,這個 Token 是有用戶屬性,要跟用戶綁定的。如果 Token 跟用戶無關,那麼黑客可以用自己賬戶的 Token 欺騙做水平攻擊。另外也有新開頁面,多個頁面是同一個 Token 還是不同 Token?如果不同 Token,後台需要存一個 Token 列表,列表長度有最大值?另外還有如果其中一個頁面退出登錄,再登錄另外賬戶。那麼其他頁面登錄態是同步了,但是 Token 如何同步?同樣你也會有一些列的解決方案。當你把這些問題都解決了,最後你發現既然 Token 有用戶屬性,那麼就可以當登錄態用,就不用 Cookie 的登錄態 key 了,又回到了方案一。
關於本文作者:@曾健原文:https://zhuanlan.zhihu.com/p/522562168
為你推薦
【第712期】前後端分離架構下CSRF防禦機制
【第849期】如何讓前端更安全?——XSS攻擊和防禦詳解
歡迎自薦投稿,前端早讀課等你。