Saturday, August 24, 2019

Client IP safelist for ASP.NET Core

This article shows three ways to implement an IP safelist (also known as a whitelist) in an ASP.NET Core app. You can use:
  • Middleware to check the remote IP address of every request.
  • Action filters to check the remote IP address of requests for specific controllers or action methods.
  • Razor Pages filters to check the remote IP address of requests for Razor pages.
In each case, a string containing approved client IP addresses is stored in an app setting. The middleware or filter parses the string into a list and checks if the remote IP is in the list. If not, an HTTP 403 Forbidden status code is returned.

The safelist

The list is configured in the appsettings.json file. It's a semicolon-delimited list and can contain IPv4 and IPv6 addresses.
JSON
{ "AdminSafeList": "127.0.0.1;192.168.1.5;::1", "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } }

Middleware

The Configure method adds the middleware and passes the safelist string to it in a constructor parameter.
C#
public void Configure( IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddNLog(); app.UseStaticFiles(); app.UseMiddleware<AdminSafeListMiddleware>(Configuration["AdminSafeList"]); app.UseMvc(); }
The middleware parses the string into an array and looks for the remote IP address in the array. If the remote IP address is not found, the middleware returns HTTP 401 Forbidden. This validation process is bypassed for HTTP Get requests.
C#
public class AdminSafeListMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<AdminSafeListMiddleware> _logger;
    private readonly string _adminSafeList;

    public AdminSafeListMiddleware(
        RequestDelegate next, 
        ILogger<AdminSafeListMiddleware> logger, 
        string adminSafeList)
    {
        _adminSafeList = adminSafeList;
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Method != "GET")
        {
            var remoteIp = context.Connection.RemoteIpAddress;
            _logger.LogDebug($"Request from Remote IP address: {remoteIp}");

            string[] ip = _adminSafeList.Split(';');

            var bytes = remoteIp.GetAddressBytes();
            var badIp = true;
            foreach (var address in ip)
            {
                var testIp = IPAddress.Parse(address);
                if(testIp.GetAddressBytes().SequenceEqual(bytes))
                {
                    badIp = false;
                    break;
                }
            }

            if(badIp)
            {
                _logger.LogInformation(
                    $"Forbidden Request from Remote IP address: {remoteIp}");
                context.Response.StatusCode = 401;
                return;
            }
        }

        await _next.Invoke(context);
    }
}

Action filter

If you want a safelist only for specific controllers or action methods, use an action filter. Here's an example:
C#
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace ClientIpAspNetCore.Filters
{
    public class ClientIdCheckFilter : ActionFilterAttribute
    {
        private readonly ILogger _logger;
        private readonly string _safelist;

        public ClientIdCheckFilter
            (ILoggerFactory loggerFactory, IConfiguration configuration)
        {
            _logger = loggerFactory.CreateLogger("ClientIdCheckFilter");
            _safelist = configuration["AdminSafeList"];
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            _logger.LogInformation(
                $"Remote IpAddress: {context.HttpContext.Connection.RemoteIpAddress}");

            var remoteIp = context.HttpContext.Connection.RemoteIpAddress;
            _logger.LogDebug($"Request from Remote IP address: {remoteIp}");

            string[] ip = _safelist.Split(';');

            var bytes = remoteIp.GetAddressBytes();
            var badIp = true;
            foreach (var address in ip)
            {
                var testIp = IPAddress.Parse(address);
                if (testIp.GetAddressBytes().SequenceEqual(bytes))
                {
                    badIp = false;
                    break;
                }
            }

            if (badIp)
            {
                _logger.LogInformation(
                    $"Forbidden Request from Remote IP address: {remoteIp}");
                context.Result = new StatusCodeResult(401);
                return;
            }

            base.OnActionExecuting(context);
        }
    }
}
The action filter is added to the services container.
C#
public void ConfigureServices(IServiceCollection services) { services.AddScoped<ClientIdCheckFilter>(); services.AddMvc(options => { options.Filters.Add (new ClientIdCheckPageFilter (_loggerFactory, Configuration)); }).SetCompatibilityVersion(CompatibilityVersion.Version_2_1); }
The filter can then be used on a controller or action method.
C#
[ServiceFilter(typeof(ClientIdCheckFilter))] [HttpGet] public IEnumerable<string> Get()
In the sample app, the filter is applied to the Get method. So when you test the app by sending a Get API request, the attribute is validating the client IP address. When you test by calling the API with any other HTTP method, the middleware is validating the client IP.

Razor Pages filter

If you want a safelist for a Razor Pages app, use a Razor Pages filter. Here's an example:
C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Net;

namespace ClientIpAspNetCore
{
    public class ClientIdCheckPageFilter : IPageFilter
    {
        private readonly ILogger _logger;
        private readonly string _safelist;

        public ClientIdCheckPageFilter
            (ILoggerFactory loggerFactory, IConfiguration configuration)
        {
            _logger = loggerFactory.CreateLogger("ClientIdCheckPageFilter");
            _safelist = configuration["AdminSafeList"];
        }

        public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
        {
            _logger.LogInformation(
                $"Remote IpAddress: {context.HttpContext.Connection.RemoteIpAddress}");

            var remoteIp = context.HttpContext.Connection.RemoteIpAddress;
            _logger.LogDebug($"Request from Remote IP address: {remoteIp}");

            string[] ip = _safelist.Split(';');

            var bytes = remoteIp.GetAddressBytes();
            var badIp = true;
            foreach (var address in ip)
            {
                var testIp = IPAddress.Parse(address);
                if (testIp.GetAddressBytes().SequenceEqual(bytes))
                {
                    badIp = false;
                    break;
                }
            }

            if (badIp)
            {
                _logger.LogInformation(
                    $"Forbidden Request from Remote IP address: {remoteIp}");
                context.Result = new StatusCodeResult(401);
                return;
            }
        }

        public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
        {
        }

        public void OnPageHandlerSelected(PageHandlerSelectedContext context)
        {
        }
    }
}
This filter is enabled by adding it to the MVC Filters collection.
C#
public void ConfigureServices(IServiceCollection services) { services.AddScoped<ClientIdCheckFilter>(); services.AddMvc(options => { options.Filters.Add (new ClientIdCheckPageFilter (_loggerFactory, Configuration)); }).SetCompatibilityVersion(CompatibilityVersion.Version_2_1); }
When you run the app and request a Razor page, the Razor Pages filter is validating the client IP.

No comments:

Post a Comment

How to register multiple implementations of the same interface in Asp.Net Core?

 Problem: I have services that are derived from the same interface. public interface IService { } public class ServiceA : IService { ...