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.