Skip to content

Dealing with Direct Object References in ASP MVC

If you are not familiar with OWASP site, I highly encourage you take a look at it.  I think everyone can learn something by reading this site.  More specifically, I wanted to concentrate on one of the top 10 mistakes, Insecure Direct Object References.

If you take a look at most of ASP.NET MVC sample applications, you will notice that they are subject to this mistake.  So, if you create a sample application, then add Entity Framework code first context with a table that is using integer primary key, then create new controller with CRUD actions from the add new controller window, then run the app, you will immediately notice that your edit, details and delete links have that primary key as part of the URI.  For example, if you follow Edit link, you will see something like the following in your URI: http://localhost:12345/Products/Edit/1, where 1 is primary key.  If you are creating an intranet site that hosts homogenous data in Product table, you are probably OK.  However, imagine you write a multitenant  internet application, IDs 1 and 2 could belong to two different tenants of you application.  As a result, tenant 1 can see data from tenant 2 by simply incrementing a primary key in the URI.  Now, you have a serious problem on your hands. 

Some of the solutions you might use are

  • Replace integer primary keys with GUIDs
  • Somehow compute hash of the key, then decipher what it is in your controller

I am not a giant fan of first solution, and neither is 99.9 percent of DBAs out there.  You can add a candidate key that is a GUID and use it, but again you make your database less efficient.  What I am going to describe in this post is second solution. 

What I want to do is make sure that the solution will require minimum effort for me to implement and is robust enough.  I am going with encryption now, using a class I described here as a base.  What I am going to do is encrypt this integer primary key when I build the list of products as follows:

@using MvcValidation.Helpers
@model IEnumerable<MvcValidation.Data.Product>
@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th>
            Name
        </th>
        <th>
            Start Date
        </th>
        <th>
            End Date
        </th>
        <th>
            Minimum Quantity
        </th>
        <th>
            Maximum Quantity
        </th>
        <th>
        </th>
    </tr>
    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.BeginDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.EndDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.BeginQuantity)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.EndQuantity)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id =
                    EncryptionUtility.Encrypt(item.Id.ToString(), ViewData["Password"].ToString(), true) })
                @Html.ActionLink("Details", "Details", new { id =
                    EncryptionUtility.Encrypt(item.Id.ToString(), ViewData["Password"].ToString(), true) })
                @Html.ActionLink("Delete", "Delete", new { id =
                    EncryptionUtility.Encrypt(item.Id.ToString(), ViewData["Password"].ToString(), true) })
            </td>
        </tr>
    }
</table>

As you can see this code is very simple.  I did however had to modify my utility to make it URI friendly.  Because it is using Base 64 strings, it will potentially result in characters such as “/” or “+”, which will break URIs.  So, I update the utility to take another parameter to take care of this little problem.

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace MvcValidation.Helpers
{
    public static class EncryptionUtility
    {

        /// <summary>
        /// Encrypt the data
        /// </summary>
        /// <param name="input">String to encrypt</param>
        /// <param name="password">The password.</param>
        /// <param name="uriFriendly">if set to <c>true</c> produce URI friendly output.</param>
        /// <returns>
        /// Encrypted string
        /// </returns>
        public static string Encrypt(string input, string password, bool uriFriendly = false)
        {
            if (password.Length < 8)
            {
                password = (password + "zzzzzzzz").Substring(0, 8);
            }
            byte[] utfData = Encoding.UTF8.GetBytes(input);
            byte[] saltBytes = Encoding.UTF8.GetBytes(password);
            string encryptedString;
            using (var aes = new AesManaged())
            {
                var rfc = new Rfc2898DeriveBytes(password, saltBytes);

                aes.BlockSize = aes.LegalBlockSizes[0].MaxSize;
                aes.KeySize = aes.LegalKeySizes[0].MaxSize;
                aes.Key = rfc.GetBytes(aes.KeySize / 8);
                aes.IV = rfc.GetBytes(aes.BlockSize / 8);

                using (var encryptTransform = aes.CreateEncryptor())
                {
                    using (var encryptedStream = new MemoryStream())
                    {
                        using (var encryptor =
                            new CryptoStream(encryptedStream, encryptTransform, CryptoStreamMode.Write))
                        {
                            encryptor.Write(utfData, 0, utfData.Length);
                            encryptor.Flush();
                            encryptor.Close();

                            byte[] encryptBytes = encryptedStream.ToArray();
                            encryptedString = Convert.ToBase64String(encryptBytes);
                        }
                    }
                }
            }
            if (uriFriendly)
            {
                encryptedString = encryptedString.Replace("+", "-").Replace("/", "_");
            }
            return encryptedString;
        }

        /// <summary>
        /// Decrypt a string
        /// </summary>
        /// <param name="input">Input string in base 64 format</param>
        /// <param name="password">The password.</param>
        /// <param name="uriFriendly">if set to <c>true</c> produce URI friendly output.</param>
        /// <returns>
        /// Decrypted string
        /// </returns>
        public static string Decrypt(string input, string password, bool uriFriendly = false)
        {
            if (password.Length < 8)
            {
                password = (password + "zzzzzzzz").Substring(0, 8);
            }
            if (uriFriendly)
            {
                input = input.Replace("-", "+").Replace("_", "/");
            }
            byte[] encryptedBytes = Convert.FromBase64String(input);
            byte[] saltBytes = Encoding.UTF8.GetBytes(password);
            string decryptedString;
            using (var aes = new AesManaged())
            {
                var rfc = new Rfc2898DeriveBytes(password, saltBytes);
                aes.BlockSize = aes.LegalBlockSizes[0].MaxSize;
                aes.KeySize = aes.LegalKeySizes[0].MaxSize;
                aes.Key = rfc.GetBytes(aes.KeySize / 8);
                aes.IV = rfc.GetBytes(aes.BlockSize / 8);

                using (var decryptTransform = aes.CreateDecryptor())
                {
                    using (var decryptedStream = new MemoryStream())
                    {
                        var decryptor =
                            new CryptoStream(decryptedStream, decryptTransform, CryptoStreamMode.Write);
                        decryptor.Write(encryptedBytes, 0, encryptedBytes.Length);
                        decryptor.Flush();
                        decryptor.Close();

                        byte[] decryptBytes = decryptedStream.ToArray();
                        decryptedString =
                            Encoding.UTF8.GetString(decryptBytes, 0, decryptBytes.Length);
                    }
                }
            }

            return decryptedString;
        }
    }
}

This code for URI friendliness is pretty simple.  I also altered the code to make sure it will automatically handle password (salt) that is smaller than 8 characters.  You will kno0w shortly as to why I did that.  Because the utility produces same results with the same input and password, I will end up with the following issue.  If I have products and categories controllers, both will produce the same URI for the same integer ID.  I want to make it a bit more secure, so I am going to use controller name as the key.  So, the password will be set by controller as follows:

using System.Data;
using System.Linq;
using System.Web.Mvc;
using MvcValidation.Data;

namespace MvcValidation.Controllers
{
    public class ProductController : Controller
    {
        private readonly ProductContext _db = new ProductContext();

        //
        // GET: /Product/

        public ViewResult Index()
        {
            ViewData.Add("Password", GetType().Name.Replace("Controller", "").ToLower());
            return View(_db.Products.ToList());
        }

I could probably use full class name as well, but I want to avoid writing decryption code in controllers, thus I want to use MVC routing to decrypt the data.  I am going to write a custom route that does exactly that.

using System.Web;
using System.Web.Routing;
using MvcValidation.Helpers;

namespace MvcValidation.Routing
{
    public class RouteWithEncryption : RouteBase
    {
        private readonly RouteBase _inner;

        /// <summary>
        /// Initializes a new instance of the <see cref="RouteWithEncryption"/> class.
        /// </summary>
        /// <param name="routeToWrap">The route to wrap.</param>
        public RouteWithEncryption(RouteBase routeToWrap)
        {
            _inner = routeToWrap;
        }

        /// <summary>
        /// When overridden in a derived class, returns route information about the request.
        /// </summary>
        /// <param name="httpContext">An object that encapsulates information about the HTTP request.</param>
        /// <returns>
        /// An object that contains the values from the route definition if the route matches the current request, or null if the route does not match the request.
        /// </returns>
        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            var routeData = _inner.GetRouteData(httpContext);
            if (routeData != null)
            {
                if (routeData.Values.ContainsKey("id") && routeData.Values.ContainsKey("controller") && routeData.Values.ContainsKey("action"))
                {
                    var id = routeData.Values["id"].ToString();
                    if (!string.IsNullOrEmpty(id))
                    {
                        var password = routeData.Values["controller"].ToString().ToLower();
                        int test;
                        if (!int.TryParse(id, out test))
                        {
                            var replacement = EncryptionUtility.Decrypt(id, password, true);
                            routeData.Values["id"] = replacement;
                        }
                    }
                }
            }
            return routeData;
        }

        /// <summary>
        /// When overridden in a derived class, checks whether the route matches the specified values, and if so, generates a URL and retrieves information about the route.
        /// </summary>
        /// <param name="requestContext">An object that encapsulates information about the requested route.</param>
        /// <param name="values">An object that contains the parameters for a route.</param>
        /// <returns>
        /// An object that contains the generated URL and information about the route, or null if the route does not match <paramref name="values"/>.
        /// </returns>
        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            return _inner.GetVirtualPath(requestContext, values);
        }
    }
}

As you can see, I am pulling controller name and ID from route values collection and decrypting the ID using controller name as password.  Voila, now you know why I use controller name.  Now, I am going to update all routes to use my encrypting route in Global.asax.cs:

using System.Web.Mvc;
using System.Web.Routing;
using MvcValidation.Routing;

namespace MvcValidation
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : System.Web.HttpApplication
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

            for (var i = 0; i < routes.Count; i++)
            {
                routes[i] = new RouteWithEncryption(routes[i]);
            }
        }

That is it, now we have hidden primary keys from users.  You can download entire solution here.

Thanks.

Post to Twitter

Validation Inside jQuery Accordion

I recently uncovered a little issue with using entry controls that use unobtrusive JavaScript validation inside jQuery accordion control.  When an error is shown, the accordion does not resize as it should, thus some controls fall off the accordion surface.  I got some feedback on  ASP.NET forum from Bruce, and wrote a little function that does accordion resizing.  You just need to call the function after the form has been loaded and jQuery initialized.  I wanted to post the code here on my blog as well as forum, mostly for my own benefit.  Yes, I am selfish :-)

 hookupErrorPlacementAndResize: function () {
    var currentForm = $('form');
    $.each(currentForm, function () {
        var validator = $(this).data('validator');
        var errorPlacement = validator.settings.errorPlacement;
        validator.settings.errorPlacement = function (error, element) {
             if (errorPlacement) {
                   errorPlacement(error, element);
            }
       $('.accordion_class').accordion('resize');
     };
   });
 }

If you have multiple forms, you can always change the code to take form name as a parameter to the function.  All function does is override errorPlacement function used by jQuery validation and hooks up additional code to resize accordions based on class.  You can always change that as well and use names.

Thanks.

Post to Twitter

Windows 8 Metro Applications and the Azure (Cont.)

As I mentioned before, I am going to write the last post about integrating Windows 8 Metro applications with Azure.

First of, let’s take a look at securing our service with Forms Authentication.  I blogged about that as well, and the only change is that I am going to use REST for authentication service.  Here is the interface for my service – it is very simple:

    [ServiceContract]
    public interface ILoginService
    {
        [OperationContract]
        [WebGet(UriTemplate = "/Login/?userName={userName}&password={password}",
            RequestFormat = WebMessageFormat.Json,
            ResponseFormat = WebMessageFormat.Json)]
        bool Login(string userName, string password);

The implementation is using Users table I mentioned before:

    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    public class LoginService : ILoginService
    {
        public bool Login(string userName, string password)
        {
            bool returnValue = false;
            using (var context = new QuotesContext())
            {
                var user = context.Users.FirstOrDefault(one => one.UserName == userName);

                if (user != null)
                {
                    returnValue = (user.Password == password);
                }
            }
            if (returnValue)
            {
                var ticket =
                    new FormsAuthenticationTicket(
                        1,
                        userName,
                        DateTime.Now,
                        DateTime.Now.AddHours(24),
                        false,
                        "",
                        FormsAuthentication.FormsCookiePath);

                string encryptedTicket = FormsAuthentication.Encrypt(ticket);
                var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket)
                {
                    Path = FormsAuthentication.FormsCookiePath,
                    Expires = DateTime.Now.AddHours(24)
                };

                HttpContext.Current.Response.Cookies.Add(cookie);
            }
            return returnValue;
        }

The code is pretty straight forward as you can see.  Just match user name and password and create authentication cookie to be passed back to the client.

Now on Windows 8 side the code is just as easy, I am just going to use HttpClient class:

var client = new System.Net.Http.HttpClient();

            var responce = await client.GetAsync(new Uri("http://xxxx.cloudapp.net/LoginService.svc/Login/?userName=admin@system.com&password=_Random1"));
            var text = await responce.Content.ReadAsStringAsync();
            if (text.ToLower().Equals("true"))
            {
                responce = await client.GetAsync(new Uri("http://xxxxx.cloudapp.net/QuoteService.svc/GetQuote"));
                text = await responce.Content.ReadAsStringAsync();
                if (text.LastIndexOf("-") >= 0)
                {
                    text = text.Substring(0, text.Replace("--", "-").LastIndexOf("-")) + Environment.NewLine + text.Substring(text.Replace("--", "-").LastIndexOf("-"));
                }
                text = text.Replace("  ", " ");
                if (text.StartsWith("\""))
                {
                    text = text.Substring(1);
                }
                if (text.EndsWith("\""))
                {
                    text = text.Substring(0, text.Length - 1);
                }
                QuoteBlock.Text = text.Trim();
                quoteGrid.Visibility = Windows.UI.Xaml.Visibility.Visible;
            }

Getting the data from protected service is a two step process.  I am first calling authentication service, passing user and password as query string parameters, then I verify that authentication was successful, then I call the real service using the same instance of the HttpClient, which will pass my authentication cookie.  The rest of the code just deal with return value processing.

As far as updating the tile, the story is less bright in my opinion.  According to Microsoft there is not way to secure the end point that tile updater will call.  So, I cannot do what I wanted to.  Not only that, but processing XML that file updater expects does not work properly when you create it from XDcoument.  So, here is the solution I came up with.  I am going to simply create a text file with a placeholder for the actual content, then write worker role that would get the content, then update the XML, then upload it to Azure Blob Storage.  Then I can retrieve very simply by calling

PeriodicUpdateRecurrence recurrence = PeriodicUpdateRecurrence.HalfHour;

System.Uri url = new Uri("http://xxxx.blob.core.windows.net/quotes/quote?sr=b&si=Random&sig=VUErCXWGIczO9pLLNI4nc4m2DTTixUg%2F9%2F2qYqY1BAY%3D");

TileUpdateManager.CreateTileUpdaterForApplication().StartPeriodicUpdate(url, recurrence);

You can use for example  https://www.myazurestorage.com/ to create a blob container and set access policy on it as well as create a URL to a specific blob entry.  That is what I did, and you can see my policy called Random as part of the URL above.  There is also no easy way to troubleshoot the process if you get any steps wrong.  Here is an example tile XML you can just upload to blob storage for testing.  I know it works.

<tile>
  <visual lang=”en-US”>
    <binding template=”TileWideText03″>
      <text id=”1″>Testing 123…</text>
    </binding> 
  </visual>
</tile>

Enjoy.

Post to Twitter