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.