Udostępnij za pośrednictwem


Securing your ASP.NET MVC 3 Application

Executive Overview

You cannot use routing or web.config files to secure your MVC application. The only supported way to secure your MVC application is to apply the [Authorize] attribute to each controller and action method (except for the login/register methods). Making security decisions based on the current area is a Very Bad Thing and will open your application to vulnerabilities. 

In ASP.NET MVC 2, it was recommended that you create a base controller with an [Authorize] attribute, and derive each controller (except the Account/Login controller) from that base class. That strategy has one big flaw: nothing prevents you from adding a new controller that doesn't derive from the [Authorize] protected base controller. Another approach for ASP.NET MVC 2 was to apply the AuthorizeAttribute to just the specific controllers or actions that need to be secured. The flaw with selectively applying the AuthorizeAttribute: it's easy to forget to add the AuthorizeAttribute to new controllers or action methods.

ASP.NET MVC 3 introduces global filters, so you can add the AuthorizeAttribute filter to the global.asax file to protect every action method of every controller.

 public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
    filters.Add(new AuthorizeAttribute()); 
    filters.Add(new HandleErrorAttribute());
}

The problem with applying Authorize globally is that you have to be logged on (authorized) before you can log on or register. What we need is a mechanism to opt out of authorization on the Logon and Register methods of the Account controller. We can do this by creating a filter that derives from AuthorizeAttribute , which runs the Authorize filter on every controller except the Account controller. The following code shows the implementation of our selective authorize filter.

 public class LogonAuthorize : AuthorizeAttribute {
     public override void OnAuthorization(AuthorizationContext filterContext) {
         if (!(filterContext.Controller is AccountController))
             base.OnAuthorization(filterContext);
     }

}

 Now we need to register the filter in global.asax as follows.
 public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
    filters.Add(new LogonAuthorize()); 
    filters.Add(new HandleErrorAttribute());
}

Now, any controller (not named account) is protected by the [Authorize] attribute. 

Limitation of the LogonAuthorize filter approach


  • If the account controller is renamed, it won’t be excluded. That’s not a security risk, as no one will be able to log on and you’ll quickly find the problem.
  • If you have multiple areas, all account controllers  will be exempted from authorization.
  • Nothing prevents someone from adding an action method to the Account controller that skips authorization.

You can remedy the last bullet by adding a line to make sure the action method is either Logon, LogOff or Register. The account controller already contains HTTP POST and GET methods for ChangePassword that are protected with the [Authorize] attribute. The code below shows the whitelist checking of the account controller.

 public class LogonAuthorize : AuthorizeAttribute {
     public override void OnAuthorization(AuthorizationContext filterContext) {
         if (!(filterContext.Controller is AccountController))
             base.OnAuthorization(filterContext);

         if ((filterContext.Controller is AccountController) &&
             !AccountControllerWhiteList(
                 filterContext.RequestContext.RouteData.Values["action"].ToString())
             )
             base.OnAuthorization(filterContext);

     }

The magic string whitelist approach above is not clean and requires changing your code in two places when you add an opt-out method.

Levi did the security review of my sample and came up with a better/cleaner approach. Instead of having the filter explicitly whitelist types, have those types or methods explicitly whitelist themselves.  For example:

 [AllowAnonymous]
 public ActionResult LogOn() {
        
 [HttpPost]
[AllowAnonymous]
 public ActionResult LogOn(LogOnModel model, string returnUrl) {
   
 [AllowAnonymous]
public ActionResult Register() {
       
 [HttpPost]
[AllowAnonymous]
public ActionResult Register(RegisterModel model) {

To implement Levi’s whitelist approach, create an AllowAnonymous attribute.

 using System;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AllowAnonymousAttribute : Attribute { }

Decorate the action methods that need to opt out of authorization with the AllowAnonymous attribute.

The new LogonAuthorize filter is shown below:

 using System.Web.Mvc;
using MvcGlobalAuthorize.Controllers;

namespace MvcGlobalAuthorize.Filters {
    public sealed class LogonAuthorize : AuthorizeAttribute {
        public override void OnAuthorization(AuthorizationContext filterContext) {
            bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)
            || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true);
            if (!skipAuthorization) {
                base.OnAuthorization(filterContext);
            }
        }
    }
}

Now all actions that aren’t opted out with the [AllowAnonymous] attribute automatically require login.  This is more granular than the original proposal, because you can opt out individual actions rather than entire controllers.  For example, ChangePassword() still requires authentication, but Logon() and Register() don’t.  You can also apply [AllowAnonymous] to the entire controller to opt out all methods.

Levi likes this approach because the whitelist is maintained on the actual types that are meant to be opted-out, which makes it more obvious when looking at the AccountController that it’s treated specially from a security perspective.  All three of the limitations listed above with my first approach are taken care of by this improved pattern.

You can download the sample here.

Am I Safe Now?

ASP.NET applications configured for forms authentication use an authentication ticket that is transmitted between web server and browser either in a cookie or in a URL query string. The authentication ticket is generated when the user first logs on and it is subsequently used to represent the authenticated user.  It contains a user identifier and often a set of roles to which the user belongs. The browser passes the authentication ticket on all subsequent requests that are part of the same session to the web server. Along with the user identity store, you must protect this ticket to prevent compromise of your authentication mechanism.

Failing to properly protect forms authentication is a common vulnerability that can lead to the following:

  • Elevation of privileges. An attacker could elevate privileges within your application by updating the user name or the list of roles contained in the ticket prior to posting it back to the server. An attacker who can upload malicious code to your application can also successfully create and modify the form’s authentication tickets.

  • Session hijacking. An attacker could capture another user's authentication ticket and use it to access your application. There are a number of ways that this could happen:

    • As a result of a cross-site scripting vulnerability.
    • If the transport is not being protected using a security mechanism such as Secure Sockets Layer (SSL).
    • If the ticket is stored in the browser cache.
  • Session usage after sign-out. Even after the user has logged out of the application and the application has called FormsAuthentication.SignOut, the authentication ticket remains valid until its time-to-live (TTL) expires, so it can be used by an attacker to impersonate another user.

  • Eavesdropping. An attacker could look inside a form’s authentication ticket to obtain any sensitive information it contains and use this information to compromise your application.

  • Compromise of the user identity store. An attacker with access to the user identity store may obtain access to user names and passwords, either directly from the data store or by using a SQL injection attack.

To protect against these threats, you can apply the RequireHttpsAttribute  to the global filters collection in the global.asax file.

 public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
    filters.Add(new LogonAuthorize());
    filters.Add(new RequireHttpsAttribute());  
    filters.Add(new HandleErrorAttribute());
}

Many web sites log in via SSL and redirect back to HTTP after you’re logged in, which is absolutely the wrong thing to do.  Your login cookie is just as secret as your username + password, and now you’re sending it in clear-text across the wire.  Besides, you’ve already taken the time to perform the handshake and secure the channel (which is the bulk of what makes HTTPS slower than HTTP) before the MVC pipeline is run, so redirecting back to HTTP after you’re logged in won’t make the current request or future requests much faster.  For information on setting up SSL on ASP.NET MVC, see my blog entry Better, Faster, Easier SSL testing for ASP.NET MVC & WebForms.

Other Approaches

Phil has an interesting and more flexible approach on his blog entry Conditional Filters in ASP.NET MVC 3. I like my approach better because it’s simpler; you only need to derive from RequireHttpsAttribute and register the new filter globally. Phil's approach requires you write and register a custom filter provider. Additionally, my approach has passed a security audit.

What’s the best way to secure a MVC application from anonymous users? A customer on the MVC Forum asked this question.  The first suggestion was the traditional ASP.NET WebForms approach; add a web.config to the folder you want to restrict. MVC uses routes and does not map URLs to physical file locations such as WebForms, PHP and traditional web servers. Therefore, using web.config will definitely open a security hole in your site.

The second suggestion was restriction of routes via route constraints. One of the tenets of the MVC pattern is maintainability. Even if you could prove a simple MVC application was secure via routes, any new methods or controllers added to the application would compound the complexity of proving your application is secure. Levi (the security expert on the MVC team) wrote:

Do not use a route constraint!

Let me be perfectly clear on this. The only supported way of securing your MVC application is to have a base class with an [Authorize] attribute, and then to have each controller type subclass that base type.  Any other way will open a security hole.

In general, it's extremely difficult to figure out all of the possible controllers that a particular route can hit.  Even if you think that a route can be used only to hit one particular controller or group of controllers, a user can probably feed some set of inputs to the system and direct it to a controller it wasn't intended to hit.

This is why we say that Routing should never be used to make a security decision.  Routing is essentially a communication channel with your application; it's a way to make your URLs pretty.  Because the controller is the resource you're actually trying to protect,  any security decisions should be done at the controller level rather than at the route level.  And currently the only way to associate a security decision with a controller is to slap an [Authorize] attribute on it (or another similar attribute that you write that subclasses it).

For example, assume you have a FooController inside your Blog area.  Normally, you would access this via /Blog/Foo/Action.  However, with the default {controller}/{action}/{id} route, you could also probably access this using just /Foo/Action (without the Blog prefix).  You may or may not be able to repro this on your own machine depending on your route configuration, but it's one of many examples.

Additionally, what happens if in a theoretical future version of MVC we add a handler MvcActivation.svc that is specifically meant to make your MVC application easier to consume by a WCF client?  Because this wouldn't go through routing at all, any decisions made at the routing level would not affect this.  Remember, the controller is the resource you want to protect.  It doesn't matter how you get there —via a route, a WCF activation path, or some other external component calling into the controller directly—the controller should secure itself.

Thanks to Levi for explaining this.

Excellent ASP.NET MVC Security Links.

Download the sample here.

Comments

  • Anonymous
    May 02, 2011
    AllowAnonymous - simple and elegant Thanks for the article

  • Anonymous
    May 02, 2011
    Please show also

  1. Roles based authorization
  2. How to integrate 1. with Active Directory
  • Anonymous
    May 03, 2011
    Great article first of all Rick!  I'm doing this exact approach on my web application.  One question I'm looking for best practices on.  In my authorize filter, I'm making a call to the database to get the Tenant based on url.  http://tenantid.mydomain.com and also pulling the User object from the database to populate the custom Principal object.  Is there a better way than adding 2 db calls to every single action?

  • Anonymous
    June 10, 2011
    Unfortunately, the longer lines in your code samples are being cut off here, which makes them of little use.

  • Anonymous
    July 20, 2011
    I changed the controller actions a little for my MVC.NET 3 application, such that the login page goes to   "www.site.com/Login" rather than "www.site.com/.../LogOn", and then implemented the technique as shown here. While it works well to block routes, I seem to have run into a problem redirecting users to my "~/Login" page. The redirect instead goes to "~/Account/Login" (note: NOT "~/Account/LogOn" --  so, something is going well). My Web.Config <forms> tag reads as follows: <forms loginUrl="~/Login" timeout="2880" /> Is the "~/Account/" route "hard-coded" somehow into the [AllowAnonymous] attribute?

  • Anonymous
    July 21, 2011
    I found the solution to my issue. It has nothing to do with this article. It turns out that the web.config property <forms loginUrl="~/Login" timeout="2880" /> no longer works for MVC.NET 3 applications. (Apparently this is a "known issue.") Instead, you have to insert a new key under <appSettings>: <add key="loginUrl" value="~/Login" /> More here: stackoverflow.com/.../mvc-forms-loginurl-is-incorrect www.asp.net/.../mvc3-release-notes

  • Anonymous
    October 01, 2011
    You should definitely consider using the FluentSecurity package offered on NuGet. It offers a clean and flexible approach on how to configure the security restrictions of different controllers.

  • Anonymous
    October 05, 2011
    Thank you. It's really informative. I'm looking for another great tutorial to build my asp.net mvc 3 site. I will wait another great post from you. I have found you and this site too, windows2008hosting.asphostportal.com. Really helpfull too. :)

  • Anonymous
    November 19, 2011
    Well done sir. Very well explained.

  • Anonymous
    December 21, 2011
    I changed the skipAuthorization slightly int[] ClientErrors = new[] {400, 404, 406, 408, 410, 411, 412, 413, 414, 417, 418, 426, 428, 429, 431, 449}; bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof (AllowAnonymousAttribute), true)    || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof (AllowAnonymousAttribute), true)    || ClientErrors.Any(err => err == filterContext.HttpContext.Response.StatusCode) This will allow requests to get to the error page properly for anonymous users. This is in no way should reduce security as you should not be serving content for those other than perhaps a custom error page with the error prettified.

  • Anonymous
    January 04, 2012
    What’s the best way to secure a MVC application from anonymous users? While you suggested 2 approaches, but both of them are not recommend. Since putting security attribute on every single action would be a maintenance nightmare, a better solution in this regard would be really appreciated.

  • Anonymous
    February 06, 2012
    Great article! I have one problem though, how do I allow sitemap.xml to be accessed by anonymous users? Today I have put it under my homecontroller and use [allowanonymous] on the action, but Id like to have it directly in my root of the webpage.. BR Johan

  • Anonymous
    February 09, 2012
    I'm using the Authorize attribute on my controller, however, this doesn't work well in the development environment as you run into a loopback situation and cannot logon.   How do you handle development while using the Authorize attribute?

  • Anonymous
    February 21, 2012
    From the looks of it, this functionality will be baked into MVC4 when it's released, it's present in the recently released Beta.

  • Anonymous
    February 27, 2012
    hi, I'm having a problem while implementing this, just wondering if anyone else facing the same issue? I've explained my problem stackoverflow.com/.../mutliple-controller-calls-in-asp-net-mvc3 here as well, but couldn't get any answer. can someone tell me is it the desired behavior or what?

  • Anonymous
    March 06, 2012
    Please could you expand on this uncomfortable advice? Amazon.com, for example, does this. >>> Many web sites log in via SSL and redirect back to HTTP after you’re logged in, which is absolutely the wrong thing to do.  Your login cookie is just as secret as your username + password, and now you’re sending it in clear-text across the wire.

  • Anonymous
    March 18, 2012
    Thanks, I put in the AllowAnonymousAttribute but had to remove it with the MVC 4 update as it is present in the framework now.

  • Anonymous
    May 16, 2012
    The comment has been removed

  • Anonymous
    June 21, 2012
    Awesome stuff. Very precise and highly rich information. Thanks for the article.

  • Anonymous
    July 26, 2012
    This is an excellent article Thanks

  • Anonymous
    October 13, 2012
    Awesome article.

  • Anonymous
    September 28, 2013
    Super, simple stuff.  a very elegant solution.

  • Anonymous
    January 07, 2014
    I made a custom filter attribute to check if accounts were approved and belongs to certain roles.. but if an Action/Controller was decorated with AllowAnonymous my Action filter ignored it and still re routed to denied pages. Was looking for that code with bool skipAuthorization for hours. THANKS!!

  • Anonymous
    April 17, 2014
    The comment has been removed

  • Anonymous
    May 15, 2015
    Excellent Article.  You really put some thoughts to this