Security Issue in ASP.NET MVC3 JsonValueProviderFactory

    Introduction:

          Model binding(a mechanism for mapping action method parameters with request data), is one of the most popular feature of ASP.NET MVC. The default model binder get its data from different value providers. In ASP.NET MVC 2, by default, these value providers include FormValueProvider, QueryStringValueProvider, RouteDataValueProvider and HttpFileCollectionValueProvider. ASP.NET MVC 3 added two additional value providers, ChildActionValueProvider and a value provider for Json. Json value provider makes it very easy to model bind your action method parameters with incoming Json data, but I have found a security issue with this value provider.  In this article, I will show you the security issue regarding Json value provider and also show you how to protect against this security threat.

 

    Description:

          To see this issue, let's create a sample ASP.NET MVC 3 application which include a blog post with comments. For making this application, you need to create a Comment model,

 

    public class Comment
    {
        [Required]
        public string Name { get; set; }

        [Required]
        public string UserComment { get; set; }
    }

 

          Then you need to store the user comments somewhere. For this sample application, you can store the user comments in a static, non thread safe generic list. Here is the definition of this generic list,

 

    public static class Blog
    {
        public static List<Comment> Comments = new List<Comment>();
    }

 

           Then just open(or create) HomeController.cs file and add the following code,

 

    public class HomeController : Controller
    {
        public ActionResult BlogPostWithComments()
        {
            ViewData["Comments"] = Blog.Comments;
            return View();
        }

        [HttpPost]
        public ActionResult BlogPostWithComments(Comment c)
        {
            if (ModelState.IsValid)
            {
                Blog.Comments.Add(new Comment { UserComment = c.UserComment, Name = c.Name });
                return RedirectToAction("BlogPostWithComments");
            }
            ViewData["Comments"] = Blog.Comments;
            return View();
        }
    }

 

           Next, add a view for this action, 

 

    <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<SecurityIssueInJsonValueProviderFactory.Models.Comment>" %>

    <%@ Import Namespace="SecurityIssueInJsonValueProviderFactory.Models" %>
    <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
        A Sample Blog Post
    </asp:Content>
    <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
        <script src="<%= Url.Content("~/Scripts/jquery.validate.min.js") %>" type="text/javascript"></script>
        <script src="<%= Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js") %>" type="text/javascript"></script>
        <h2>
            A Sample Blog Post</h2>
        <br />
        <br />
        This is just a sample blog post.
        <br />
        <br />
        <br />
        <br />
        <h3 style="text-decoration: underline;">
            Comments:</h3>
        <br />
        <% using (Html.BeginForm())
           { %>
        <%= Html.ValidationSummary(true) %>
        <fieldset>
            <legend>Comment</legend>
            <div class="editor-label">
                <%= Html.LabelFor(model => model.Name) %>
            </div>
            <div class="editor-field">
                <%= Html.TextBoxFor(model => model.Name) %>
                <%= Html.ValidationMessageFor(model => model.Name) %>
            </div>
            <div class="editor-label">
                <%= Html.LabelFor(model => model.UserComment) %>
            </div>
            <div class="editor-field">
                <%= Html.TextAreaFor(model => model.UserComment) %>
                <%= Html.ValidationMessageFor(model => model.UserComment) %>
            </div>
            <p>
                <input type="submit" value="Save" />
            </p>
        </fieldset>
        <% } %><br />
        <table>
            <% foreach (var item in ViewData["Comments"] as List<Comment>)
               { %>
            <tr>
                <td>
                    <strong>Name:</strong>
                    <%= item.Name %>
                </td>
            </tr>
            <tr>
                <td>
                    <strong>Comment:</strong>
                    <%= item.UserComment %>
                </td>
            </tr>
            <tr>
                <td>
                    <br />
                </td>
            </tr>
            <% } %>
        </table>
    </asp:Content>

 

            Now just run this application. You will find that everything is working perfectly. When malicious user try to enter any input which contains HTML tags, they will get a HttpRequestValidationException exception. Now let's see the security issue.

            Open the fiddler tool, then build a Json request from fiddler and then execute this request as shown in the figure below,

 

                         

 

            Now just run your application again, you will see the following screen,

 

             

 

            This shows that an attacker can easily bypass the ASP.NET request validation if you are using JsonValueProviderFactory. Therefore, if you are not using Json requests to bind your action method parameters, then I recommend to remove Json value provider, 

 

    ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories[2]);

 

            But if your application is using Json value provider then you can mitigate this issue by creating a custom default model binder by creating a class that inherits from DefaultModelBinder,

 

    public class MyDefaultModelBinder : DefaultModelBinder
    {
        protected override bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
        {
            if (value is string && (controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase)))
            {
                if (controllerContext.Controller.ValidateRequest && bindingContext.PropertyMetadata[propertyDescriptor.Name].RequestValidationEnabled)
                {
                    int index;
                    if (IsDangerousString(value.ToString(), out index))
                    {
                        throw new HttpRequestValidationException("Dangerous Input Detected");
                    }
                }                
            }
            return base.OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, value);
        }

        private static char[] startingChars = new char[] { '<', '&' };

        private static bool IsAtoZ(char c)
        {
            return (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')));
        }

        static bool IsDangerousString(string s, out int matchIndex)
        {
            matchIndex = 0;
            int startIndex = 0;
            while (true)
            {
                int num2 = s.IndexOfAny(startingChars, startIndex);
                if (num2 < 0)
                {
                    return false;
                }
                if (num2 == (s.Length - 1))
                {
                    return false;
                }
                matchIndex = num2;
                char ch = s[num2];
                if (ch != '&')
                {
                    if ((ch == '<') && ((IsAtoZ(s[num2 + 1]) || (s[num2 + 1] == '!')) || ((s[num2 + 1] == '/') || (s[num2 + 1] == '?'))))
                    {
                        return true;
                    }
                }
                else if (s[num2 + 1] == '#')
                {
                    return true;
                }
                startIndex = num2 + 1;
            }
        }
    }

 

             The above code first check that whether this is a Json request, if request is json then it check whether the request validation is enabled for the current property, if request validation is enabled then it check whether the provided value of the current property is dangerous using IsDangerousString method. If the value is dangerous, then it will throw a HttpRequestValidationException exception. Note that the IsDangerousString method is the same method used by the default request validator. Finally you need to register this default model binder in global.asax.cs file,

 

    ModelBinders.Binders.DefaultBinder = new MyDefaultModelBinder();

 

             Now with the help of this model binder your application will throw HttpRequestValidationException exception in Json requests if malicious user send some dangerous input.

 

    Summary:

          In this article, I showed you the security issue regarding JsonValueProviderFactory in ASP.NET MVC 3. I also showed you how you can protect your application against this security issue. I am also attaching a sample application, so that you can test this issue by yourself. Hopefully you will enjoy this article too.


 


15 Comments

  • The only security flaw in this code, is the programmer never Html.Encoded, user submitted input. Just use instead.

  • I mean .

  • @Ryan,
    I think you don't understand the issue.
    The issue is that JsonValueProviderFactory is bypassing the ASP.NET request validation during model binding which is very dangerous.

  • ASP.NET request validation was never intended to prevent XSS; it was only intended to make it harder for inexperienced developers to get it wrong in the most basic scenarios. It's not guaranteed to pick up every invalid input, and there's no excuse for not properly encoding any user-supplied data.

  • @RichardD,
    There are still lot of developers which does not encode user-supplied data. Perhaps, this is the reason why ASP.NET introduced encoding tag in ASP.NET 4. Also this the reason why Razor always encode its values.

    >>@RichardD, ASP.NET request validation was never intended to prevent XSS.

    From here,
    http://msdn.microsoft.com/en-us/library/system.web.httprequestvalidationexception.aspx

    Request validation detects potentially malicious client input and throws this exception to abort processing of the request. A request abort can indicate an attempt to compromise the security of your application, such as a cross-site scripting attack

  • Why dont you use the sanitizer provided by microsoft, to sanitize the input serverside ?
    Instead of trying to code it yourself ?

  • @Martin kirk
    I just wanted to show that how easily a malicious user can by pass the ASP.NET request validation(both granular and normal) in ASP.NET MVC 3.

  • Can you not use the ValidateAntiForgeryToken attribute on the Post ActionResult method to prevent this?

  • @Adam,

    ValidateAntiForgeryToken is used to avoid CSRF attacks. This is possible to avoid this using ValidateAntiForgeryToken because ASP.NET MVC internally always looking for hidden element inside form element(it will not look for hidden element in JSON).

  • There is no security issue. So worst case the script (or sql injection) is stored in your database.

    The problem shown is that the output has not been escaped. Just as Ryan said. In razor @ is escaped by default, so this would not be an issue. I'd go back through any ASP views and change all <%= to <%: and then only put them back when you need raw html.

  • @Scott,
    Please read the article carefully, the issue is that it is bypassing ASP.NET request validation.

  • Nice post! As you mentioned in the previous comments, this was more about illustrating the issue and the mechanics behind it vs. something else. Very nice!

  • ok you should encode the output but what happens if you are returning the same object via Json like this (i'm just using the same method for "posting" and "returning" but you get the point):

    public class TestClass
    {
    public string Name { get; set; }
    public int? Id { get; set; }
    }

    [HttpPost]
    public ActionResult PostTestClass(TestClass testClassParam)
    {
    return Json(new
    {
    ServerTime = DateTime.Now.ToString("s"),
    ObjectReceived = testClassParam
    });
    }

    in this case you would need to make sure that the javascript that is making use of this return json object is encoding the properties.

  • @tobias, very nice point.

  • Nice writeup. I found one problem, however.

    OnPropertyValidating() only handles strings - not string arrays/collections.

    If my JSON contains a array of (malicious) strings the exception will not be thrown.

    A better implementation would be (sorry for the poor formatting) :

    protected override bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
    {
    // Content of Http Request is JSON ?
    if (controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
    {
    // Is Validation enabled (in the web.config) ?
    if (controllerContext.Controller.ValidateRequest && bindingContext.PropertyMetadata[propertyDescriptor.Name].RequestValidationEnabled)
    {
    int index;

    // Check strings and string collections ...

    if (value is string)
    {
    if (IsDangerousString(value.ToString(), out index))
    {
    throw new HttpRequestValidationException("Dangerous Input Detected");
    }
    }
    else if (value is IEnumerable)
    {
    foreach (object obj in value as IEnumerable)
    {
    if (IsDangerousString(obj.ToString(), out index))
    {
    throw new HttpRequestValidationException("Dangerous Input Detected");
    }
    }
    }
    }
    }

    return base.OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, value);
    }

Comments have been disabled for this content.