CSRF(Cross-Site Request Forgery)是攻擊者利用受害者的身份,在受害者不知情的狀況下,向伺服器發送惡意請求,例如:發送訊息、購買商品、銀行轉帳等。
與 XSS(Cross-Site Scripting)不同的是,XSS 通常是在網站上輸入惡意程式碼來進行攻擊,利用的是使用者對目標網站的信任;而 CSRF 則是基於網站伺服器對使用者瀏覽器的信任。
攻擊原理
多數的網站在使用者登入後,都會在瀏覽器產生 Cookie,之後的操作就不需要再次登入,瀏覽器會自動將 Cookie 發送給網站伺服器來識別身份。不過,因為 Cookie 會在瀏覽器中存在一段時間,使得攻擊者有機可趁,在 Cookie 過期前,都有遭受 CSRF 攻擊的可能性。
最簡單的攻擊方法,是在 URL 中加上參數。假設使用者在登入部落格的情況下,點擊了攻擊者提供的連結,伺服器就會接收到 GET 請求,將文章刪除:
<a href="<https://example-blog.com/delete?id=4>"></a>
攻擊者也能利用圖片的 src
屬性發送 request
<img src="<https://example-blog.com/delete?id=4>" width="0" height="0">
甚至可以用 <form>
發送 POST 請求,讓表單提交之後的結果出現在一個看不見的 <iframe>
裡面,而且還可以自動提交,完全不需要經過使用者的任何操作。
<iframe style="display:none" name="csrf-frame"></iframe>
<form method="POST" action="<https://example-blog.com/delete>" target="csrf-frame" id="csrf-form">
<input type="hidden" name="id" value="4">
<input type="submit" value="submit">
</form>
<script>document.getElementById("csrf-form").submit()</script>
使用者端的防範
CSRF 之所以能夠成立,是因為使用者在被攻擊的網站中,處於登入的狀態,攻擊者才能偽造使用者的身份執行一些行為。因此,在含有敏感資訊(如個人資料、銀行帳戶等)的網站進行操作時,必須特別注意:
- 處於登入狀態時,不要隨意開啟其他來源不明的網站
- 不要使用瀏覽器記住帳號、密碼的功能
- 完成操作後,立刻登出
不過使用者能做的防禦有限,伺服器端還是應當做好預防措施。
伺服器端的防範
檢查 Referer
HTTP Request Headers 有一個 referer 欄位,代表發出請求的來源位址(URL),可以透過 referer 檢查是否為相同 domain 所發出的請求,不是的話就拒絕請求。不過這個做法並不完善,有幾點要注意:
- 有些瀏覽器可能不會在 Request Headers 中帶上 referer 欄位
- 有些使用者可能會關閉或停用自動帶 referer 的功能
- 攻擊者可以透過竄改 referer 來騙過目標網站的伺服器
圖形、簡訊驗證碼
通常在網路銀行轉帳的時候,都會收到一組簡訊驗證碼,為了確保行為是由使用者本人操作。圖形驗證碼也是同樣的道理,如果攻擊者不知道圖形驗證碼的答案,也就不可能攻擊成功了。雖然這是一個相對安全的作法,但是對使用者來說卻十分麻煩。
Synchronizer Token Pattern
Synchronizer Token Pattern(STP)指的是每次使用者發出請求時都必須傳回一個伺服器所指定的亂數,而這個亂數可以設計成適用於整個 session 階段,也可以設計成每個請求只能使用一次,後者(per-request token)的作法會比前者來得安全,但是對使用者來說較為不便,所以在大部分的情況下,前者(per-session token)反而是比較常用的作法。
伺服器產生一組隨機且不重複的 token 放在表單中隱藏的欄位:
<form action="<https://example.com>" method="POST">
<input type="hidden" name="CSRFToken" value="KbyUmhTLMpYj7CD2di7JKP1P3qmLlkPt">
...
</form>
表單提交時,伺服器會比對這組 token 與自己 session 中存放的值是否相同,攻擊者則因為無法得知正確的 token 而請求失敗。
Double Submit Cookie
做法和 STP 類似,只不過 Double Submit Cookie 是無狀態的(stateless),意思是伺服器不需要保存 token,而是存放在 client-side 的 cookie 中。表單提交時,伺服器會比對 cookie 中 token 與表單中 token 的值:
Set-Cookie: CSRFToken=KbyUmhTLMpYj7CD2di7JKP1P3qmLlkPt
<form action="<https://example.com>" method="POST">
<input type="hidden" name="CSRFToken" value="KbyUmhTLMpYj7CD2di7JKP1P3qmLlkPt">
...
</form>
受到同源政策的限制,攻擊者不能從其他 domain 讀取或設置目標網站的 cookie,因此無法得知正確的 token。
不過這個做法依然有些缺點,攻擊者如果掌握了目標網站的 subdomain,就能夠設置 cookie,因此最好確保每個 subdomains 都是安全的,並且只接受 HTTPS connections。
上面的做法,token 是由 server-side 產生,如果網站是 SPA(Single Page Application)的話,則可以由 client-side 產生,把 token 統一放在 Request Headers 裡面,就不用每個表單都放。(axios 就有提供這樣的功能)
SameSite Cookie
SameSite
不允許 cookie 來自跨來源的請求(cross-origin requests)。分別有三種模式:Strict
、Lax
、None
Strict
最為嚴格,<a href="...">
、<form>
、new XMLHttpRequest
等等,只要不是相同來源的請求,就不會帶上這個 cookie。Lax
稍微寬鬆,除了 GET 以外的方法都不會帶上這個 cookie。None
則不限制跨站請求。
Set-Cookie: JSESSIONID=xxxxx; SameSite=Strict
Set-Cookie: JSESSIONID=xxxxx; SameSite=Lax
目前 Google Chrome 的預設值是 Lax
。在 Lax
模式下,使用者可以從外部的連結進入網站,並且保持登入狀態,有些網站會在敏感操作時,才帶上 SameSite=Strict
的 cookie,以防止 CSRF 攻擊。