6 Security Steps For Your ASP.NET MVC Web Site

The following are some best practices that should be considered and are easy to implement in ASP.NET MVC. I am not a security ‘guru’. In fact there is a lot about security that I am learning. New security risks / threats are created as hackers learn to exploit users (social hacking) and bad code. Not in any particular order of importance, here are a few things to consider.

1. Securing Content

If the content of your site is sensitive enough that you don’t want someone sniffing HTTP packets to intercept, then you must use encryption. If the content does not need to be decrypted on the client side (browser) then you can encrypt it your self before sending. In fact, many of the browser cookies are encrypted this way.

Another area where you will want to encrypt your data is password storage. Never store the password in clear text. Always store a hash of the password. This helps secure your user’s data if the password database is compromised.

2. Membership – Authentication / Authorization

These are two separate security tasks. Authentication deals with identifying who is using the site. This typically involves a login process. Once the user has logged in, then authentication relies on security tokens sent between the browser and the server. Stealing of security tokens or tricking the user’s own browser to use the security token for evil are two major sources of security holes that must be addressed. Authorization is usually the process of assigning users roles (Admin, User…etc) that allow access to certain features of the application.

ASP.NET MVC leverages the ASP.NET authentication / authorization framework. This was configured for Mr. Marky using the Web.Config file as follows:


    


    
        
        
    

The above configuration does two things. It sets the authentication mode to “forms” and then configures a controller / action to the login form. It then configures the membership provider for the web site. In this case I am using an XML based membership provider, but many others exist.

At this point, we have configured authentication, and now we must tell the APS.NET MVC framework when to force authentication. Don’t fall into the trap of configuring this using the Web.Config file as you do in ASP.NET. This opens up security holes. More information on this risk is covered here.

In ASP.NET MVC forcing authentication is correctly done using the “Authorize” attribute. Some controller / action combinations may not require authentication. For instance, placing the “Authorize” attribute on the “Account/LogOn” controller / action would not allow the user to reach the login view with out being authorized first. Not very useful! The “Authorize” attribute can be placed on an action method or on the controller class itself. If placed on the controller class, then all actions will require authorization. The following demonstrates this:

[Authorize]
public class TaskController : Controller
{
    /* code removed for brevity */
}

public class AccountController : Controller
{
    public ActionResult Login()
    {
    }

    [Authorize(Roles="Admin")]
    public ActionResult DeleteUser()
    {
    }

    [Authorize(Users="Bob")]
    public ActionResult DeleteAllUsers()
    {
    }

    [Authorize]
    public ActionResult DeleteMyData(Task task)
    {
        if(task.UserName == User.Identity.Name)
        {
            /* Code to delete user's data */
        }
        else
        {
            /* Invalid request */
        }
    }
}

In the above example, all action methods in the Task action controller will require authentication.

The “Authorize” attribute also takes parameters that allow you to narrow down access to specific users / roles. In the above example, the DeleteUser action on the Account controller requires the user to be a member of the “Admin” roles. The DeleteAllUsers action is requires the user name to be “Bob”.

The last example of the code above shows how you can filter the request to allow only the user to delete their own data. In this example, each data item contains a property indicating ownership. This is used to check against the current user.

3. Custom Filters

One major design goal of the APS.NET MVC framework extensibility. If you don’t like the default behavior of a feature, usually you are allowed to substitute your own. This is true of the “Authorize” attribute. Because this code is the front line of defense for your web site security caution must be taken when rolling your own security code.

For instance, one security measure is to ensure the request came from your site.  This can be accomplished using a customer filter. The following example is directly from the Professional ASP.NET MVC 1.0 book authored by Conery, Hanselman, Haack, and Guthrie.

public class IsPostedFromThisSiteAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext != null)
        {
            if (filterContext.HttpContext.Request.UrlReferrer == null)
            {
                throw new System.Web.HttpException("Invalid submission");
            }
            if (filterContext.HttpContext.Request.UrlReferrer.Host != "mysite.com")
            {
                throw new System.Web.HttpException("This form wasn't submitted from this site!");
            }
        }
        base.OnAuthorization(filterContext);
    }
}

You can then decorate your action with the “IsPostedFromThisSite” attribute to ensure the requested page referrer URL is your site. Here are a few other links on custom authentication attributes:

4. Don’t HTTP GET (anchors / links) To Modify Database Values

Stephen Walther has covered the dangers of this in a recent post.  Go read that for details of the security hole this introduces. The following code is an example that opens up this security hole:


    delete

Unfortunately, this is the markup that was in the first version of Mr. Marky that I recently posted. Just goes to show that we all benefit from continually review our code. Here is a better implementation that relies on a HTTP POST.

<% using (Html.BeginForm('DeleteTask', 'Task', new { task.Id })) { %>
    <%= Html.AntiForgeryToken() %>
    <input type="image" class="confirm_delete" src="<%= Url.Content('~/Content/Images/delete_hover.png') %>" />
<%} %>

Ignore for now the AntiForgeryToken markup as this will be covered in the next section. Here the delete feature is implemented using an input tag (of image type). This input tag causes the form to POST back.

The important bit to grasp is that the delete action is no longer called on a HTTP GET request. During an HTTP GET request, we have no opportunity to filter a request from a legitimate user from that of someone trying to do evil. The HTTP POST is more secure because we can embed data into the web site during the HTTP GET that can be used to discriminate against the evil doers.

5. Anti-Forgery Token

In the above delete task form, we injected into the mark up an anti-forgery token on the HTTP GET request. This is key to thwarting disingenuous requests on the HTTP POST. The following is the “DeleteTask” action method.

[AcceptVerbs(HttpVerbs.Post)]
[IsPostedFromThisSite]
[ValidateAntiForgeryToken]
public RedirectToRouteResult DeleteTask(Guid id)
{
    Task task = _taskRepository.Retrieve(id);
    if (task != null)
    {
        if (task.UserName == User.Identity.Name)
        {
            /* Delete code ... */
        }
        else
        {
            /* Invalid request ... */
        }
    }
    else
    {
        /* Invalid request ... */
    }
    return RedirectToAction("List");
}

This action method is decorated with the following attributes:

  • AcceptVerbs – This attribute allows the routing engine to differentiate between the HTTP GET and POST action methods of the same name.
  • IsPostFromThisSite – This attribute was covered in a previous section and validates the original HTTP GET came from the same site that is handling the HTTP POST.
  • ValidateAntiForgeryToken – This attribute examines the token we embedded on the HTTP GET to ensure it is valid.

6. Never Trust User Input

The first step on any posted forms should be to scrub user input. This should be done even if you have implemented the best client-side validation. Devious individuals could turn off JavaScript to allow the form to post content you were not expecting. Data should be boxed into the smallest allowed inputs. For instance if you are expecting numbers, scrub out all characters.

The most challenging content to scrub is markup and code (HTML, JavaScript, SQL). The best solution is to not allow any at all. Allowing some markup usually means that you risk your “white-list” (scrub by removing all but the allowed mark-up) or your “black-list” (scrub by removing only the not allowed mark-up) scrubber failing over some cleverly crafted content.

The risk is user input is redisplayed that contains HTML or JavaScript. This could allow a mischievous user to inject content into your page that compromises the security of your site. There are plenty of resources on the web for cross-site scripting (XSS) and cross-site request forgery (XSRF).

If you must allow markup and code (for instance a blog) then use the Html.Encode, Html.AttributeEncode and Url.Encode methods found in the ASP.NET MVC framework. Here is an example of using Html.Encode to scrub user input:

<%= Html.TextBox('Name', Html.Encode(Model.Name) ) %>

All data that is stored into the SQL database needs to be scrubbed also. Here is a great comic strip illustrating an example:

sql injection

Summary

These are a few tips that I am trying to keep in mind when writing web site code that needs to be secure. As I said in the opening paragraph, I am not a security guru. There are many aspects of security that I am continuing to learn. We all must take ownership of the code that we write. This includes making sure production code is as secure as possible.

Tags:,
Comments
  1. Neil N
    • rcravens
  2. Sami

Leave a Reply

Your email address will not be published. Required fields are marked *

*