防止 ASP.NET MVC 應用程式中的跨網站偽造要求 (CSRF) 攻擊
跨網站偽造要求 (CSRF) 是一種攻擊,惡意網站會將要求傳送至使用者目前登入的易受攻擊網站
以下是 CSRF 攻擊的範例:
使用者使用表單驗證登入
www.example.com
。伺服器對使用者進行驗證。 來自伺服器的回應包含驗證 Cookie。
若未登出,使用者就會瀏覽惡意網站。 此惡意網站包含下列 HTML 表單:
<h1>You Are a Winner!</h1> <form action="http://example.com/api/account" method="post"> <input type="hidden" name="Transaction" value="withdraw" /> <input type="hidden" name="Amount" value="1000000" /> <input type="submit" value="Click Me"/> </form>
請注意,表單動作會發佈到易受攻擊的網站,而不是惡意網站。 這是 CSRF 的「跨網站」部分。
使用者按一下 [提交] 按鈕。 瀏覽器包含具有要求的驗證 Cookie。
該請求在具有使用者驗證內容的伺服器上執行,並且可以執行允許已驗證使用者執行的任何操作。
雖然此範例需要使用者按一下表單按鈕,但惡意頁面可能會同樣輕鬆地執行自動提交表單的指令碼。 此外,使用 SSL 並不會防止 CSRF 攻擊,因為惡意網站可以傳送「https://」要求。
一般而言,CSRF 攻擊可能會針對使用 Cookie 進行驗證的網站,因為瀏覽器會將所有相關 Cookie 傳送至目的地網站。 但是,CSRF 攻擊不僅限於利用 Cookie。 例如,基本和摘要式驗證也很容易受到攻擊。 在使用者以基本或摘要式驗證登入之後, 瀏覽器會自動傳送認證,直到工作階段結束為止。
防偽權杖
為了協助防止 CSRF 攻擊,ASP.NET MVC 會使用反偽造權杖,也稱為 要求驗證權杖。
- 用戶端會要求包含表單的 HTML 頁面。
- 伺服器在回應中包含兩個權杖。 一個權杖會以 Cookie 的形式傳送。 另一個會放在隱藏的表單欄位。 權杖會隨機產生,讓敵人無法猜測這些值。
- 當用戶端提交表單時,它必須將這兩個權杖傳送回伺服器。 用戶端會以 Cookie 形式傳送 Cookie 權杖,並在表單資料內傳送表單權杖。 (當使用者提交表單時,瀏覽器用戶端會自動執行此動作。)
- 如果要求不包含這兩個權杖,伺服器就不允許要求。
以下是具有隱藏表單權杖的 HTML 表單範例:
<form action="/Home/Test" method="post">
<input name="__RequestVerificationToken" type="hidden"
value="6fGBtLZmVBZ59oUad1Fr33BuPxANKY9q3Srr5y[...]" />
<input type="submit" value="Submit" />
</form>
反偽造權杖的運作因惡意頁面無法讀取使用者的權杖,因為相同來源原則。 (相同來源原則會防止託管於兩個不同的網站上的文件存取彼此的內容。因此,在先前的範例中,惡意頁面可以將要求傳送至 example.com,但無法讀取回應。
若要防止 CSRF 攻擊,請使用反偽造權杖搭配瀏覽器在使用者登入之後以無訊息方式傳送認證的任何驗證通訊協定。 這包括 Cookie 型驗證通訊協定,例如表單驗證,以及基本驗證和摘要式驗證等通訊協定。
您應該要求任何非安全方法的防偽權杖 (POST、PUT、DELETE)。 此外,請確定安全的方法 (GET、HEAD) 沒有任何副作用。 此外,如果您啟用跨網域支援,例如 CORS 或 JSONP,則即使是 GET 之類的安全方法仍可能容易受到 CSRF 攻擊,讓攻擊者讀取潛在的敏感資料。
ASP.NET MVC 中的反偽造權杖
若要將防偽權杖新增至 Razor 頁面,請使用 HtmlHelper.AntiForgeryToken 協助程式方法:
@using (Html.BeginForm("Manage", "Account")) {
@Html.AntiForgeryToken()
}
這個方法會新增隱藏的表單欄位,同時設定 Cookie 權杖。
防 CSRF 和 AJAX
表單權杖對於 AJAX 要求來說可能是一個問題,因為 AJAX 要求可能會傳送 JSON 資料,而不是 HTML 表單資料。 有一個解決方案是在自訂 HTTP 標頭中傳送權杖。 下列程式碼使用 Razor 語法來產生權杖,然後將權杖新增至 AJAX 要求。 系統會藉由呼叫 AntiForgery.GetTokens 在伺服器上產生權杖。
<script>
@functions{
public string TokenHeaderValue()
{
string cookieToken, formToken;
AntiForgery.GetTokens(null, out cookieToken, out formToken);
return cookieToken + ":" + formToken;
}
}
$.ajax("api/values", {
type: "post",
contentType: "application/json",
data: { }, // JSON data goes here
dataType: "json",
headers: {
'RequestVerificationToken': '@TokenHeaderValue()'
}
});
</script>
當您處理要求時,請從要求標頭擷取權杖。 然後呼叫 AntiForgery.Validate 方法以驗證權杖。 如果權杖無效,則 Validate 方法將擲回異常。
void ValidateRequestHeader(HttpRequestMessage request)
{
string cookieToken = "";
string formToken = "";
IEnumerable<string> tokenHeaders;
if (request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
{
string[] tokens = tokenHeaders.First().Split(':');
if (tokens.Length == 2)
{
cookieToken = tokens[0].Trim();
formToken = tokens[1].Trim();
}
}
AntiForgery.Validate(cookieToken, formToken);
}