Anti ForgeryToken in ASP.NET MVC
To prevent Cross-Site Request Forgery (CSRF) in ASP.NET MVC
applications we use AntiForgeryToken () helper.
Before that, we’ll have a look on how CSRF worksImagine you have an ASP.NET MVC’s controller class as follows
public class UserProfileController
: Controller
{public ViewResult Edit() { return View(); }
public ViewResult SubmitUpdate()
{
// Get the user's existing profile data (implementation omitted)
ProfileData profile = GetLoggedInUserProfile();
// Update the user object
profile.EmailAddress = Request.Form["email"];
profile.FavoriteHobby = Request.Form["hobby"];
SaveUserProfile(profile);
ViewData["message"] = "Your profile was updated.";
return View();
}
}
This is all very normal. First, the visitor goes to Edit(),
which renders some form to let them change their user profile details.
Secondly, they post that form to SubmitUpdate(), which saves the
changes to their profile record in the database. There’s no XSS vulnerability
here. We implement this sort of thing all the time.
Imagine that an attacker sets up the following HTML page and
hosts it on some server of their own:
<body onload="document.getElementById('fm1').submit()">
<form id="fm1" action="http://yoursite/UserProfile/SubmitUpdate" method="post">
<input name="email" value="hacker@somewhere.evil" />
<input name="hobby" value="Defacing websites" />
</form>
</body>
When this HTML page loads, it submits a valid form post to /UserProfile/SubmitUpdate on your
server. Assuming you’re using Windows authentication or some kind of
cookie-based authentication system such as Forms Authentication, the automated
form post will be processed within the victim’s established authentication
context, and will successfully update the victim’s email address to something
under the attacker’s control. All the attacker has to do now is use your
“forgotten password” facility, and they’re taken control of the victim’s
account.
Ways to stop CSRF
1.
Check that incoming requests have a Referer header
referencing your domain. This will stop requests unwittingly submitted from a
third-party domain.
2.
Put a user-specific token as a hidden field in
legitimate forms, and check that the right value was submitted. Instead,
it’s better to use some random value (such as a GUID) which you’ve stored in
the visitor’s Session collection or into a Cookie.
We can implement security while we submit the ASP.NET MVC Page
to the server. For this, ASP.NET MVC provides an excellent mechanism to
prevent CSRF, i.e., Anti Forgery Token:
- The server
prints tokens to cookie and inside the form
- When the form is
submitted to server, token in cookie and token inside the form (hidden
type) are sent in the HTTP request
- Server validates
the tokens
HtmlHelper.
AntiForgeryToken()
would be used to generate a token inside the form and also
writes it to Cookie.
<% using(Html.Form("UserProfile",
"SubmitUpdate")) { %>
<%= Html.AntiForgeryToken() %><% } %>
HTML source
<form action="/UserProfile/SubmitUpdate" method="post"><input name="__RequestVerificationToken" type="hidden" value="saTFWpkKN0BYazFtN6c4YbZAmsEwG0srqlUqqloi/fVgeV2ciIFVmelvzwRZpArs" />
<!-- rest of form goes here -->
</form>
At the same time, Html.AntiForgeryToken() will
give the visitor a cookie called __RequestVerificationToken, with the same value as the random hidden value
shown above
When the form is submitted, cookie and the hidden
value are both sent to server.
On the server side, [ValidateAntiForgeryToken] attribute is used to specify the controllers or actions to
validate them
[ValidateAntiForgeryToken]
public ViewResult SubmitUpdate(){
…..
}
Authorization filter that checks that:
·
The incoming request has a cookie called __RequestVerificationToken
·
The incoming request has a Request.Form entry
called __RequestVerificationToken· These cookie and Request.Form values match
Assuming all is well, the request goes through as
normal. But if not there’s an authorization failure with message “A required
anti-forgery token was not supplied or was invalid”.
[ValidateAntiForgeryTokenWrapper(HttpVerbs.Post)]
[AcceptVerbs(HttpVerbs.Post)]public ActionResult FarmingResults(FarmingRequestData data)
{
return this.GetFarmingResults(data);
}
One single
[Validate
AntiForgeryToken]
attribute is expected to declare on controller,
but actually a lot of attributes have be to declared on controller's each POST
actions. Because POST
actions are usually much more than controllerspublic partial class DivisionAdminController : SecureController
{
private const string DIVISION_VIEW = "Division";
private const string MASTERLIST_VIEW = "MasterProductList";
. . . .
Usually a controller contains both actions for HTTP
GET
requests and actions for POST
, and, usually validations are expected for only HTTP POST
requests. So, if the [Validate
AntiForgeryToken]
is declared on
the controller, the HTTP GET
requests become invalid
To avoid a large number of
[Validate
AntiForgeryToken]
attributes
(one for each POST
action), the following Validate
AntiForgeryTokenWrapperAttribute
wrapper class can be helpful, where HTTP verbs (GET and POST) can be specified.
File path: Source\PresentationFramework\Security\ValidateAntiForgeryTokenWrapperAttribute.cs
namespace PresentationFramework.Security
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
public class ValidateAntiForgeryTokenWrapperAttribute : FilterAttribute, IAuthorizationFilter
{
private readonly ValidateAntiForgeryTokenAttribute _validator;
private readonly AcceptVerbsAttribute _verbs;
public ValidateAntiForgeryTokenWrapperAttribute(HttpVerbs verbs)
: this(verbs, null)
{
}
.. . . . . . .
Please refer the above file for complete code.
Due to above code, GET actions are not affected. Only
HTTP POST requests are validated.
In case you want to protect multiple forms in your application independently of each
other, you can use a “salt” value
when you call Html.AntiForgeryToken(),
<%=
Html.AntiForgeryToken("Some_String_Srinivas")
%>[ValidateAntiForgeryToken(Salt="Some_String_Srinivas")]
public ViewResult SubmitUpdate()
{
// ... etc
}
Salt is just an random string.
A different salt value means a different anti-forgery token will be generated.
This means that even if an attacker manages to get hold of a valid token
somehow, they can’t reuse it in other parts of the application where a
different salt value is required.
By default, the salt should be a compile time
constant, so it can be used for the
[Validate
AntiForgeryToken]
or [Validate
AntiForgeryTokenWrapper]
attribute.Non-constant Salt in Runtime
One Web product might be sold to many clients. If a constant salt is evaluated in compile time, after the product is built and deployed to many clients, they all have the same salt. Of course, clients do not like this. Some clients might even expect a configurable custom salt. In these scenarios, salt is required to be a runtime value.
As mitigation, we pass Salt parameter in ValidateAntiForgeryTokenWrapperAttribute’s constructor
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple=false, Inherited=true)]
public class
ValidateAntiForgeryTokenWrapperAttribute : FilterAttribute, IAuthorizationFilter{
private readonly ValidateAntiForgeryTokenAttribute _validator;
private readonly AcceptVerbsAttribute _verbs;
public
ValidateAntiForgeryTokenWrapperAttribute(HttpVerbs
verbs)
: this(verbs,
null){
}
public ValidateAntiForgeryTokenWrapperAttribute(HttpVerbs verbs, string
salt)
{
this._verbs
= new AcceptVerbsAttribute(verbs);
this._validator
= new ValidateAntiForgeryTokenAttribute(){
Salt = salt
};
}
. . . .
AJAX calls through
jQuery
Basically, the tokens must be printed to browser then sent back to
server. So first of all,HtmlHelper.AntiForgeryToken() need to
be called somewhere. Now the browser has token in both HTML and cookie.
jQuery must find the printed token in the HTML, and append token
to the data before sending:
function DeletePropertyProduct(state,
gProductPriceID) {
$.post(_deletePropertyFees, { State: state, ProductPriceID:
gProductPriceID, __RequestVerificationToken:
getAntiForgeryToken() },function (data) {
$("#PropertyFeesContainer").html(data);
}
);
To reuse the same, this can be encapsulated into a separate
jQuery plugin as below (Reference: http://www.codeproject.com/Articles/267694/Security-in-ASP-NET-MVC-by-using-Anti-Forgery-Toke)
///
(function ($) {
$.getAntiForgeryToken = function (tokenWindow, appPath)
{
tokenWindow = tokenWindow &&
typeof tokenWindow === typeof window ? tokenWindow : window;
appPath = appPath && typeof appPath === "string" ? "_" + appPath.toString() : "";
var tokenName = "__RequestVerificationToken" + appPath;
var inputElements = tokenWindow.document.getElementsByTagName("input");
for (var i = 0; i < inputElements.length; i++) {
var inputElement = inputElements[i];
if (inputElement.type === "hidden" && inputElement.name === tokenName) {
return {
name: tokenName,
value: inputElement.value
};
}
}
};
$.appendAntiForgeryToken = function (data, token) {
if (data && typeof data !== "string")
{
data = $.param(data);
}
// Gets token from current window by default.
token = token ? token : $.getAntiForgeryToken();
// $.getAntiForgeryToken(window).
data = data ? data + "&" : "";
// If token exists, appends {token.name}={token.value} to data.
return token ? data + encodeURIComponent(token.name) +
"=" + encodeURIComponent(token.value) : data; };
// Wraps $.post(url, data, callback, type) for most common scenarios.
$.postAntiForgery = function (url, data, callback, type) {
return $.post(url, $.appendAntiForgeryToken(data), callback, type);
};
// Wraps $.ajax(settings).
$.ajaxAntiForgery = function (settings) {
var token = settings.token ? settings.token :
$.getAntiForgeryToken(settings.tokenWindow, settings.appPath);
settings.data = $.appendAntiForgeryToken(settings.data, token);
return $.ajax(settings);
};})(jQuery);
In most of the scenarios, it is
Ok to just replace
$.post()
invocation with $.postAntiForgery()
, and replace$.ajax()
with $.ajaxAntiForgery()
:$.postAntiForgery(url, {
productName: "Tofu", categoryId: 1}, callback);
// The same usage as $.post(), but token is posted.
There might be some scenarios
of custom token, where
$.append
AntiForgeryToken()
is
useful:data = $.appendAntiForgeryToken(data, token);
// Token is already in data. No need to invoke
$.postAntiForgery().$.post(url, data, callback);
And there are special scenarios
that the token is not in the current window. For example:
- An HTTP
POST
request can be sent from aniframe
, while the token is in the parent window or top window; - An HTTP
POST
request can be sent from an popup window or a dialog, while the token is in the opener window;
data = $.appendAntiForgeryToken(data, $.getAntiForgeryToken
(window.parent));// Token is already in data.
// No need to invoke $.postAntiForgery().$.post(url, data, callback);
$.ajaxAntiForgery({
type: "POST", url: url, data: {
productName: "Tofu", categoryId: 1 }, success: callback,
// The same usage as $.ajax(), supporting more options.
tokenWindow: window.parent
// Token is in another window.});
Limitations
of the Anti-Forgery helpers
1.
All genuine users
must accept cookies
2.
It only works
with POST requests, not GET requests
http://blog.stevensanderson.com/2008/09/01/prevent-cross-site-request-forgery-csrf-using-aspnet-mvcs-antiforgerytoken-helper/
http://www.codeproject.com/Articles/267694/Security-in-ASP-NET-MVC-by-using-Anti-Forgery-Toke
Comments