Coding Stephan

Modify a HttpClientHandler with Dependency Injection

In the previous post we have seen how you can protect your application to Man-in-the-middle attacks. The sample code there, was not showing how you could do the same if you are not managing the HttpClient yourself. In this post we will see how you can utilize this with dependency injection and the Microsoft.Extensions.Http package, which allows you to also protect your application if you’re using dependency injection to manage your HttpClient instances.

The problem

When your application is talking to external systems over HTTP, you’re trusting the default SSL/TLS implementation of the device. These devices trust the locally installed root certificates, and thus are susceptible to being modified by the client. If you don’t want your traffic to be intercepted and/or modified, you also need to verify the exact certificate or the used chain of certificates. More details, in the previous post.

Stop managing the HttpClient

These days everyone is using dependency injection to manage the lifetime of used services. This is also true for the HttpClient. In the past, we were advised to manage the HttpClient ourselves, and dispose it after use. This was to prevent socket exhaustion, but it also meant that you had to manage the HttpClientHandler yourself. This is not the case anymore, and you should not manage the HttpClient yourself. The Microsoft.Extensions.Http package will help you with this.

Microsoft really tried to make it as easy as possible to have them manage the HttpClient for you. They pool the HttpClientHandler instances, and reuse them as much as possible. Not having to manage lifetime of the client handler also means that it’s kind of hidden out of sight. You can still modify it though. The sample code below is the WorkerService template from the latest .NET 8 SDK with the Microsoft.Extensions.Http package added.

using WorkerService1;

var builder = Host.CreateApplicationBuilder(args);
// This registers the IHttpClientFactory and a default HttpClient to the service collection
builder.Services.AddHttpClient();
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();

Please stop using using statements for you HttpClient. As of .NET 6 disposing it is no longer needed, though is would still cancel all the ongoing requests. Register the HttpClient in your service provider instead. Have it injected in your Service through constructor injection and let the library manage it for you.

Using the HttpClient in your service

namespace WorkerService1;

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly HttpClient _httpClient;

    public Worker(ILogger<Worker> logger, HttpClient httpClient)
    {
        _logger = logger;
        _httpClient = httpClient;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            if (_logger.IsEnabled(LogLevel.Information))
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            }
            // Do something with the _httpClient, not a real example
            var response = await _httpClient.GetAsync("https://www.google.com", stoppingToken);
            _logger.LogInformation("Response: {response} {success}", response.StatusCode, (response.IsSuccessStatusCode ? "✅" : "❌"));

            // A Task.Delay with the stoppingToken will throw an OperationCanceledException when the token is cancelled
            // meaning you can exit quickly when requested
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Configure the HttpClientHandler

But wait, you just told me that we have to modify the HttpClientHandler to protect to protect against man-in-the-middle attacks, right? How does that work when you don’t manage the HttpClientHandler?

The AddHttpClient() method returns an IServiceCollection so no help there. You can still chain it with the ConfigureHttpClientDefaults(http => {...}) method to configure the default http client, this gives you access to a IHttpClientBuilder which you can use to configure the PrimaryHttpMessageHandler.

builder.Services.AddHttpClient()
    .ConfigureHttpClientDefaults(http =>
    {
        http.ConfigurePrimaryHttpMessageHandler(() =>
        {
            var handler = new HttpClientHandler();
            handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
            {
                // Your custom validation logic, from https://svrooij.io/2024/02/22/nuke-man-middle-attack/#validate-requests-to-microsoft-graph-api
                return true;
            };
            return handler;
        });
    });

You can also use a static method to handle the ServerCertificateCustomValidationCallback:

public class MyHttpHandlerValidator
{
  public static Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> GraphApiValidator => (message, cert, chain, errors) =>
  {
    // Can someone tell me if this is the default?
  bool defaultResult = errors == System.Net.Security.SslPolicyErrors.None;
  if (!defaultResult) {
    return false; //Fail fast, if the default validation fails.
  }

  if (message.RequestUri!.Host == "graph.microsoft.com")
  {
    // Microsoft uses DigiCert, so we can check the thumbprint of the root certificate.
    bool graphResult = chain!.ChainElements.Last().Certificate.Thumbprint == "A8985D3A65E5E5C4B2D7D66D40C6DD2FB19C5436";
    if (!graphResult)
    {
        Console.WriteLine("Got cert with name {0} and thumb {1} for Graph", cert!.Subject, cert.Thumbprint);
        foreach (var element in chain!.ChainElements)
        {
            Console.WriteLine("Element: {0} thumb: {1}", element.Certificate.Subject, element.Certificate.Thumbprint);
        }
    }
    return graphResult;
  }

  return true;

  }
}

And use it in the ConfigurePrimaryHttpMessageHandler method:

builder.Services.AddHttpClient()
    .ConfigureHttpClientDefaults(http =>
    {
        http.ConfigurePrimaryHttpMessageHandler(() =>
        {
            var handler = new HttpClientHandler();
            handler.ServerCertificateCustomValidationCallback = MyHttpHandlerValidator.GraphApiValidator;
            return handler;
        });
    });

Conclusion

In this post I showed you how you can protect your application from man-in-the-middle attacks when you’re using dependency injection to manage your HttpClient (which you definitely need if you’re running this on a server). The Microsoft.Extensions.Http package makes it easy to manage the HttpClient and still allows you to modify the HttpClientHandler to protect your apps.

I hope this post was useful to you and if you have any questions, let me know on Twitter or LinkedIn.