I want a server application to send webhooks (i.e. outgoing http-post requests) to user-controlled http(s) URLs. A naive implementation of that functionality is vulnerable to server-side request forgery (SSRF), since the target host could be within the same private network as the server. To prevent that, I would like to blacklist private IP addresses.
On classic .NET, HttpWebRequest supported this via the ServicePoint.BindIPEndPointDelegate, which received the IP address the client connects to. However in .NET Core, HttpWebRequest was turned into a wrapper over HttpClient, and this callback is ignored completely. This introduces an SSRF vulnerability into previously secure applications.
But even migrating from the deprecated using HttpWebRequest to using HttpClient directly doesn't appear to help. I could not find any callback in HttpClientHandler that received the IP address before connecting to it.
Resolving the domain to an IP address before passing the original URL to HttpClient doesn't work reliably either. This is complex since it needs to handle multiple returned IP addresses, and it's vulnerable to ToC/ToU, since the name resolution might return a different response for the check and the request execution. This also requires manual handling of HTTP redirects, since the IP addresses of the redirect target(s) need to be checked as well.
Another idea is to resolve the domain, fill the host-to-connect-to with the IP, and the original domain in the http host header (if HttpClient even supports that). This also requires manual handling of HTTP redirects, making it complex and fragile.
There is also the option to handle it outside the application (via proxies, firewalls, etc.), but that doesn't feel like a clean solution either and increases infrastructure cost and complexity.
Is there a reasonable way to support sending webhooks using HttpClient without suffering from SSRF? This feels like a pretty common use-case, which shouldn't require complex workarounds.
SocketsHttpHandlernot onHttpClientHandler. I'll verify tomorrow if that callback actually works for my use-cases.