Request Filtering for ASP.NET Core applications: Part 2 - Building Abstractions & Implementations APIs

Sep 24, 2016     Viewed 8309 times    0 Comments
Posted in #Request Filtering 

This is the second post of a series in Request Filtering in ASP.NET Core. Last post I gave an overview of the Request Filtering, and explain little bit about IIS Request Filtering. In this post I will try to build the abstractions & implementation API's to support Request Filtering in ASP.NET Core applications, also I'm gonna to explore different techniques for Request Filtering like what IIS offers out of the box.

Abstraction APIs

As we saw in last post, IIS offers many types of request filtering base on Url, file extension .. etc. I was thinking to create a build a generic foundation for the request filtering, so you can create any type of filtering in the future and plug it into your the application easily.

Now let's see some code, but before that I wanna mention that I will follow the same pattern that many of the aspnet repositories follow when they writing the APIs.

public interface IRequestFilter
{
    void ApplyFilter(RequestFilteringContext context);
}
public interface IRequestFilter<T> : IRequestFilter
{

}

I started by creating a simple interface called IRequestFilter which has a single method to apply or execute the custom filtering logic, also I created a generic version to that interface to create our next class.

public abstract class RequestFilter<TOptions> : IRequestFilter<TOptions> where TOptions : IRequestFilterOptions
{
     public abstract TOptions Options { get; }

     public virtual void ApplyFilter(RequestFilteringContext context)
     {
        context.Result = RequestFilteringResult.Continue;
     }
}

As we saw before the RequestFilter<TOptions> is an abstract class that allow the implementation classes to pass IRequestFilterOptions implementation which is nothing but options for the request filter. Some of you may ask, what's the RequestFilteringContext? It is a simple context defines as the following:

public class RequestFilteringContext
{
    public HttpContext HttpContext { get; set; }

    public RequestFilteringResult Result { get; set; }
}

which let you access the HttpContext for filtering purpose, also a RequestFilteringResult which is an enumeration that let us know if we need to continue or stop filtering

public enum RequestFilteringResult
{
    Continue,
    StopFilters
}

Also may we need an options for the request filtering to access all the registered filters.

public class RequestFilteringOptions
{
    public IList Filters { get; } = new List();
}

After that need more three classes one is an extensions for the RequestFilteringOptions and two required for the middleware.

public static class RequestFilteringOptionsExtensions
{
    public static RequestFilteringOptions AddRequestFilter(this RequestFilteringOptions requestFilteringOptions, IRequestFilter filter)
    {
        if (filter == null)
        {
            throw new ArgumentNullException(nameof(filter));
        }

        requestFilteringOptions.Filters.Add(filter);
        return requestFilteringOptions;
    }
}
public class RequestFilteringMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RequestFilteringOptions _options;

    public RequestFilteringMiddleware(
        RequestDelegate next,
        RequestFilteringOptions options)
    {
        if (next == null)
        {
            throw new ArgumentNullException(nameof(next));
        }

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

        _next = next;
        _options = options;
    }

    public Task Invoke(HttpContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var requestFilteringContext = new RequestFilteringContext
        {
            HttpContext = context,
            Result = RequestFilteringResult.Continue
        };

        foreach (var filter in _options.Filters)
        {
            filter.ApplyFilter(requestFilteringContext);

            switch (requestFilteringContext.Result)
            {
                case RequestFilteringResult.Continue:
                    break;
                case RequestFilteringResult.StopFilters:
                    return Task.FromResult(0);
                default:
                    throw new ArgumentOutOfRangeException($"Invalid filter termination {requestFilteringContext.Result}");
            }
         }

         return _next(context);
    }
}
public static class RequestFilteringMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestFiltering(this IApplicationBuilder app, RequestFilteringOptions options)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

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

        return app.UseMiddleware(options);
    }
}

The middleware classes is straightforward for those who writing ASP.NET middleware before, the idea for our middleware is simple, iterate over all the request filtering that have been registered an calling the ApplyFilter which execute the actual filter, after that we gathering the result from each filter and check the Result property in the context to know if we need to go further to the next filter, or the actual filter is applied and no need to keep filtering.

Implementation APIs

Now we are ready to implement the basic request filtering techniques that are available in the IIS. FYI I will dig only into the essentials classes for each type.

Filter based on File Extensions

As we saw in the previous post that file extensions filter have AllowUnlisted, FileExtension and Allowed properties, so we can use our abstraction to define a set of options as the following:

public class FileExtensionsOptions : IRequestFilterOptions
{
     public bool AllowUnlisted { get; set; } = true;

     public IList FileExtensionsCollection { get; set; } = new List();
}
public class FileExtensionsElement
{
    public string FileExtension { get; set; }

    public bool Allowed { get; set; }
}

Then we can implement FileExtensionRequestFilter which is basically checks the extension of request path whether allowed or not.

public class FileExtensionRequestFilter : RequestFilter<FileExtensionsOptions>
{
    public FileExtensionRequestFilter() : this(new FileExtensionsOptions())
    {

    }

    public FileExtensionRequestFilter(FileExtensionsOptions options)
    {
        Options = options;
    }

    public override FileExtensionsOptions Options { get; }

    public override void ApplyFilter(RequestFilteringContext context)
    {
        var extension = Path.GetExtension(context.HttpContext.Request.Path.Value);

        if (Options.AllowUnlisted)
        {
            if (Options.FileExtensionsCollection.Any(f => f.FileExtension == extension && f.Allowed == false))
            {
                context.HttpContext.Response.StatusCode = 404;
                context.Result = RequestFilteringResult.StopFilters;
                return;
            }

            context.Result = RequestFilteringResult.Continue;
        }
        else
        {
            if (Options.FileExtensionsCollection.Any(f => f.FileExtension == extension && f.Allowed == true))
            {
                context.Result = RequestFilteringResult.Continue;
                return;
            }

            context.HttpContext.Response.StatusCode = 404;
            context.Result = RequestFilteringResult.StopFilters;
        }
    }
}

Filter by Verbs

As we saw in the previous post that HTTP verbs filter have AllowUnlisted, Verb and Allowed properties, so we can use our abstraction to define a set of options as the following:

public class HttpVerbsOptions : IRequestFilterOptions
{
    public bool AllowUnlisted { get; set; } = true;

    public IList HttpVerbsCollection { get; set; }
}
public class HttpVerbElement
{
    public HttpVerb Verb { get; set; }

    public bool Allowed { get; set; }
}

Then we can implement HttpVerbRequestFilter which is basically checks the request method whether is it allowed or not

public class HttpVerbRequestFilter : RequestFilter<HttpVerbsOptions>
{<>
    public HttpVerbRequestFilter() : this(new HttpVerbsOptions())
    {

    }

    public HttpVerbRequestFilter(HttpVerbsOptions options)
    {
        Options = options;
    }

    public override HttpVerbsOptions Options { get; }

    public override void ApplyFilter(RequestFilteringContext context)
    {
        var verb = context.HttpContext.Request.Method;

        if (Options.AllowUnlisted)
        {
            if (Options.HttpVerbsCollection.Any(v => v.Verb.ToString().Equals(verb, StringComparison.OrdinalIgnoreCase) && v.Allowed == false))
            {
                context.HttpContext.Response.StatusCode = 404;
                context.Result = RequestFilteringResult.StopFilters;
                return;
            }

            context.Result = RequestFilteringResult.Continue;
        }
        else
        {
            if (Options.HttpVerbsCollection.Any(v => v.Verb.ToString().Equals(verb, StringComparison.OrdinalIgnoreCase) && v.Allowed == true))
            {
                context.Result = RequestFilteringResult.Continue;
                return;
            }

            context.HttpContext.Response.StatusCode = 404;
            context.Result = RequestFilteringResult.StopFilters;
        }
    }
}

Filter Based on URL Sequences

As we saw in the previous post that URL sequences filter have a denied URL sequence collection and allowed URLs properties, so we can use our abstraction to define a set of options as the following:

public class UrlsOptions : IRequestFilterOptions
{
    public IList AllowedUrls { get; set; }

    public IList DeniedUrlSequences { get; set; }
}

Then we can implement UrlRequestFilter which is basically checks the request path whether it contains a denied sequence.

public class UrlRequestFilter : RequestFilter<UrlsOptions>
{
    public UrlRequestFilter() : this(new UrlsOptions())
    {

    }

    public UrlRequestFilter(UrlsOptions options)
    {
        Options = options;
    }

    public override UrlsOptions Options { get; }

    public override void ApplyFilter(RequestFilteringContext context)
    {
        var url = context.HttpContext.Request.Path.Value;

        if (Options.AllowedUrls.Contains(url))
        {
            context.Result = RequestFilteringResult.Continue;
        }
        else
        {
            Options.DeniedUrlSequences.ToList().ForEach(s =>
            {
                if (url.Contains(s))
                {
                    context.HttpContext.Response.StatusCode = 404;
                    context.Result = RequestFilteringResult.StopFilters;
                }
            });
        }
    }
}

Filter Based on Query Strings

As we saw in the previous post that query strings filter have AllowUnlisted, QueryString and Allowed properties, so we can use our abstraction to define a set of options as the following:

public class QueryStringElement
{
    public string QueryString { get; set; }

    public bool Allowed { get; set; }
}
public class QueryStringsOptions : IRequestFilterOptions
{
    public bool AllowUnlisted { get; set; } = true;

    public IList QueryStringsCollection { get; set; } = new List();
}

Then we can implement QueryStringRequestFilter which is basically checks the query strings of request whether allowed or not.

public class QueryStringRequestFilter : RequestFilter<QueryStringsOptions>
{
    public QueryStringRequestFilter() : this(new QueryStringsOptions())
    {

    }

    public QueryStringRequestFilter(QueryStringsOptions options)
    {
        Options = options;
    }

    public override QueryStringsOptions Options { get; }

    public override void ApplyFilter(RequestFilteringContext context)
    {
        if (!context.HttpContext.Request.QueryString.HasValue)
        {
            context.Result = RequestFilteringResult.Continue;
            return;
        }

        if (Options.AllowUnlisted)
        {
            if (Options.QueryStringsCollection.Any(q => context.HttpContext.Request.Query[q.QueryString].SingleOrDefault() != null && q.Allowed == false))
            {
                context.HttpContext.Response.StatusCode = 404;
                context.Result = RequestFilteringResult.StopFilters;
                return;
            }

            context.Result = RequestFilteringResult.Continue;
        }
        else
        {
            if (Options.QueryStringsCollection.Any(q => context.HttpContext.Request.Query[q.QueryString].SingleOrDefault() != null && q.Allowed == true))
            {
                context.Result = RequestFilteringResult.Continue;
                return;
            }

            context.HttpContext.Response.StatusCode = 404;
            context.Result = RequestFilteringResult.StopFilters;
        }
    }
}

Filter Out Hidden Segments

As we saw in the previous post that hidden segments filter have segments collection properties, so we can use our abstraction to define a set of options as the following:

public class HiddenSegmentsOptions : IRequestFilterOptions
{
    public IList HiddenSegmentsCollection { get; set; }
}
public class HiddenSegmentElement
{
    public string Segment { get; set; }
}

Then we can implement HiddenSegmentRequestFilter which is basically checks the request path whether it contains a hidden segment to block or not.

public class HiddenSegmentRequestFilter : RequestFilter<HiddenSegmentsOptions>
{
    public HiddenSegmentRequestFilter() : this(new HiddenSegmentsOptions())
    {

    }

    public HiddenSegmentRequestFilter(HiddenSegmentsOptions options)
    {
        Options = options;
    }

    public override HiddenSegmentsOptions Options { get; }

    public override void ApplyFilter(RequestFilteringContext context)
    {
        var path = context.HttpContext.Request.Path.Value;
        var segments = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);

        if (segments.Length == 0)
        {
            context.Result = RequestFilteringResult.Continue;
            return;
        }

        if (Options.HiddenSegmentsCollection.Any(s => segments.Contains(s.Segment)))
        {
            context.HttpContext.Response.StatusCode = 404;
            context.Result = RequestFilteringResult.StopFilters;
            return;
        }

        context.Result = RequestFilteringResult.Continue;
    }
}

Filter based on Request Limits

As we saw in the previous post that request limits filter have MaxAllowedContentLength , MaxUrl, MaxQueryString, Header and SizeLimit properties, so we can use our abstraction to define a set of options as the following:

public class HeadersOptions : IRequestFilterOptions
{
    public long MaxAllowedContentLength { get; set; } = 30000000;

    public long MaxUrl { get; set; } = 4096;

    public long MaxQueryString { get; set; } = 2048;

    public IList HeadersCollection { get; set; } = new List();
}
public class HeaderElement
{
    public string Header { get; set; }

    public long SizeLimit { get; set; }
}

Then we can implement HeaderRequestFilter which is basically checks the request header is exceeds the limit that predefined or not.

public class HeaderRequestFilter : RequestFilter<HeadersOptions>
{
    public HeaderRequestFilter() : this(new HeadersOptions())
    {

    }

    public HeaderRequestFilter(HeadersOptions options)
    {
        Options = options;
    }

    public override HeadersOptions Options { get; }

    public override void ApplyFilter(RequestFilteringContext context)
    {
        if (Options.MaxAllowedContentLength < context.HttpContext.Request.ContentLength)
        {
            context.HttpContext.Response.StatusCode = 404;
            context.Result = RequestFilteringResult.StopFilters;
        }

        if (Options.MaxQueryString < context.HttpContext.Request.QueryString.Value.Length)
        {
            context.HttpContext.Response.StatusCode = 404;
            context.Result = RequestFilteringResult.StopFilters;
        }

        if (Options.MaxUrl < context.HttpContext.Request.GetDisplayUrl().Length)
        {
            context.HttpContext.Response.StatusCode = 404;
            context.Result = RequestFilteringResult.StopFilters;
        }

        Options.HeadersCollection.ToList().ForEach(header =>
        {
            if (context.HttpContext.Request.Headers.Keys.Contains(header.Header) && context.HttpContext.Request.Headers[header.Header].ToString().LongCount() > header.SizeLimit)
            {
                context.HttpContext.Response.StatusCode = 404;
                context.Result = RequestFilteringResult.StopFilters;
                return;
            }
        });

        context.Result = RequestFilteringResult.Continue;
    }
}

That is it!! I know it's a long journey, but If you notice that we implemented almost the filters that are available in the IIS Request Filtering module, with the minimum amount of code.

You can download the source code for this post from my RequestFiltering repository.

Twitter Facebook Google + LinkedIn


Leave a Comment