The cross-site request forgery (CSRF or XSRF or one-click) is a sneaky kind of attack that, unlike script or SQL injection, doesn’t really depend on something that developers may have done patently wrong. Your ASP.NET application can have a regular form that posts data within the fences of cookie authentication and it can also use model binding and data and request validation to fend off injection of potentially malicious data: even so, the posting form and the entire application is still at risk of being compromised. To be precise, the risk is not that the application data set is compromised but that one particular user account is hacked and owned by an outsider. The actual damage for the application then depends on the type and the power of the compromised account. CSRF is in the list of major threats of the OWASP organization. (See Cross-Site Request Forgery (CSRF))
The good news is that both ASP.NET MVC and ASP.NET Core provide effective tools that can protect your forms against CSRF attacks. The bad news is that you must enable those tools explicitly. However, in ASP.NET Core things are going to be better because CSRF protection is almost entirely on by default. Let’s find out more about the mechanics of the attack and defense strategy.
Mechanics of CSRF
The primary victim of the attack is the user, but because the hacker can impersonate the user—realistically only for a short period of time—some misbehavior may result also at the application level especially if the role of the hacked user includes several permissions.
Let’s suppose that the victim is regularly logged to the site and while logged she is allured to click on some link or visit some particularly attractive page. Let’s also suppose that the link points to a page with the following structure.
1 2 3 |
<body onload="postForm()"> <!-- Some attractive content --> </body> |
The script runs upon loading of the page and can create a form ‘on the fly’ to post to a known URL endpoint. The authentication cookie is sent when the form is posted, because the whole thing takes place on the victim’s computer, and the domain of the cookie matches the domain of the target server. To be harmful, however, the attack must target a URL that performs a sensitive operation such as changing the password or deleting some data. Besides this, the hacker must have discovered a fair amount about the internal structure of the site. However, the attack is definitely possible whether or not there is then any opportunity for harm
To defend against CSRF, you need to add additional and user-specific information to each and every form that the hacker can’t find out. Needless to say, the code on the server side must check that this additional information has not been tampered with.
Preventing CSRF Attacks in ASP.NET
ASP.NET MVC has offered a strong line of defense against CSRF attacks for a long while, but developers too often tend to forget to enable it. I believe the reason is that the defense requires two steps, and everybody is too busy and rushed to be able to stop and think about basic facts of security.
Any HTML form created out of Razor pages should include a call to the AntiForgeryToken HTML helper. The helper emits a hidden field and a cookie.
1 2 3 4 |
<form ...> @Html.AntiForgeryToken() <!-- Content of the form --> </form> |
The hidden field contains a randomly-generated binary blob that is 128 bytes long. The cookie contains the same binary blob but encrypted using the Data Protection API with the key kept in the Local Security Authority of the Windows operating system.
1 2 3 |
<input name="__<a id="post-71688-_Hlk487132269"></a>RequestVerificationToken" type="hidden" value="saTFWpk...c4YbZAm" /> |
The hidden field is not transmitted when the user clicks a link on an external web site, whereas the cookie could make it to the attacker’s site. However, because the content of the cookie is encrypted the hacker has no way to figure out the value for the hidden field and can’t forge a valid POST.
This defense strategy works just as long as the controller’s code that handles the POST double-checks that it is receiving a hidden field named __RequestVerificationToken and a cookie with the same name. If both are sent, then the code should decrypt the cookie’s content and match it to the content of the hidden field. If the two values won’t match, well, a security exception should be thrown. ASP.NET MVC offers an action method selector component for the job—the ValidateAntiForgeryToken attribute.
1 2 3 4 5 6 |
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Save(...) { ... } |
The attribute only works with POST requests. This is because GET requests should never perform any tasks that can alter the state of the system.
Preventing CSRF Attacks in ASP.NET Core
In ASP.NET Core, the core of the defense strategy is the same. The arsenal of tools, instead, is a bit more powerful. In particular, Microsoft attempts to hide from developers as much as possible of the effort to protect applications from CSRF. The AntiForgeryToken HTML helper is still there and works as usual. The ValidateAntiForgeryToken attribute is still there and works in the same way as in classic ASP.NET MVC. In other words, the same solution that did the job in older versions of ASP.NET MVC can still be used as-is in ASP.NET Core. In addition, though, ASP.NET Core features a few other solutions to offer especially to make the process of injecting the request verification token a bit more automatic and seamless.
The Razor engine in ASP.NET Core supports a new type of server-side component called a ‘tag helper’. A tag helper is invoked by the Razor parser in order to transform some custom markup elements and attributes into standard HTML elements and attributes. In the end, the output emitted is the same HTML that you could have coded yourself manually except that the syntax required to express it is more concise and readable. Let’s consider the following syntax for a FORM element.
1 2 3 4 5 6 |
<form class="form-horizontal" method="post" asp-controller="Account" asp-action="Register"> ... </form> |
Clearly, neither asp-action nor asp-controller are standard HTML attributes. However, they are recognized as tag helper attributes by Visual Studio and IntelliSense and, most importantly, by the Razor parser. ASP.NET Core comes with an entire library of tag helper components that a view can reference through the following @addTagHelper new directive:
1 |
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
All classes in the library that are tag helpers are decorated with a special attribute and are derived from a common base class. All elements with a linked tag helper enjoy a special treatment in Visual Studio and from the Razor parser. When the parser encounters these elements, it yields to the tag helper and tag helper has a chance to inspect the structure of the element and can edit its content. In particular, the asp-* attributes associated with the FORM element modify the action attribute of the FORM by setting it to the action resulting from the combination of the controller name (asp-controller attribute) and the action name (asp-action attribute). It doesn’t end here, though. By applying those attributes, you also tell the form tag helper to emit the anti-forgery hidden field and cookie. In ASP.NET Core, the name of the request verification token is different but role and content are just the same as in classic ASP.NET MVC. In other words, tag helpers automatically emit the token just for the cost of using tag helper attributes to define the action URL of the form.
Flavors of Anti-Forgery Token Attributes
The ValidateAntiForgeryToken attribute is not alone in ASP.NET Core. It is partnered by the AutoValidateAntiForgeryToken attribute, which does the same job except that it covers all potentially unsafe HTTP verbs and not just POST. It also covers, in fact, PUT, DELETE and PATCH. It doesn’t cover other verbs supposed to be used for read-only actions only. Interestingly, if you register the AutoValidateAntiForgeryToken attribute as a global filter, and use the asp-* attributes on all FORM elements then you’re very well protected against possible CSRF attacks without having to explicitly code for it each and every time.
1 2 3 4 |
services.AddMvc(options => { options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); }); |
If the AutoValidateAntiForgeryToken attribute is registered as a global filter, however it throws any time a FORM element, that has been created without the asp-* attributes, posts its content. The exception being thrown results in a HTTP 400 Bad Request code.
It’s a type of exception that you run into pretty soon during development and that is sort of an alarm bell that reminds you to use the tag helper attributes on the FORM element. However, if you have reasons to disable the verification of the token on a particular method on an unsafe HTTP verb, then you can use the IgnoreValidateAntiforgeryToken attribute instead.
Playing with the Referrer Header
The referrer HTTP header indicates the URL that requested the resource being currently served. In other words, it links the caller of the current page to the previous page. In a CSRF context, the referrer would contain the URL of the site that is actually placing the call. In other words, the URL of the attacker’s site. In light of this, an obvious conclusion would be that one would be able to easily fend off any CSRF attacks simply by checking the content of the referrer HTTP header. All that would do, in fact, is to check that the form was posted from the same site, if not from a specific page.
1 2 3 4 5 6 |
[HttpPost] public ActionResult Save(...) { // Check referrer content here ... } |
The problem with the referrer HTTP header is that it is considered an optional information and it is not guaranteed to be there all the time. Some browsers, in fact, allow users to disable referrers and sometimes that information might be stripped off by proxy servers. In addition, the HTML5 standard introduced the noreferrer attribute for anchor tags which instructs the browser to retain from setting the referrer header.
1 |
<a href="..." rel="noreferrer" /> |
Also consider that it is not a far-fetched idea to speculate that it could be spoofed especially if the request is forged outside a browser and set using a custom application as the HTTP client.
The bottom line is that for the purpose of defending a web site from CSRF one-click attacks, it is not sufficient to check the URL referrer’s HTTP header. If you’re looking for a full line of defense, then your best option is to use the verification token as discussed so far; especially in ASP.NET Core where there is so little coding effort required to use verification tokens.
Using Referrer Anyway
The referrer HTTP header, however, can be also quite helpful in slightly different circumstances. Suppose you have a set of HTTP endpoints—yes, let’s call it a web API—that you invoke via Ajax/JavaScript from the client side. Because they are an internal resource you sometimes just invoke them through ASP.NET controllers, subject to authentication and authorization rule. In other words, those endpoints will be invoked if a valid authentication cookie can be found.
It could be useful, though, just reinforce the security a bit by enforcing a given server to appear as the referrer. Here’s a simple action method selector that does just that. It’s part of a set of helper methods that I published at http://github.com/despos and on Nuget under the Youbiquitous.Mvc moniker.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class RequireReferrerAttribute : ActionMethodSelectorAttribute { public RequireReferrerAttribute(params string[] trustedServers) { TrustedServers = trustedServers; } public string[] TrustedServers { get; } public override bool IsValidForRequest( ControllerContext controllerContext, MethodInfo methodInfo) { var referrer = controllerContext.HttpContext.Request.UrlReferrer; if (referrer == null) return false; var list = new List<string>(TrustedServers); var uri = referrer.AbsoluteUri.ToLower(); return list.Any(ts => uri.StartsWith(ts.ToLower())); } } |
If no referrer is found, the method protected by this attribute is denied. Here’s how you would use the attribute.
1 2 3 4 5 6 |
[HttpPost] [RequireReferrer("http://yourserver.com", "http://www.yourserver.com")] public ActionResult Save( ... ) { ... } |
The Save method will only be invoked if the request comes from any of the listed servers.
A Few Words on IP Addresses
To wrap up the discussion, what about checking IP addresses? Unfortunately, IP addresses are not reliable either because it can be hidden in case of a serious attack. In addition, it might be difficult to find the exact IP address and map that to an authorized user. When running behind a proxy or a router, the HTTP context only reports the address of the router: Not to mention that a legitimate user might be using the site from a variety of different locations and connections. At any rate, while checking IP addresses cannot be considered a generally valid measure of protection for a web API, it still remains an option on the table for some specific situations. Here’s an example of how to get the IP address.
1 2 3 4 5 6 7 |
public string GetIP(HttpContext context) { var ip = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"]; if (String.IsNullOrEmpty(ip)) return context.Request.ServerVariables["REMOTE_ADDR"]; return ip; } |
Summary
Web security is always a hot, and rapidly-changing, topic. It is not one of those software concerns that can be added as an afterthought. It needs serious preliminary analysis for any site that needs good security. However, even for applications where security is not the primary concern, you can always implement some basic lines of defense with almost no cost that ensure that no abuse of the API can succeed: Or, at least, limit it as much as possible.
Load comments