asp.net-mvc-4json.netmodelbindersdefaultmodelbinder

How to sanitize JSON input parameters when using MVC4 ApiController?


I had built an HTML sanitizer based on AntiXSS to automatically sanitize user input strings by overriding the default model binder, which works fine on standard post requests. However when using the new ApiController the default model binder never gets called, and I think it is because this new MVC controller uses the JSON formatter instead to bind the input data from the body of the request.

So how would one go about extending the formatter so that I can modify the string properties after they have been bound by JSON? I'd rather not have to implement this at the controller level and there should be a way to do it before it even gets to the controller.


Solution

  • I solved my problem by creating a modified Json formatter, however most of the docs on how to do this are based on pre-release code for .Net 4.0.

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using System.IO;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Net.Http.Formatting;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Json;
    using System.Text;
    using System.Reflection;
    using System.Threading.Tasks;
    using System.Web;
    using System.Web.Script.Serialization;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;
    
    public class JsonNetFormatterAntiXss : JsonMediaTypeFormatter
    {
        public override bool CanReadType(Type type)
        {
            return base.CanReadType(type);
        }
        public override bool CanWriteType(Type type)
        {
            return base.CanWriteType(type);
        }
    
        public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
        {
            HttpContentHeaders contentHeaders = content == null ? null : content.Headers;
            // If content length is 0 then return default value for this type
            if (contentHeaders != null && contentHeaders.ContentLength == 0)
            {
                return Task.FromResult(MediaTypeFormatter.GetDefaultValueForType(type));
            }
    
            // Get the character encoding for the content
            Encoding effectiveEncoding = SelectCharacterEncoding(contentHeaders);
    
            try
            {
                using (JsonTextReader jsonTextReader = new JsonTextReader(new StreamReader(readStream, effectiveEncoding)) { CloseInput = false, MaxDepth = _maxDepth })
                {
                    JsonSerializer jsonSerializer = JsonSerializer.Create(_jsonSerializerSettings);
                    if (formatterLogger != null)
                    {
                        // Error must always be marked as handled
                        // Failure to do so can cause the exception to be rethrown at every recursive level and overflow the stack for x64 CLR processes
                        jsonSerializer.Error += (sender, e) =>
                        {
                            Exception exception = e.ErrorContext.Error;
                                formatterLogger.LogError(e.ErrorContext.Path, exception);
                            e.ErrorContext.Handled = true;
                        };
                    }
    
                    return Task.FromResult(DeserializeJsonString(jsonTextReader, jsonSerializer, type));
                }
    
            }
            catch (Exception e)
            {
                if (formatterLogger == null)
                {
                    throw;
                }
                formatterLogger.LogError(String.Empty, e);
                return Task.FromResult(MediaTypeFormatter.GetDefaultValueForType(type));
            }
        }
    
        private object DeserializeJsonString(JsonTextReader jsonTextReader, JsonSerializer jsonSerializer, Type type)
        {
            object data = jsonSerializer.Deserialize(jsonTextReader, type);
    
            // sanitize strings if we are told to do so
            if(_antiXssOptions != AntiXssOption.None)
                data = CleanAntiXssStrings(data); // call your custom XSS cleaner
    
            return data;
        }
    
        /// <summary>
        /// Clean all strings using internal AntiXss sanitize operation
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private object CleanAntiXssStrings(object data)
        {
            PropertyInfo[] properties = data.GetType().GetProperties();
            foreach (PropertyInfo property in properties)
            {
                Type ptype = property.PropertyType;
                if (ptype == typeof(string) && ptype != null)
                {
                    // sanitize the value using the preferences set
                    property.SetValue(data, DO_MY_SANITIZE(property.GetValue(data).ToString()));
                    }
                }
                return data;
            }
    
        public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, System.Net.TransportContext transportContext)
        {
            return base.WriteToStreamAsync(type, value, writeStream, content, transportContext);
        }
    
        private DataContractJsonSerializer GetDataContractSerializer(Type type)
        {
            Contract.Assert(type != null, "Type cannot be null");
            DataContractJsonSerializer serializer = _dataContractSerializerCache.GetOrAdd(type, (t) => CreateDataContractSerializer(type, throwOnError: true));
    
            if (serializer == null)
            {
                // A null serializer means the type cannot be serialized
                throw new InvalidOperationException(String.Format("Cannot serialize '{0}'", type.Name));
            }
    
            return serializer;
        }
    
        private static DataContractJsonSerializer CreateDataContractSerializer(Type type, bool throwOnError)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }
    
            DataContractJsonSerializer serializer = null;
            Exception exception = null;
    
            try
            {
                // Verify that type is a valid data contract by forcing the serializer to try to create a data contract
                XsdDataContractExporter xsdDataContractExporter = new XsdDataContractExporter();
                xsdDataContractExporter.GetRootElementName(type);
                serializer = new DataContractJsonSerializer(type);
            }
            catch (InvalidDataContractException invalidDataContractException)
            {
                exception = invalidDataContractException;
            }
    
            if (exception != null)
            {
                if (throwOnError)
                {
                    throw new InvalidOperationException(String.Format("Can not serialize type '{0}'.", type.Name), exception);
                }
            }
    
            return serializer;
        }
    
    }
    

    I based this code based on the Json.NET implementation of JsonMediaTypeFormatter as well as this article. As for my AntiXSS implementation I didn't bother posting it, it uses a combination of AntiXSS 4.2.1 along with custom parsing because that library is insanely overprotective.