「你怎麼避免 CSRF?」這個題目也是面試時的常客,通常會跟著 XSS 一起出現,也是一個很基本的網站安全問題,網路上的相關文章俯拾即是,我將其整理下來給自己參考。
我參考的文章有一大堆,可見得 CSRF 的問題受到多大的重視,大家可以直接去閱讀這些文章。
- Wiki - 跨站請求偽造
- [ASP.NET] 實現與防範 CSRF 跨網站請求偽造攻擊
- ASP.NET MVC - ValidateAntiForgeryToken 與 自定 HandleError 處理顯示客製的錯誤訊息頁
- ASP.NET MVC 防範 CSRF 攻擊 - 在 AJAX 裡使用 AntiForgeryToken 的處理
- ASP.NET MVC如何避免CSRF攻擊
- [技術分享] Cross-site Request Forgery (Part 1)
- [技術分享] Cross-site Request Forgery (Part 2)
- Page.ViewStateUserKey 屬性
- AngularJS and AntiForgeryToken in ASP.NET MVC
- XSRF/CSRF Prevention in ASP.NET MVC and Web Pages
- Preventing Cross-Site Request Forgery (CSRF) Attacks in ASP.NET Web API
CSRF (Cross-site request forgery)
Wiki 的定義是這樣的:CSRF 被稱為 one-click attack 或者 session riding,通常縮寫為 CSRF 或者 XSRF, 是一種挾制用戶在當前已登錄的Web應用程式上執行非本意的操作的攻擊方法。跟跨網站指令碼(XSS)相比,XSS 利用的是用戶對指定網站的信任,CSRF 利用的是網站對用戶網頁瀏覽器的信任。
從以上描述各位就可以知道為什麼 CSRF 跟 XSS 通常在面試的時候會一起出現了吧,接著我用一個簡單的範例來 demo 什麼是 CSRF 攻擊。
身為疼惜老婆的軟體主廚,每天要匯 1000 塊給老婆,因此軟體主廚登入了一個匯款的網頁,準備匯 1000 塊給老婆。
有一天軟體主廚依舊登入匯款的網頁,準備要匯 1000 塊給老婆,不過他覺得網頁怎麼怪怪的,但是也不疑有他,就照著步驟照樣匯 1000 塊給老婆,當他匯出去之後才驚覺原來他中了圈套,剛剛那個匯款網頁是假的,不管匯給誰都會匯給壞人!
假網頁有一個名為 target 的 hidden field,value 永遠是「壞人」,所以不管使用者要匯款給誰,永遠都會匯款給「壞人」,這個就是一個 CSRF 攻擊。
<form id="form1" action="http://localhost:9361/Home/Transfer" method="post">
<p>從 <input id="source" name="source" type="text" /></p>
<p>匯 <input id="money" name="money" type="text" /></p>
<p>給 <input id="faketarget" name="faketarget" type="text" /><input id="target" name="target" type="hidden" value="壞人" /></p>
<p><input id="Submit1" type="submit" value="submit" /></p>
</form>
如何防範 CSRF 攻擊?
一種是檢查 Referer 欄位,在 HTTP Headers 裡傳送 Referer 資訊,以便讓網站辨別這是來自於合法 domain 的 Request。
但是 Referer 有被篡改的風險,所以使用第二種方式 - 檢查票證(Token) 是比較推薦的方式,從網站直接 Hash 一個值當成 Token 發給使用者端,而使用者端要發 Request 時必須帶入這個 Token 以提供給網站檢查,有點認票不認人的味道。
1. ASP.NET MVC 防範 CSRF 攻擊的方法
我們先在網頁的 form 之中加入 @Html.AntiForgeryToken()
這行程式碼。
<form id="form1" action="http://localhost:9361/Home/Transfer" method="post">
@Html.AntiForgeryToken()
<p>從 <input id="source" name="source" type="text" /></p>
<p>匯 <input id="money" name="money" type="text" /></p>
<p>給 <input id="faketarget" name="faketarget" type="text" /><input id="target" name="target" type="hidden" value="壞人" /></p>
<p><input id="Submit1" type="submit" value="submit" /></p>
</form>
加入這行程式碼後,我們會取得一個 Cookie __RequestVerificationToken
,之後要發 Request 時都要帶上這個 Cookie。
然後在 Action 加上 [ValidateAntiForgeryToken]
這個 Filter,只要沒有帶上 VerificationToken 的 Request 都視為是偽造的。
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Transfer(string source, int money, string target)
{
…
}
2. 自行打造防範 CSRF 攻擊的方法
剛剛是利用 AntiForgeryToken Helper 以及 ValidateAntiForgeryToken Filter 幫我們建立起防範 CSRF 攻擊的機制,如果我要自己做的話怎麼辦?
首先我們必須自己撰寫 Hash Token 的邏輯
private static string GetAntiForgeryToken()
{
string cookieToken;
string formToken;
// 這邊我用 AntiForgery 這個 Helper 來幫我產生 Token。
// 我們也可以自己撰寫自己想要的邏輯,用 MD5、SHAxxx…等方式產生一個 Hash 過的 Token。
AntiForgery.GetTokens(null, out cookieToken, out formToken);
return string.Concat(cookieToken, ":", formToken);
}
接著就把這個 Token 交給前端並且保存好,我這邊為了方便起見,把 Token 存到一個 hidden field。
<form id="form1" action="/Home/Transfer" method="post">
<input type="hidden" id="verificationToken" name="verificationToken" value="@ViewBag.VerificationToken" />
<p>從 <input id="source" name="source" type="text" /></p>
<p>匯 <input id="money" name="money" type="text" /></p>
<p>給 <input id="target" name="target" type="text" /></p>
<p><input id="Submit1" type="button" value="submit" /></p>
</form>
再來,客製一個 Action Filter 來檢查 Token。
public class CustomValidateAntiForgeryToken : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
try
{
ValidateAntiForgeryToken(filterContext.HttpContext.Request.Headers["VerificationToken"]);
}
catch (HttpAntiForgeryException ex)
{
throw new HttpAntiForgeryException("注意!您現在正在操作一個偽造的網頁!");
}
}
private static void ValidateAntiForgeryToken(string verificationToken)
{
string cookieToken = "";
string formToken = "";
if (!string.IsNullOrEmpty(verificationToken))
{
string[] tokens = verificationToken.Split(':');
if (tokens.Length == 2)
{
cookieToken = tokens[0].Trim();
formToken = tokens[1].Trim();
}
}
// 因為我用 AntiForgery Helper 幫我產生 Token,這邊一樣用 AntiForgery 來做 Validation。
// 如果 Token 是自己 Hash 的,就自己另外寫檢查的邏輯。
AntiForgery.Validate(cookieToken, formToken);
}
}
然後把我們客製的 CustomValidateAntiForgeryToken 掛到 Action 上
[HttpPost]
[CustomValidateAntiForgeryToken]
public ActionResult Transfer(string source, int money, string target)
{
…
}
最後我發一個 AJAX Request 執行我原先的商業行為,並且把 VerificationToken 放到 Headers 提供給網站驗證。
$.ajax({
url: '/Home/Transfer',
method: 'POST',
data: {
source: $("#source").val(),
money: $("#money").val(),
target: $("#target").val()
},
headers: {
"VerificationToken": $("#verificationToken").val()
}
})
.done(function (data) {
alert(JSON.stringify(data));
});
如果是沒有帶有 VerificationToken 的 Request 就會收到我自定義的錯誤訊息
< Source Code >