Preventing Cross-Site Request Forgery (CSRF) Attacks in ASP.NET Web API
by Mike Wasson
Cross-Site Request Forgery (CSRF) is an attack where a malicious site sends a request to a vulnerable site where the user is currently logged in
Here is an example of a CSRF attack:
- A user logs into www.example.com, using forms authentication.
- The server authenticates the user. The response from the server includes an authentication cookie.
Without logging out, the user visits a malicious web site. This malicious site contains the following HTML form:
[!code-htmlMain]
1: <h1>You Are a Winner!</h1>
2: <form action="http://example.com/api/account" method="post">
3: <input type="hidden" name="Transaction" value="withdraw" />
4: <input type="hidden" name="Amount" value="1000000" />
5: <input type="submit" value="Click Me"/>
6: </form>
- The user clicks the submit button. The browser includes the authentication cookie with the request.
The request runs on the server with the user’s authentication context, and can do anything that an authenticated user is allowed to do.
Although this example requires the user to click the form button, the malicious page could just as easily run a script that submits the form automatically. Moreover, using SSL does not prevent a CSRF attack, because the malicious site can send an “https://” request.
Typically, CSRF attacks are possible against web sites that use cookies for authentication, because browsers send all relevant cookies to the destination web site. However, CSRF attacks are not limited to exploiting cookies. For example, Basic and Digest authentication are also vulnerable. After a user logs in with Basic or Digest authentication. the browser automatically sends the credentials until the session ends.
Anti-Forgery Tokens
To help prevent CSRF attacks, ASP.NET MVC uses anti-forgery tokens, also called request verification tokens.
- The client requests an HTML page that contains a form.
- The server includes two tokens in the response. One token is sent as a cookie. The other is placed in a hidden form field. The tokens are generated randomly so that an adversary cannot guess the values.
- When the client submits the form, it must send both tokens back to the server. The client sends the cookie token as a cookie, and it sends the form token inside the form data. (A browser client automatically does this when the user submits the form.)
- If a request does not include both tokens, the server disallows the request.
Here is an example of an HTML form with a hidden form token:
[!code-htmlMain]
1: <form action="/Home/Test" method="post">
2: <input name="__RequestVerificationToken" type="hidden"
3: value="6fGBtLZmVBZ59oUad1Fr33BuPxANKY9q3Srr5y[...]" />
4: <input type="submit" value="Submit" />
5: </form>
Anti-forgery tokens work because the malicious page cannot read the user’s tokens, due to same-origin policies. (Same-origin policies prevent documents hosted on two different sites from accessing each other’s content. So in the earlier example, the malicious page can send requests to example.com, but it cannot read the response.)
To prevent CSRF attacks, use anti-forgery tokens with any authentication protocol where the browser silently sends credentials after the user logs in. This includes cookie-based authentication protocols, such as forms authentication, as well as protocols such as Basic and Digest authentication.
You should require anti-forgery tokens for any nonsafe methods (POST, PUT, DELETE). Also, make sure that safe methods (GET, HEAD) do not have any side effects. Moreover, if you enable cross-domain support, such as CORS or JSONP, then even safe methods like GET are potentially vulnerable to CSRF attacks, allowing the attacker to read potentially sensitive data.
Anti-Forgery Tokens in ASP.NET MVC
To add the anti-forgery tokens to a Razor page, use the HtmlHelper.AntiForgeryToken helper method:
[!code-cshtmlMain]
1: @using (Html.BeginForm("Manage", "Account")) {
2: @Html.AntiForgeryToken()
3: }
This method adds the hidden form field and also sets the cookie token.
Anti-CSRF and AJAX
The form token can be a problem for AJAX requests, because an AJAX request might send JSON data, not HTML form data. One solution is to send the tokens in a custom HTTP header. The following code uses Razor syntax to generate the tokens, and then adds the tokens to an AJAX request. The tokens are generated at the server by calling AntiForgery.GetTokens.
[!code-htmlMain]
1: <script>
2: @functions{
3: public string TokenHeaderValue()
4: {
5: string cookieToken, formToken;
6: AntiForgery.GetTokens(null, out cookieToken, out formToken);
7: return cookieToken + ":" + formToken;
8: }
9: }
10:
11: $.ajax("api/values", {
12: type: "post",
13: contentType: "application/json",
14: data: { }, // JSON data goes here
15: dataType: "json",
16: headers: {
17: 'RequestVerificationToken': '@TokenHeaderValue()'
18: }
19: });
20: </script>
When you process the request, extract the tokens from the request header. Then call the AntiForgery.Validate method to validate the tokens. The Validate method throws an exception if the tokens are not valid.
[!code-csharpMain]
1: void ValidateRequestHeader(HttpRequestMessage request)
2: {
3: string cookieToken = "";
4: string formToken = "";
5:
6: IEnumerable<string> tokenHeaders;
7: if (request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
8: {
9: string[] tokens = tokenHeaders.First().Split(':');
10: if (tokens.Length == 2)
11: {
12: cookieToken = tokens[0].Trim();
13: formToken = tokens[1].Trim();
14: }
15: }
16: AntiForgery.Validate(cookieToken, formToken);
17: }
|