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 (25)

  1. Radu Enuca says:

    AllowAnonymous – simple and elegant

    Thanks for the article

  2. Andrei Ignat says:

    Please show also

    1. Roles based authorization

    2. How to integrate 1. with Active Directory

  3. Kevin Jensen says:

    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?

  4. Lee Dumond says:

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

  5. WestLABoy says:

    I changed the controller actions a little for my MVC.NET 3 application, such that the login page goes to   "http://www.site.com/Login&quot; rather than "http://www.site.com/…/LogOn&quot;, 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?

  6. WestLABoy says:

    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

    http://www.asp.net/…/mvc3-release-notes

  7. Marius Schulz says:

    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.

  8. Gareth Wood says:

    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. 🙂

  9. Kyle G. says:

    Well done sir. Very well explained.

  10. Chris Marisic says:

    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.

  11. Ashraful.NET says:

    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.

  12. Johan says:

    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

  13. Pat says:

    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?

  14. Nick says:

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

  15. afr0 says:

    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?

  16. Pete says:

    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.

  17. Timothy says:

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

  18. Jeff says:

    One potential security issue I've found is that to get this solution to work correctly you need to remove the <authorization><deny users="?" /> entry from the web.config, otherwise if you aren't authorized you still get redirected to the login page before the AuthorizationAttribute code is executed.  

    By removing that it opens up any handlers from being secured so anyone could get to those handlers without being logged in, including web crawlers.  This presents a huge security hole depending on what handlers you have open.  I saw it because of Glimpse and Elmah which show server variables on the client and must be secured (see http://www.troyhunt.com/…/aspnet-session-hijacking-with-google.html).  

    I fixed it by adding a <location> tag in the web.config for each handler and added <authorization><deny users="?" /> to it to make sure a user was logged in before accessing those pages.

    Is there a better way to handle this?  Possibly to block all handlers unless a user is logged in?

  19. SanjaySutar says:

    Awesome stuff. Very precise and highly rich information. Thanks for the article.

  20. Mohammad says:

    This is an excellent article

    Thanks

  21. Andrew C says:

    Super, simple stuff.  a very elegant solution.

  22. Thanks says:

    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!!

  23. JTech says:

    Just a small editing issue:

    I could be wrong but it seems that the first two points in "Limitation of the LogonAuthorize filter approach" are mute because the check for the AccountController is done with the 'is' operator…not on a magic string matching.

    Therefore, if someone was to change the name of the AccountController, then the Filter code would no longer compile and one would easily know to update it…

    Furthermore, if you have multiple Areas, each with an AccountController, you would be forced to use their namespaces to specify exactly which controller to Whitelist.

  24. Anonymous says:

    Excellent Article.  You really put some thoughts to this