
Preventing Cross-site Request Forgery (CSRF) Attacks Using ViewState
This article describes what Cross-site request forgery attacks are and how to mitigate them.
Cross-site request forgery (CSRF) is "a type of malicious exploit of a website whereby unauthorized commands are transmitted from a user that the website trusts ->". A CSRF attack typically forces users to execute unwanted actions while they are logged into a trusted website. What basically happens with CSRF is that a bad guy lures your website's users to a webpage he controls. By crafting that page in specific way the attacker tricks the user's browser into sending requests to your website. When that user is logged in to your website at that time (or your site has a remember me option), the request is executed in the context of that user.
Wikipedia has a good example of a CSRF attack where the attacker uses an HTML image tag to transmit a command in the context of the logged in user:
<img src="http://bank.example/withdraw?account=bob&amount=1000000&for=mallory">
For this attack to be successful, the bank.example website must allow execution of commands through HTTP GET operations. One of the easiest ways in preventing CSRF attacks is preventing any mutations through GET operations. Only post backs should be able to trigger any transaction or change any state. This eliminates this particular type of CSRF attacks.
Disallowing GET for mutations however, is not enough. There is a second type of CSRF and it uses post backs. By creating a malicious (possibly hidden) form in a web page, the bad guy would trick the victim or the victim's browser into posting the form to your website. While it's easy to understand that PHP websites are vulnerable to this type of attack, sometimes .NET developers have the misconception that ASP.NET is safe, because the ViewState protects them. Well, the ViewState can protect you, but it doesn’t do so by default.
As you know, the ASP.NET ViewState is this block of Base64 encoded data, containing the state of a web page, sent to the browser for storage between two requests. This data will be sent back to the web server on each post back. With a normal configuration, the ViewState data is not encrypted and can be read by anyone who wishes to. However, because the ViewState contains a hash value (when EnableViewStateMac is set to true, which is the default), it's practically impossible for it to be tampered with. A post back will only succeed if the ViewState is of an exact form. The nature of CSRF attacks doesn't enable the attacker to steal the user's ViewState from a page. But what developers often don't realize is that the ViewState isn't user specific (by default). Therefore, an attacker with access to the website (for instance when he has a user account) can simply copy the page's ViewState and use it to build the HTML form of his malicious web page.
When that form is posted to the website from the victim's browser, the ASP.NET website will, as always, validate that ViewState. However, because that ViewState isn't tampered with, the ViewState's hash is still valid for that particular page and the page is executed. When the user is logged in at that time, the CSRF attack is successful.
Mitigating the risk
There are several ways to mitigate the risk. The easiest way is by using the Page's ViewStateUserKey property. You can set this property in the page's Init event. What will happen is that ASP.NET will use this value to generate the ViewState MAC (which is the hash for the ViewState). This means that when the user key changed on a post back, the validation for the MAC will fail and a HttpException will be thrown. The following example shows how to implement this protection in a web page:
protected void Page_Init(object sender, EventArgs e)
{
// Validate whether ViewState contains the MAC fingerprint
// Without a fingerprint, it's impossible to prevent CSRF.
if (!this.Page.EnableViewStateMac)
{
throw new InvalidOperationException(
"The page does NOT have the MAC enabled and the view" +
"state is therefore vulnerable to tampering.");
}
this.ViewStateUserKey = this.Session.SessionID;
}
There are a few short comes to this solutions however. First of all, while using a SessionID is very safe, this means post backs wouldn't survive a AppDomain recycle. So it might be better to use the user name as user key. This however makes the ViewState valid for an infinite amount of time, which might be a risk that's to great. And while this mechanism works with pages that are protected by a login, it has some issues with public pages.
Public pages can be accessed by people without a user name (or who's user name isn't known yet). The anonymous user name (presumably an empty string) will be used as ViewStateUserKey and will therefore be used by ASP.NET to generate the MAC. However, when a user posts back that page after she logged in (for instance when the user opened multiple tabs in her browser) the user name changed and the MAC validation fails.
When you expect your users to work with multiple tabs, and have a site that uses both public as private pages, it prevents you from using the presented code in a base page from which all pages in your application inherit. The mechanism might be fine though when defining a 'SecureBasePage' that implements this mechanism. All pages that must be protected by login can inherit from this page.
This mechanism doesn't help you in protecting public pages. However, we should question the use of that. Public pages are accessible by everybody and they therefore shouldn't be able to change your application's state in the first place (except perhaps for things like a poll). Trying to protect something that's insecure by definition might be strange. Of course you have to think about what pages and data you give public exposure, but that is something you will hopefully find in the functional specs and it's not specific to CSRF. Also be careful by using public pages that add functionality once a user is logged in. It's hard to protect those type of pages against CSRF.
A different approach
A few months ago, when helping a client of mine with some security issues in one of their web applications, I implemented a different approach. The CSRF prevention mechanism had to be implemented in the base page, but the system was based on a internal framework that used one single aspx page for all its functionality (both public and login protected) in the application (mostly based on dynamic loading of user controls). This basically rendered the use of the ViewStateUserKey useless. What I came up with was an alternative mechanism, that stored the user name and a DateTime in the ViewState (in the control state to be more precise). This allowed the ViewState to be user specific and have a expiration time that was beyond a possible AppDomain recycle. Next, because the user name wasn't used to generate the ViewState MAC, I was able to do an alternative check and throw a more specific exception message on failure. I also allowed the validation to succeed when the user name, stored in the ViewState, or the currently logged in user name where unknown. This allowed the system to work properly when users posted back public pages with a login or logout in between. The solution looked much like this:
using System;
using System.Security;
using System.Web.UI;
public abstract class CsrfBasePage : System.Web.UI.Page
{
// Expiration time of 12 hours.
private static readonly TimeSpan ExpirationTime =
new TimeSpan(0, 12, 0, 0);
private string controlStateUserName;
private DateTime controlStateGenerationDate;
protected override void OnInit(EventArgs e)
{
this.RegisterRequiresControlState(this);
base.OnInit(e);
}
protected override void LoadControlState(object savedState)
{
Pair controlStatePair = (Pair)savedState;
Pair csrfData = (Pair)controlStatePair.First;
this.controlStateUserName = (string)csrfData.First;
this.controlStateGenerationDate = (DateTime)csrfData.Second;
base.LoadControlState(controlStatePair.Second);
}
protected override void OnLoad(EventArgs e)
{
this.PreventPostbackCSRF();
base.OnLoad(e);
}
protected override object SaveControlState()
{
// The control state is used to store user name and date time.
// Control state is part of the view state, but it's impossible
// to disable it, so this solution will also work when the
// viewstate is disabled.
string currentUserName = UserRepository.GetCurrentUserName();
DateTime controlStateGenerationTime = DateTime.Now;
Pair csrfData =
new Pair(currentUserName, controlStateGenerationTime);
return new Pair(csrfData, base.SaveControlState());
}
private void PreventPostbackCSRF()
{
// Validate whether ViewState contains the MAC fingerprint
// Without a fingerprint, it's impossible to prevent CSRF.
if (!this.Page.EnableViewStateMac)
{
throw new InvalidOperationException(
"The page does NOT have the MAC enabled and the view" +
"state is therefore vurnerable to viewstate tampering.");
}
if (this.IsPostBack)
{
string currentlyLoggedInUserName =
HttpContext.Current.User.Identity.Name;
ValidateUserName(currentlyLoggedInUserName);
ValidateGenerationDate();
}
}
private void ValidateUserName(string loggedInUser)
{
bool userIsValid = this.controlStateUserName == loggedInUser;
bool pageIsPublic = this.IsCurrentPagePublic(loggedInUser);
if (!userIsValid && !pageIsPublic)
{
string message = string.Format(
"A possible Cross-site Request Forgery attack " +
"is detected. ViewState was generated by user " +
"'{0}', but the current user is '{1}'.",
controlStateUserName, loggedInUser);
throw new SecurityException(message);
}
}
private bool IsCurrentPagePublic(string loggedInUser)
{
// Because it's impossible to tamper with the view state, when
// either the control state user name or the actual username
// are null or empty, the current page must be a public page.
return String.IsNullOrEmpty(this.controlStateUserName) ||
String.IsNullOrEmpty(loggedInUser);
}
private void ValidateGenerationDate()
{
if (this.IsViewStateTooOld)
{
throw new SecurityException("The ViewState is expired.");
}
}
private bool IsViewStateTooOld
{
get
{
DateTime expirationTime =
this.controlStateGenerationDate + ExpirationTime;
return expirationTime < DateTime.Now;
}
}
}
While the implementation with the ViewStateUserKey is much easier, the code above fixes the short comes of the ViewStateUserKey and will hopefully serve some of you.
UPDATE 2010-11-18: Please use this post for its education value. Instead of using the article's code however, please visit the AntiCSRF project on CodePlex. It use a much cleaner approach that doesn't depend on inheriting from a base class.
- ASP.NET, C#, Security - No comments / No trackbacks - § ¶