The Problem
As a developer, you may or may not ever think about Cross Site Request Forgery (CSRF) and how it can be a handy channel for hackers to maliciously attack you through the use of other users. So what is it exactly. Well simply put a Cross Site Request Forgery is when a hacker does not have enough access rights to attack your system directly. So they will usually trick users with with the access rights to malicious submit data on their behalf. Usually this kind of attack involves the user having an opened session using either session or persistent cookies.
For example, consider a manager working at a bank. Lets call him Bob. He happens to have some IT skills and knows what a HTML form looks like. He currently has access to bank account information, and had the ability to transfer funds to any account of his choosing from the form illustrated below (seems far fetched, but just go with me on this).
All of a sudden, he was caught photocopying his butt, because lets just say he's nuts, and was then fired. So as a result all access he had to such functionality had been revoked. Knowing how the administration screens worked, he then took a copy of the HTML markup just before he left the building and emailed it to himself. The markup looked something like this.
- <form action="http://hostsite.com/Account/Transfer" method="POST">
- <fieldset>
- <legend>Transfer Details</legend>
- <div>
- <label for="fromAccountNumber">From Account Number:</label>
- <input type="text" id="fromAccountNumber" name="fromAccountNumber" />
- </div>
- <div>
- <label for="toAccountNumber">To Account Number:</label>
- <input type="text" id="toAccountNumber" name="toAccountNumber" />
- </div>
- <div>
- <label for="amount">Amount: </label>
- <input type="text" id="amount" name="amount" />
- </div>
- <div>
- <input type="submit" value="Submit" />
- </div>
- </fieldset>
- </form>
Keeping in mind that the important factor was the forms action and the input names, he was able to generate his own form that can be hidden from the user on a hack website page hosted at http://hacksite.com. It would contain similar markup noted below.
- <form id="hackForm" action="http://hostsite.com/Account/Transfer" method="POST" style="display:none;">
- <input type="text" name="fromAccountNumber" value="10000001" />
- <input type="text" name="toAccountNumber" value="10000002" />
- <input type="text" name="amount" value="1000000" />
- </form>
- <script>
- document.getElementById("hackForm").submit();
- </script>
Note that the action attribute on the form points directly to that host sites url (http://hostsite.com/Account/Transfer) which happens to be the url that handles the form post for transferring funds. The value attributes of each input element also contains hard coded values, ensuring that when the post is made that the account number he chooses will be involved in the transfer (Poor person who owns account 10000001). Now I know that this probably is more effective to be an ajax post, but just for simplicity I will keep it as a standard form post.
Now lets also consider that Bob has a buddy who still works at the company. Lets just call him Ed. Bob then sends an email to Ed with a link in it. The link might state "Click here to see my funny pictures". But the href redirects him to http://hacksite.com instead.
The form above is then immediately posted via some javascript and the input is then submitted to http://hostsite.com/Account/Transfer. Ed having proper authentication and authorization to access the hostsite.com website is then able to transfer the funds from account "10000001" to account "10000002" without actually intending it.
Given a normal MVC scenario, where a developer has not considered this problem he may write a simple piece of code like this to handle the form post.
- [Authorize]
- public class AccountController : Controller
- {
- [HttpPost]
- public ActionResult Transfer(string fromAccountNumber, string toAccountNumber, decimal amount)
- {
- var accountRepository = new AccountRepository();
- accountRepository.TranferFunds(fromAccountNumber, toAccountNumber, amount);
- return RedirectToAction("Success");
- }
- }
Given this attack is not an indirect one, and really a forgery trick, (thus the term Request Forgery), a simple Authorize attribute is not sufficient enough to prevent such an attack. Ed will generally have access to perform the post and Bob knows it. Therefore Bob scores himself 1000000 bucks.
The fix
This is relatively easy to implement in MVC. It just requires 2 changes in order to prevent the attack. The first involves making a change to your view by making a call to the AntiForgeryToken method of the HtmlHelper as shown below.
- <form action="http://hostsite.com/Account/Transfer" method="POST">
- @Html.AntiForgeryToken()
- <fieldset>
- <legend>Transfer Details</legend>
- <div>
- <label for="fromAccountNumber">From Account Number:</label>
- <input type="text" id="fromAccountNumber" name="fromAccountNumber" />
- </div>
- <div>
- <label for="toAccountNumber">To Account Number:</label>
- <input type="text" id="toAccountNumber" name="toAccountNumber" />
- </div>
- <div>
- <label for="amount">Amount: </label>
- <input type="text" id="amount" name="amount" />
- </div>
- <div>
- <input type="submit" value="Submit" />
- </div>
- </fieldset>
- </form>
Doing this emits 2 pieces of information to the client's browser. The first is a hidden field with the name of "__RequestVerificationToken". This hidden field contains an encrypted value that is updated on each request to the server, so it never remains consistent. At the same time the helper will also generate a cookie which is "HttpOnly", and therefore not accessible via javascript. This cookie contains the same name as the hidden field and also contains the same value.
The second part involves simply including the ValidateAntiForgeryTokenAttribute on top of your controller or action method.
- [Authorize]
- public class AccountController : Controller
- {
- [HttpPost]
- [ValidateAntiForgeryTokenAttribute]
- public ActionResult Transfer(string fromAccountNumber, string toAccountNumber, decimal amount)
- {
- var accountRepository = new AccountRepository();
- accountRepository.TranferFunds(fromAccountNumber, toAccountNumber, amount);
- return RedirectToAction("Success");
- }
- }
This attribute implements the IAuthorizationFilter interface. This means that by default the controller will attempt to invoke this authorize attribute and ensure the data being posted is valid before it will allow the action to even be invoked in the first place. It's responsibility is to look at both the posted "__RequestVerificationToken" form data and the value from the cookie. If both values match, then it assumes that the information posted was legit and allows processing of the request to continue. If they do not match, or either the hidden field or cookie is missing, then you will receive an error and the data will never be processed.
How does this actually fix the problem?
So going back to the Bob and Ed scenario, how does this prevent Bob from transferring funds through Ed? The answer is pretty straight forward. There is no way ahead of time that Bob can predict the encrypted cookie value, that Ed has on his system. Therefore he could never properly generate the hidden field that matches the cookie. Thus he'll never been able to access the action method and perform the money transfer.
Limitations and Assumptions
Unfortunately the Anti Forgery Token is not a full proof solution. It involves a number of things in which to work.
- Cookies are required.
- Only works with POST requests. Though GET should really only be used for reading not updating.
- The are some Cross Site Scripting tricks that can bypass it. I won't mention them here.
Thanks for reading...