0

I have been working on a solution to allow us to encrypt and decrypt data in a URL for an asp.net MVC project. My goal is to allow us to encrypt route data and/or querystring data without the developer having to do anything special in the controller or action to decrypt and bind the data to the model(s). I've achieved this by writing my own IValueProvider. My value provider searches the incoming RouteData and query string keys for a prefix that indicates they are an encrypted value. If they begin with the prefix, it decrypts them and will provide a value for them. Here is that much of it in case you're interested TL;DR (right now for testing I'm just using base 64 encoding, not encryption):

public class EncryptedDataValueProvider : IValueProvider
{
    string _encryptionPrefix = "_enc!";
    Dictionary<string, string> _values;

    public EncryptedDataValueProvider(ControllerContext controllerContext)
    {
        _values = new Dictionary<string, string>();

        foreach (KeyValuePair<string, object> entry in controllerContext.RouteData.Values)
        {
            object val = entry.Value;
            if (val != null && val is string && ((string)val).StartsWith(_encryptionPrefix))
            {
                //TODO: Decrypt substring
                string encryptedVal = ((string)val).Substring(_encryptionPrefix.Length);
                string decryptedVal = Encoding.UTF8.GetString(Convert.FromBase64String(encryptedVal));
                _values.Add(entry.Key, decryptedVal);
            }
        }

        //Note: The .NET source code seems to indicate accessing QS keys might trigger request validation before we want it to. 
        //May have to use controllerContext.HttpContext.Request.Unvalidated to get at the keys instead. See 
        //QueryStringValueProvider.cs, NameValueCollectionValueProvider.cs, and UnvalidatedRequestValuesWrapper.cs ("M.W.I" is Microsoft.Web.Infrastructure where request validation is built)
        foreach (string key in controllerContext.HttpContext.Request.QueryString) 
        {
            if (key.StartsWith(_encryptionPrefix))
            {
                string val = controllerContext.HttpContext.Request.QueryString[key];

                if (val != null)
                {
                    //TODO: Decrypt val
                    string decryptedVal = Encoding.UTF8.GetString(Convert.FromBase64String(val));
                    NameValueCollection decryptedQs = HttpUtility.ParseQueryString(decryptedVal);

                    foreach (string decryptedKey in decryptedQs)
                        if (!_values.ContainsKey(decryptedKey))
                            _values.Add(decryptedKey, decryptedQs[decryptedKey]);
                }
            }
        }
    }

    public bool ContainsPrefix(string prefix)
    {
        return _values.ContainsKey(prefix);
    }

    public ValueProviderResult GetValue(string key)
    {
        if (_values.ContainsKey(key))
            return new ValueProviderResult(_values[key], _values[key], System.Globalization.CultureInfo.InvariantCulture);
        else
            return null;
    }
}

This decrypts incoming encrypted (encoded) values fine. What I need to do now is provide a way of specifying that a given action parameter (a.k.a. model) must be encrypted. That is, I don't want to accept unencrypted URL values. One caveat is I want to leave this as flexible as possible. Ideally, I would have an attribute that I could apply to individual action method parameters, the whole action method, or an entire controller so that we still have the option of accepting unencrypted parameters if needed. I'm picturing something like this:

[RequireEncryption]
public class MyController : Controller
{
    public ActionResult Action1(string secret)
    {
    }

    [RequireEncryption(false)]
    public ActionResult Action2(string notSecret)
    {

    }

    public ActionResult Action3(string secret, [RequireEncryption(false)]string notSecret)
    {
    }
}

Well, I've nearly achieved this too (except the class/method-level attribute always overrides the parameter-level attribute), but it just feels like an ugly hack. To do it, I had to create a ModelBinder, a CustomModelBinderAttribute, and an IAuthorizationFilter FilterAttribute. I use these to replace the built-in ValueProviderCollection with just my single EncryptedDataValueProvider so that if it doesn't have a decrypted value for the model, the model remains null. Both attributes use this same approach but I had to use the CustomModelBinderAttribute to get a parameter-level attribute and the authorization filter to get action & controller-level attributes.

What I don't like about this is 1) that I have to have two different attributes and it's not quite achieving my vision above, but 2) that the ModelBinder and authorization filter are modifying the value providers collection. That seems like they are reaching beyond their intended scope. Is there a more proper way to extend the MVC framework to achieve this?

public class RequireEncryptionOnAllAttribute : FilterAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        filterContext.Controller.ValueProvider = new EncryptedDataValueProvider(filterContext.Controller.ControllerContext);
    }
}

public class RequireEncryption : CustomModelBinderAttribute
{
    public override IModelBinder GetBinder()
    {
        return new EncryptionRequiredBinder();
    }
}

/// <summary>
/// Binds the model using only the EncryptedDataValueProvider. Unencrypted values will not be bound.
/// </summary>
public class EncryptionRequiredBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        bindingContext.ValueProvider = new EncryptedDataValueProvider(controllerContext);

        return base.BindModel(controllerContext, bindingContext);
    }
}
xr280xr
  • 12,621
  • 7
  • 81
  • 125
  • 4
    Out of curiosity: Why do this? Are you trying to recreate `https` which does this out of the box? [see also - Are https URLs encrypted?](http://stackoverflow.com/a/499594/1260204) – Igor Apr 05 '17 at 20:49
  • That's a good question and I don't have a great answer but basically just to obscure the URL parameters. This will be in a multi-tenant intranet application and this will just make it harder for a user to make requests that the application did not provide a link for or direct them to. This is a POC. I think some minds involved feel it will provide adequate isolation of data by making it hard to guess and type in IDs of data that does not belong to them. Obviously we'll need something at a much lower level to actually achieve that. – xr280xr Apr 05 '17 at 21:04
  • 1
    Sounds like security through obscurity. I would use an SSL certificate for the point to point encryption. Then use something like ASP.NET identity for Authentication. For Authorization add an AuthorizationAttribute that validates the request in the context of the user. Finally use DI (autofac or ninject) and create an injectable ExecutionContext type that supplies the CompanyId associated to the Users Identity and use that in any select/update/insert/delete queries in your business code. That should provide for everything. – Igor Apr 05 '17 at 21:16
  • 1
    This just feels like the totally wrong way of attempting to enforce access control. Keep in mind that such links are cached by the browser, so somebody else later using the same browser can still access the data that they should not have the right to access. You also don't seem to have any encryption yet in your code, only encoding. – TheGreatContini Apr 05 '17 at 21:16
  • Thank you for pointing it out. It had to be said. But I know, guys. The site will be using SSL, and proper user authentication & authorization. I like the sounds of DI for associating users with an ID - thanks for pointing that out. I'm looking at SQL Server 2016 RLS amongst other options for isolating data. That takes care of everything except that the guy in charge doesn't want parameters in plain text ;) There's no harm in a layer of obscurity as long as it doesn't stand in the way of us building and maintaining the app. – xr280xr Apr 05 '17 at 21:26

0 Answers0