Monday, October 5, 2009

A Safe, jQuery-less, Downlevel-friendly ASP.NET MVC Action Link Button

One gaping security hole in some web apps is the exposure of ‘Delete’ or other functionality via a url.  The problem goes something like this: suppose you have code that handles requests to myapp/controller/delete/1.  A hacker could trick you into deleting a record by sending you to a page with an image like: <img src=”myapp/controller/delete/1”/>  Your browser will connect, passing your authentication cookies, windows login, etc., to said page and it will execute whatever code it usually does, as you.   ‘Deletes’ are the most commonly discussed scenario, but you could have similar issues with routes that insert or update data. <img src=”http://mybank.com/transactions/sendmoney?to=hacker&amount=1000000”/>, if you catch my drift. 

The partial  fix is to use forms and require the request be a HTTP POST request.  This prevents the image-based attack above, but still allows for a slightly more complex attack where the hacker creates a form similar to yours and posts it to your server.  Web Forms has ways of handling this in most cases, so that it typically is not an issue, though it is completely possible to introduce similar bugs in such apps if you try.  In ASP.NET MVC, you’re given greater control over the rendered HTML, but  with great power comes great responsibility. Fortunately, MVC gives you the tools to handle these scenarios in the form of AntiForgeryValidationToken.  You can read up more on this class of vulnerability and the solution over on Steve Sanderson’s blog, but in a nutshell, the fix is to do this in your view:

<% using(Html.Form("Delete", "Delete")) { %>
       <%= Html.AntiForgeryToken() %>
       <!-- rest of form goes here –>
   <% } %>

And this in your controller:

[ValidateAntiForgeryToken]
  public ViewResult Delete()  

For any action method that updates, inserts, or deletes data, or does anything in the app besides just query data.  This causes a hidden field to be put in the form that contains a token that the server creates for the user’s session.  If not present in the form’s post, the action method will throw an exception.

This sounds easy enough, but in many cases, it sure is harder than just doing the old insecure thing:

<%=Html.ActionLink(“Delete”,”Delete”,new{id=item.id})%>

Faced with this in a recent app, I decided to write a helper method to make creating this sort of form a little more developer-friendly.  What I wanted was something like this:

<%=Html.ActionLinkForm(”Delete”, Url.Action(“Delete”, new{id=item.id}))%>

It’s slightly harder than you may think.  Creating the form is easy enough, but if you really want a link, a little javascript is in order to let the link submit the form.  However, the form then no longer supports non-javascript-enabled browsers, so some tweaks are required to address that.  I found several similar implementations, including a nice jQuery one from Phil Haack.  However, it seemed a little heavy to require jQuery.  Don’t get me wrong – I love jQuery and use it all the time, but something about requiring it in such a basic thing smells a bit to me.  I felt the same effect could be had with less dependencies. So, I built my own, making use of the <noscript> element, and a little creative plain-jane javascript:

public static string ActionLinkForm(this HtmlHelper helper, string linkText, string url)
{
   var result = new StringBuilder();
   result.AppendFormat("<form action=\"{0}\" method=\"post\">", url);
   result.Append(helper.AntiForgeryToken());
   result.AppendFormat("<script type=\"text/javascript\">document.write('<a href=\"#\" onClick=\"this.parentNode.submit()\">{0}</a>');</script>",linkText);
   result.AppendFormat("<noscript><input type=\"submit\" value=\"{0}\"/></noscript>", linkText);
   result.Append("</form>");
   return result.ToString();
}    

This requires no additional scripts on the page, or special css elements and is compatible with IE and FF.  With javascript off, the link turns into a button and the code works as it normally would.  I’m fairly certain it works in most other browsers as well, though you should test it in others before using this in production.

But Wait, There’s More!  Act Now, and Receive an Entity Framework Delete Link Button Absolutely Free!

The particular scenario I wanted to use this in was for a delete link inside MvcContrib’s awesome GridView helper.  My model used Entity Framework, and what I really wanted was this:

        column.For(x => Html.DeleteLink(x)).DoNotEncode();

There’s plenty not to like about EF in it’s current implementation, but one nice thing is that it exposes the primary keys via an interface that is usable inside your code.  With the convention of a link named ‘Delete’, that works against an action named ‘Delete’ on the same controller as the current request, and accepts the primary key fields as parameters, I came up with this implementation:

public static string DeleteLink(this HtmlHelper helper, IEntityWithKey entity)
{
    var routeValues = new RouteValueDictionary();
    foreach (var member in entity.EntityKey.EntityKeyValues)
    {
        routeValues.Add(member.Key, member.Value);
    }
    var url = new UrlHelper(helper.ViewContext.RequestContext);
    return ActionLinkForm(helper, "Delete", url.Action("Delete", routeValues));
}

Obviously, additional overloads and methods can be added for other common scenarios, but the end result is a great reduction in code and clean, safe links.

No comments: