how to sanitize input data in web api using anti xss attack

Ankur Bhutani picture Ankur Bhutani · Sep 16, 2015 · Viewed 13.8k times · Source

Below is the snippet of my code

Model class

// Customer.cs

using CommonLayer;

namespace Models
{
    public class Customer
    {
        public int Id { get; set; }

        [MyAntiXss]
        public string Name { get; set; }
    }
}

I want to sanitize the value in the 'Name' field of the Model class as below

// CutstomModelBinder.cs

 using Microsoft.Security.Application;
    using System.ComponentModel;
    using System.Linq;
    using System.Web.Mvc;

    namespace CommonLayer
    {
        public class CutstomModelBinder : DefaultModelBinder
        {
            protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
            {
                if (propertyDescriptor.Attributes.OfType<MyAntiXssAttribute>().Any())
                {
                    ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
                    string filteredValue = Encoder.HtmlEncode(valueResult.AttemptedValue);
                    propertyDescriptor.SetValue(bindingContext.Model, filteredValue);
                }
                else
                    base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
            }
        }
    }

I changed the 'DefaultBinder' to my 'CutstomModelBinder' as below

// Global.asax.cs

using CommonLayer;
using System.Web.Http;
using System.Web;
using System.Web.Mvc;

namespace WebAPI
{
    public class WebApiApplication : HttpApplication
    {
        protected void Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);
            ModelBinders.Binders.DefaultBinder = new CutstomModelBinder();
        }
    }
}

I wrote a controller class as below

// CustomerController.cs

using Models;
using System.Collections.Generic;
using System.Web.Http;

namespace WebAPI.Controllers
{
    public class CustomerController : ApiController
    {
        public string Post([FromBody]Customer customer)
        {
            //customer.Name = Encoder.HtmlEncode(customer.Name);
            return string.Format("Id = {0}, Name = '{1}'", customer.Id, customer.Name);
        }
    }
}

When I am calling the above controller's class 'Post' method as below, it is giving call to the 'Post' method of the controller's class as expected. But it is not calling the 'BindProperty' method in my 'CutstomModelBinder' class.

// Program.cs

using Models;
using System;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;

namespace Client
{
    public static class Program
    {
        public static void Main(params string[] args)
        {
            bool success = Post();
            Console.WriteLine("success = " + success);
            Console.Read();
        }

        private static HttpClient GetHttpClient()
        {
            HttpClient client = new HttpClient { BaseAddress = new Uri("http://localhost:49295/") };
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            return client;
        }

        private static bool Post()
        {
            Customer customer = new Customer { Id = 1, Name = "<br>Anivesh</br>" };
            HttpContent content = new ObjectContent<Customer>(customer, new JsonMediaTypeFormatter());

            HttpClient client = GetHttpClient();
            HttpResponseMessage response = client.PostAsync("Customer", content).Result;
            client.Dispose();

            if (response.IsSuccessStatusCode)
            {
                string expected = string.Format("Id = {0}, Name = '{1}'", customer.Id, customer.Name);
                string result = response.Content.ReadAsAsync<string>().Result;
                return expected == result;
            }
            else
                return false;
        }
    }
}

Please let me know the correct way of using the 'DataBinders', so that I could sanitize the input data at a common place, before receiving calls in the controllers.

Answer

RMD picture RMD · Apr 18, 2016

To sanitize input in a generic fashion using Web API, you could create your own ModelBinder as described in my previous answer, however an easier approach would likely be to modify the existing JsonMediaTypeFormatter to include the desired santization logic within the ReadFromStreamAsync method.

One approach you could try is as follows:

First, create a generic Attribute which is used to decorate the properties within your DTO that requires sanitization, ie:

 [AttributeUsage(AttributeTargets.Property)]
 public sealed class SanitizeAttribute : Attribute
 { }

Then create a sub-type of the JsonMediaTypeFormatter which takes care of the sanitization, ie:

public sealed class SanitizingJsonMediaTypeFormatter : JsonMediaTypeFormatter
{
    public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger, CancellationToken cancellationToken)
    {
        Task<object> resultTask = base.ReadFromStreamAsync(type, readStream, content, formatterLogger, cancellationToken);

        var propertiesFlaggedForSanitization = type.GetProperties().Where(e => e.GetCustomAttribute<SanitizeAttribute>() != null).ToList();
        if (propertiesFlaggedForSanitization.Any())
        {
            var result = resultTask.Result;
            foreach (var propertyInfo in propertiesFlaggedForSanitization)
            {
                var raw = (string)propertyInfo.GetValue(result);
                if (!string.IsNullOrEmpty(raw))
                {
                    propertyInfo.SetValue(result, AntiXssEncoder.HtmlEncode(raw, true));
                }
            }
        }
        return resultTask;
    }
}

This implementation simply checks to see if the resulting Type has any properties that are decorated with the Sanitize attribute, and if so, uses the built-in System.Web.Security.AntiXss.AntiXssEncoder (.NET 4.5 and above) to perform the sanitization.

You'll likely will want to optimize this class such that it caches type and property information such that you're not doing heavy weight reflection calls on each deserialization.

The last step in the process is to replace the built-in JSON media type formatter with your own, within the WebAPI start-up code:

var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
config.Formatters.Remove(jsonFormatter);
config.Formatters.Add(new SanitizingJsonMediaTypeFormatter());

Now any DTO that has properties decorated with the Sanitize attribute will be properly encoded before the DTO even hits your controller.