Cross-Site Request Forgery (CSRF) attacks are widespread, and even some BigTech companies suffer from them.
- Netflix suffered in 2006 with CSRF vulnerabilities. Attackers could change login credentials, change the shipping address and send DVDs to a newly set address.
- YouTube suffered from CSRF attacks where an attacker could perform actions of any user
- ING Direct Banking has lost money to CSRF attackers who used their web application to do illicit money transfers
- McAfee Secure’s vulnerability allowed attackers to change their company system.
How were the attackers able to do this? What were some of the techniques they used? To understand this, we need to understand CSRF in detail.
What is a Cross-Site Request Forgery (CSRF) attack?
CSRF is when an attacker submits unauthorized commands to a website user already logged in. In layman’s terms, When you click on a malicious hyperlink, it triggers scripts that perform actions on your behalf to your logged-in bank website. Viola, the attackers, have your money.
The malicious link would look like these.
- “You are a winner.”
- “You placed order # 648722.”
Why are CSRF vulnerabilities a must-fix in the ASP.NET MVC world?
ASP.NET MVC and ASP.NET Core are traditionally some of the most used platforms to build financial web applications, such as banks and hedge funds. From a statistical standpoint, these platforms are trusted more than their counterparts, such as Express or NodeJS, for financial web applications. In addition, it is easier to fix CSRF issues in ASP.NET Core than in ASP.NET MVC because of the better tools and support available. We will investigate techniques to fix CSRF issues in ASP.NET MVC.
How to attack an ASP.NET MVC website using a CSRF attack vector
Before we understand how to fix CSRF issues, we need to know how they happen in the first place. For example, suppose you log into your bank website at onlinebank.com. And you are visiting a malicious website on another tab, which looks like this. (This is an actual screenshot of a spam email I received.).
The “Get Started” button could be hiding a script and Html form like this.
1 2 3 4 |
<h1>Affordable Medicare Plans are available. Hurry now!</h1> <form action="http://onlinebank.com/transfer?account=76543865&amount=15000" method="post"> <input type="submit" value="Get Started"/> </form> |
When you click the “Get Started” button, you can see the network traffic originating from it using the developer console built into your browser by pressing the F12 key on most any browser (Developer console is essential for web development and almost any engineers who work on front end browser code should know about it.(
If you are already logged into the onlinebank.com website, this traffic would include the session cookies. Now the attacker has all the pieces needed to solve the puzzle:
- a logged-in user
- users’ session cookies
- a URL that steals the victims’ money.
No wonder CSRF attacks are also called session-riding and one-click attacks.
There are other ways the attack can also happen, using img tags.
Now that we have seen how the attack can happen let’s discuss our prevention strategies.
Enter Anti-Forgery Tokens. Drum roll, please 🥁🥁🥁!
The suggested way to prevent CSRF
attacks is to use tokens that you would only know. Your ASP.NET MVC web app generates the tokens, and we verify these tokens on relevant requests to the server. Since GET requests are not supposed to alter the persisted information, it is ideal to use and verify this token on POST
, PUT
, PATCH
, and DELETE
requests.
Let’s outline the steps needed.
- User visits a page
- On page request, ASP.NET MVC generates two tokens. A cookie token and a hidden form field token. The server embeds both tokens in response.
- When the user does an action that alters data, such as a form submission, the request should contain both of these tokens.
- Server verifies if the action request has both tokens; if not, the server says ‘no’ to the request.
In short, think of this as accessing a bank locker, but you can only do it in the presence of a bank manager(anti-forgery tokens) sent by the bank(server) even though you have the key to the locker(session cookie).
Enough talk, Show me the code for ASP.NET MVC.
You would add `@Html.AntiForgeryToken()`
to your Html forms.
1 2 3 4 5 |
@using (Html.BeginForm("Transfer", "Account", FormMethod.Post)) { @Html.AntiForgeryToken() <input type="submit" value="Submit Form" /></td> } |
It would translate to:
1 2 3 4 |
<form action="Account/Transfer" method="post"> <input name="__RequestVerificationToken" type="hidden" value="CfDJles20ilG0VXAiNm3Fl5Lyu_JGpQDA......"/> <input type="submit" value="Submit Form" /> </form> |
As you can see, ASP.NET MVC has added `__RequestVerificationToken`
to this form token as a hidden field. This token is generated at the server.
Now, when this form is submitted, this form token will be submitted along with form data, and the cookie token will make it to the server as part of the request as well.
Now we have only one job left; verify both tokens at the server.
To do that, a method like `AntiForgery.Validate(cookieToken, formToken); `
will do the job. But for ASP.NET MVC, there is a built-in attribute that would do this job for you – `ValidateAntiForgeryToken`
.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using System.Web.Mvc; namespace Online.Bank.App.Controllers { public class AccountController : Controller { [HttpPost] [ValidateAntiForgeryToken] // This attribute will do the Anti-Forgery token validation for you. public ActionResult Transfer() { return Json(true); // This line is added for brevity. You would be doing good stuff here. } } } |
What about AJAX and jQuery?
AJAX primarily uses JSON data instead of HTML form data. It poses a problem for us. The above code won’t work. How do we solve this?
Let’s assume we need to call an endpoint Account/Manage using AJAX.
As the first step, using razor syntax, we can ask the server to generate the tokens and give them to us like this.
1 2 3 4 5 6 7 8 9 10 |
<script> @functions{ public string GetAntiForgeryTokens() { string cookieToken, formToken; AntiForgery.GetTokens(null, out cookieToken, out formToken); return cookieToken + ":" + formToken; } } </script> |
And then send the tokens to the server using AJAX.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<input type="button" onclick="manageAccount()" value="Manage Account" /> <script> function manageAccount() { $.ajax("<baseurl>/Account/Manage", { type: "post", contentType: "application/json", data: { }, // JSON data dataType: "json", headers: { '__RequestVerificationToken': '@GetAntiForgeryTokens()' }, success: function (data) { console.log(`Account updated: ${data}`); } }); } </script> |
Finally, verify these tokens on the server side. The `Manage`
action would be similar to this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
using System; using System.Linq; using System.Web.Helpers; using System.Web.Mvc; namespace Online.Bank.App.Controllers { public class AccountController : Controller { [HttpPost] public ActionResult Manage() { string cookieToken = ""; string formToken = ""; var tokenHeaders = Request.Headers.GetValues("__RequestVerificationToken"); string[] tokens = tokenHeaders?.First()?.Split(':'); if (tokens.Length == 2) { cookieToken = tokens[0].Trim(); formToken = tokens[1].Trim(); } try { AntiForgery.Validate(cookieToken, formToken); } catch (Exception ex) { // Alert folks that someone is trying to attack this method. } return Json(true); // This line is added for brevity. You would be doing good stuff here. } } } |
That’s it. Now our Account/Manage
endpoint is protected.
But do you see a problem? Do we have to repeat this in every action method? How to avoid that?
Custom Anti-Forgery Attribute to the rescue!
What if we create a custom ASP.NET MVC attribute with the above code? Then we can decorate action methods or controllers with that attribute, right?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
using System; using System.Web; using System.Web.Helpers; using System.Web.Mvc; namespace Online.Bank.App.Attributes { [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public sealed class ValidateHeaderAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter { // Bonus method private void TellEveryoneWeBlockedAttacker(HttpContextBase httpContext, Exception ex) { string controllerName = httpContext?.Request?.RequestContext?.RouteData?.Values["controller"]?.ToString(); string actionName = httpContext?.Request?.RequestContext?.RouteData?.Values["action"]?.ToString(); ex.Data.Add("ControllerName", controllerName); ex.Data.Add("ActionName", actionName); // Use the exception we created to notify the logging or error reporting system. // You may alternatively send an email message or slack message here. } public void OnAuthorization(AuthorizationContext filterContext) { if (filterContext == null) { throw new ArgumentNullException("filterContext"); } var httpContext = filterContext.HttpContext; var cookie = httpContext.Request.Cookies[AntiForgeryConfig.CookieName]; try { AntiForgery.Validate(cookie != null ? cookie.Value : null, httpContext.Request.Headers["__RequestVerificationToken"]); } catch (Exception ex) { TellEveryoneWeBlockedAttacker(httpContext, ex); } } } } |
And decorate the action method with the ValidateHeaderAntiForgeryToken attribute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System.Web.Mvc; using Online.Bank.App.Attributes; namespace Online.Bank.App.Controllers { public class AccountController : Controller { [HttpPost] [ValidateHeaderAntiForgeryToken] // This is where we put the Anti-Forgery attribute we just created public ActionResult Manage() { return Json(true); // This line is added for brevity. You would be doing good stuff here. } } } |
See how clean the code got. Now we can use this attribute across the entire ASP.NET MVC web application to decorate action methods and controllers. The beauty of attributes, isn’t it.
Can we optimize the client-side code as well?
ASP.NET MVC generally has a _Layout.cshtml
file. We can get the Anti-Forgery tokens there using JavaScript and @Html.AntiForgeryToken()
. Though _Layout.cshtml
would be the ideal spot, it can be done anywhere in your razor files.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<script> function getAntiForgeryToken() { let token = '@Html.AntiForgeryToken()'; token = $(token).val(); return token; } </script> Usage <input type="button" onclick="manageAccount()" value="Manage Account" /> <script> function manageAccount() { $.ajax("<baseurl>/Account/Manage", { type: "post", contentType: "application/json", data: { }, // JSON data dataType: "json", headers: { '__RequestVerificationToken': getAntiForgeryToken() }, success: function (data) { alert(`Account Updated: ${data}`); } }); } </script> |
That’s good. What if we have a lot of AJAX calls, and we want the Anti-Forgery token to be present in every request?
You can set up jQuery requests to have the tokens added to the request header by default using $.ajaxSetup()
.
Usage is like this.
1 2 3 4 5 |
$.ajaxSetup({ headers: { '__RequestVerificationToken': getAntiForgeryToken() } }); |
That’s it. We made the life of the attacker a bit more complicated. Isn’t that a good thing?
Wrap up
Cross-Site Request Forgery (CSRF) vulnerabilities are not easily detectable without security scans. Implementing a technique presented here (or any technique for that matter) would save numerous heads, pain, and suffering. When it comes to application performance, reactive will do just fine. But in application security, proactive is always better than reactive! CSRF is just one attack vector; there are others like XSS, SQL Injections, and many others. More on that later! In the meantime, feel free to check out this example project demonstrating the code and concepts described above.
Load comments