Asp net core action filters and requests html encoding

Learn how you can use action filters in Asp Net core with a practical example.

Normally you need to accept text user input in your REST Apis, in that case XSS can become a security concern, you can but certainly shouldn’t rely on the frontend to send safe text input o to rendered it back properly for many reasons. The Asp Net MVC Framework (the classic controllers and views) does an html encoding when rendering the view models, but if you are using an Api based application or you want to make sure you only save sanitized input you will have to do this yourself. In this article I will explain what Filters are in the context of asp net and how we can apply them to make sure all requests are properly html encoded. You can follow along with the explanation or look at the source code here.

Filters

Let’s begin by explaining what Filters are, in asp net core a MVC filter is a piece of code that its being executed at specific stage in the request/response pipeline and they can inspect, change or stop the pipeline. You can have different types of Filters: Authorization Filters, Resource Filters, Actions filters Result filters and Exception, Filters (the last two are more useful when dealing with responses so let’s ignore them).

Authorization Filters can use the circuit braker pattern to prevent the action to be executed while  Resource Filters that are useful to implement patterns like caching and so on.

On the other hand, you have Action Filters which are executed after the model binding process and right before the controller’s action, meaning you will get a ActionArguments property containing the actual arguments that are going to be passes to the controller action making it a perfect fit for inspecting and modifying the request objects using reflection.

Action Filters

To create a custom action filter, you can need a class that implement the IActionFilter interface provided by the framework or you can derive the abstract class ActionFilterAttribute as this allows you to only implement the method you are interested in. Either way the you get the same contract.

public interface IActionFilter : IFilterMetadata
{
    void OnActionExecuting(ActionExecutingContext context);
    void OnActionExecuted(ActionExecutedContext context);
}
IActionFilter

After looking at the contract you can probably guess that OnActionExecuting is executed before the target controller action, while OnActionExecuted comes after. In this article we'll put the focus on the first method as given that we need to modify the request. The ActionExecutingContext has several properties that describe the action being executed, here is a list with the specific properties that we'll be using for quick reference.

Name Description
HttpContext Provides details of the current HTTP request and the HTTP response.
ActionArguments A dictionary of the arguments that will be passed to the action method (indexed by name).
ActionDescriptor Describes the action method.

The Code

Now that we now how this filters work let’s put this in practice and create one. First, we need to create a marking interface and an attribute.

public interface IRequireHtlmEncoding { }

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method |       
 AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class NeverEncodeAsHtmlAttribute : Attribute { }   
Contracts

We'll use the first interface to mark our request view models since html encoding may be a thing that you would not want to apply to the whole application. The attribute will allow us to skip a given property of the model or the entire action. Here is example how it can be used.

[ApiController]
[Route("api/v1/test")]
public class TestController : ControllerBase
{
   
    //Skip this action
    [HttpPost("model-ignore-all-attribute-on-action")]
    [NeverEncodeAsHtml]
    public void PostModelIgnoreAction([FromBody] TypedModel model)
    {
        ...
    }
}

//Every model that derive from this class will be included
public class RequestModelBase : IRequireHtlmEncoding { }

public class RequestModel : RequestModelBase
{
    //Only skip this field
    [NeverEncodeAsHtml]
    public string? NeverEncodeField { get; set; } = string.Empty;
}
How to use contracts

Of course, contracts don’t do magic so let’s create the filter itself.

public class HtmlEncodeActionFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
	//To Do
    }
}
Html Encode Action Filter 

You can see that the class derives from the abstract class ActionFilterAttribute provided by the framework. The next step would be applying the filter only when a Post or Put request arrives.

public override void OnActionExecuting(ActionExecutingContext context)
{
    if (!HttpMethodApplies(context.HttpContext.Request))
        return;
}

private static bool HttpMethodApplies(HttpRequest request)
{
    var method = request.Method;
    return method == "POST" || method == "PUT";
}
Filter based on Http Request Method

To access the current http request you can access the HttpContext from ActionExecutingContext object. Then we need to check if the action has been marked with the NeverEncodeAsHtmlAttribute as that would be signaling that no encoding is required.

public override void OnActionExecuting(ActionExecutingContext context)
{
    ... 
    if (IsBeingExplicitMarkAsIgnored(context)) return;
}

private static bool IsBeingExplicitMarkAsIgnored(ActionExecutingContext context)
{
    return context.ActionDescriptor
    		  .EndpointMetadata
                  .Any(x => x is NeverEncodeAsHtmlAttribute);
}
Filter based on Action attributes

The ActionExecutingContext.ActionDescriptor.EndpointMetadata returns a list of the attributes applied in the action itself.  In the next listing I show you how to get access to the Parameters that are going to be passed to the action after the model binding has been applied.

public override void OnActionExecuting(ActionExecutingContext context)
{
    ... 
    var inspectArguments = context
    	.ActionArguments
        .Where(x => ShouldInspectObject(x.Value))
        .ToList();

    if (!inspectArguments.Any())
        return;
}

private static bool ShouldInspectObject(object? value)
{
    if (value is null)
        return false;

    bool hasIgnoreAttribute = value.GetType()
    	.GetCustomAttribute<NeverEncodeAsHtmlAttribute>() is not null;

    if (hasIgnoreAttribute)
        return false;

    return value is IRequireHtlmEncoding;
}

The list of arguments is contained in ActionExecutingContext.ActionArguments, the ShouldInspectObject makes sure that two conditions are meet: the Argument cannot be marked with NeverEncodeAsHtmlAttribute and it should implement the IRequireHtlmEncoding interface.

Now that we have the list of arguments to be updated, we need to start the introspection, lets create a method for that.

public override void OnActionExecuting(ActionExecutingContext context)
{
    ... 
    foreach (var argument in inspectArguments)
    {
        var value = argument.Value;
        switch (value) {
        case IRequireHtlmEncoding encodeObject:
            InspectAndEncodeObjectSringProperties(encodeObject);
            break;
        }
    }
}

private static void InspectAndEncodeObjectSringProperties(
    IRequireHtlmEncoding objectToEncodeStringProperties)
{
    // To Do
}
Start Arguments introspection

Before changing any property of the object, we need to make sure again that such property is not being mark with the same NeverEncodeAsHtmlAttribute, we can use reflection for that.

private static void InspectAndEncodeObjectSringProperties(
    IRequireHtlmEncoding objectToEncodeStringProperties)
{
    if (objectToEncodeStringProperties == null)
        return;

    var propertiesToInspect = objectToEncodeStringProperties.GetType()
              .GetProperties(BindingFlags.Public | BindingFlags.Instance)
              .Where(p => p.GetCustomAttribute<NeverEncodeAsHtmlAttribute>() is null)
              .ToList();

    foreach (var property in propertiesToInspect) {
        // To Do
    }
}
Get the properties that need to inspect

The next step is the actual encoding the property as you can see in the next listing.


private static void InspectAndEncodeObjectSringProperties(
    IRequireHtlmEncoding objectToEncodeStringProperties)
{
    ... 
    foreach (var property in propertiesToInspect)
    {
        string propertyName = property.Name;

        if (property.PropertyType == typeof(string)) {
            if (property.SetMethod != null) {
                string? strValue = (string?) property.GetValue(objectToEncodeStringProperties);
                property.SetValue(objectToEncodeStringProperties, HtmlEnconde(strValue));
            }
        }
    }
}

private static string? HtmlEnconde(string? original)
{
    return original is null ? null : WebUtility.HtmlEncode(original);
}
Actual html encoding

Before changing a property, we need to make sure that it has some sort of ‘set’, then we can modify it with the same value but this time properly encoded, the actual encoding can be done through the WebUtility class. Lastly let’s deal with nested objects.

private static void InspectAndEncodeObjectSringProperties(
    IRequireHtlmEncoding objectToEncodeStringProperties)
{
    ... foreach (var property in propertiesToInspect)
    {
        string propertyName = property.Name;

        if (property.PropertyType == typeof(string)) 
        {
            ...
        } 
        else
        {
            object? propertyValue
                = property.GetValue(objectToEncodeStringProperties);
            if (ShouldInspectObject(propertyValue)) 
            {
                switch (propertyValue) 
                {
                	case IRequireHtlmEncoding encodeInnerObject:
                   	InspectAndEncodeObjectSringProperties(encodeInnerObject);
                    break;
                }
            }
        }
    }
}
Dealing wih nesteed objects

Reflection and recursiveness are our friends here, we basically need check value and based on its type call the same InspectAndEncodeObjectSringProperties method. We’ve just have finished with the filter, but we need to register the filter with the container.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options => {
        options.Filters.Add(new HtmlEncodeActionFilterAttribute());
    });
}
Add the filter to the IoC Container

To register the filter, you need to added it to the Filters collection of the MvcOptions object while registering the controllers.

Summary

Actions filters, and filters in general are part of the MVC pipeline and can be useful when it comes to expanding the framework itself and are not that difficult to implement. Today we saw a practical example of how you can do one filter to make sure request are safely using html encoding without polluting the code in your controllers or you application. Hope you find it useful and remember that you can check out the source code here.