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

3

14

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.

Postgraduate answered 16/9, 2015 at 4:43 Comment(1)
Theres an interessting post about this topic at StackExchange: security.stackexchange.com/questions/108559/…Orlop
K
11

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.

Kasper answered 18/4, 2016 at 16:58 Comment(3)
I'm trying your code at the attribute level and I don't know if I'm missing something but instead of returning JSON, it returns XML. Any ideas how to return it back as JSON once it has been sanitized?Corycorybant
@Corycorybant - sounds like you have both the XML and JSON formatters configured. You can either remove the XML formatter if you don't need it (similar to what I did above re: removing the default JSON formatter), or you could Insert the new JSON formatter at position 0, so that it is hit first, (ie: config.formatters.Insert(0, new SanitizingJsonMediaTypeFormatter())Kasper
This is how I was able to do what RMD said: WebApiConfig: var appXmlTypes = config.Formatters.XmlFormatter.SupportedMediaTypes; foreach (var appXmlType in appXmlTypes.ToList()) { config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType); } ```Antler
M
4

.NetCore Web API 2. Sanitize Recursively all Properties (in any depth) of the incoming JSON by using InputFormatter.

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

public class SanitizeTextInputFormatter: Microsoft.AspNetCore.Mvc.Formatters.TextInputFormatter
{
    private List<String> ExcludeTypes = new List<string>()
    {
        "System.DateTime",
        "System.Int32",
        "System.Int64",
        "System.Boolean",
        "System.Char",
        "System.Object"
    };

    private string CleanInput(string strIn)
    {
        // Replace invalid characters with empty strings.
        try
        {
            // [<>/] or @"[^\w\.@-]"
            return Regex.Replace(strIn, @"[<>/]", "",
                                 RegexOptions.None, TimeSpan.FromSeconds(1.5));
        }
        // If we timeout when replacing invalid characters, 
        // we should return Empty.
        catch (RegexMatchTimeoutException)
        {
            return String.Empty;
        }
    }

    public SanitizeTextInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    private bool ValidateSanitizeProperty(Type type, PropertyInfo PropertyInfo, List<PropertyInfo> orgTypes)
    {
        var listedProperty = orgTypes.Where(_ => _ == PropertyInfo).FirstOrDefault();
        if (PropertyInfo != null && listedProperty == null) orgTypes.Add(PropertyInfo);

        if (listedProperty != null) return false;

        if (type.FullName == "System.String" && PropertyInfo != null)
        {
            var sanitizePropertyAttribute = PropertyInfo.GetCustomAttribute<SanitizePropertyAttribute>();
            //var sanitizeClassAttribute = PropertyInfo.CustomAttributes.Where(e => e.AttributeType == typeof(SanitizePropertyAttribute)).FirstOrDefault();

            return sanitizePropertyAttribute != null;
        }

        var typeProperties = type.GetProperties().Where(_ => _.PropertyType.IsAnsiClass == true && !ExcludeTypes.Contains(_.PropertyType.FullName)).ToList();

        var doSanitizeProperty = false;
        typeProperties.ForEach(typeProperty =>
        {
            if (doSanitizeProperty == false)
                doSanitizeProperty = ValidateSanitizeProperty(typeProperty.PropertyType, typeProperty, orgTypes);
        });

        return doSanitizeProperty;

    }

    protected override bool CanReadType(Type type)
    {
        var result = ValidateSanitizeProperty(type, null, new List<PropertyInfo>());
        return result;
    }

    private object SanitizeObject(object obj, Type modelType)
    {
        if (obj != null)
        {
            List<PropertyInfo> propertiesFlaggedForSanitization = modelType.GetProperties().Where(e => e.GetCustomAttribute<SanitizePropertyAttribute>() != null).ToList();
            if (propertiesFlaggedForSanitization.Any())
            {
                foreach (var propertyInfo in propertiesFlaggedForSanitization)
                {
                    var raw = (string)propertyInfo.GetValue(obj);
                    if (!string.IsNullOrEmpty(raw))
                    {
                        propertyInfo.SetValue(obj, CleanInput(raw));
                        //propertyInfo.SetValue(obj, AntiXssEncoder.HtmlEncode(raw, true));
                        //propertyInfo.SetValue(obj, AntiXssEncoder.UrlEncode(raw));
                    }
                }
            }
        }

        modelType.GetProperties().ToList().Where(_ => _.PropertyType.IsAnsiClass == true && !ExcludeTypes.Contains(_.PropertyType.FullName)).ToList().ForEach(property =>
        {
            try
            {
                var nObj = property.GetValue(obj);
                if (nObj != null)
                {
                    var sObj = SanitizeObject(nObj, property.PropertyType);
                    property.SetValue(obj, sObj);
                }
            }
            catch(Exception ex)
            {   
            }
        });

        return obj;
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (encoding == null)
        {
            throw new ArgumentNullException(nameof(encoding));
        }

        using (var streamReader = context.ReaderFactory(context.HttpContext.Request.Body, encoding))
        {
            string jsonData = await streamReader.ReadToEndAsync();
            var nObj = Newtonsoft.Json.JsonConvert.DeserializeObject(jsonData, context.ModelType);
            var modelType = context.ModelType;

            try
            {
                var sbj = SanitizeObject(nObj, modelType);

                return await InputFormatterResult.SuccessAsync(sbj);
            }catch (Exception ex)
            {
                return await InputFormatterResult.FailureAsync();
            }
        }
    }
}

And we declare that in public void ConfigureServices(IServiceCollection services) function of the startup.cs class, like this:

services.AddMvcCore(options => { options.InputFormatters.Add(new SanitizeTextInputFormatter()); })
Manley answered 19/2, 2019 at 23:2 Comment(1)
It didn't do anything, so I removed the default JsonInputFormatter before adding this one. Result is getting a 415 Unsupported Media Type.Honduras
K
0

The DefaultModelBinder is in the System.Web.ModelBinding namespace, which is used by MVC controllers.

For a WebAPI project, you need to implement the System.Web.Http.ModelBinding.IModelBinder interface.

A sample model binder, taken right from the MSDN site follows:

public class GeoPointModelBinder : IModelBinder
{
    // List of known locations.
    private static ConcurrentDictionary<string, GeoPoint> _locations
        = new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);

    static GeoPointModelBinder()
    {
        _locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
        _locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
        _locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
    }

    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(GeoPoint))
        {
            return false;
        }

        ValueProviderResult val = bindingContext.ValueProvider.GetValue(
            bindingContext.ModelName);
        if (val == null)
        {
            return false;
        }

        string key = val.RawValue as string;
        if (key == null)
        {
            bindingContext.ModelState.AddModelError(
                bindingContext.ModelName, "Wrong value type");
            return false;
        }

        GeoPoint result;
        if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result))
        {
            bindingContext.Model = result;
            return true;
        }

        bindingContext.ModelState.AddModelError(
            bindingContext.ModelName, "Cannot convert value to Location");
        return false;
    }
}

A full post that backs up this sample can be found here: MSDN Model Binding

Kasper answered 18/4, 2016 at 14:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.