CodesInChaos
CodesInChaos

Reputation: 108840

Preventing SSRF when sending Webhooks via HttpClient

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.

Upvotes: 1

Views: 191

Answers (1)

sa-es-ir
sa-es-ir

Reputation: 5082

I retracted my vote to duplicate the question because that question doesn't answer the question but the solution is similar.

I've tried this way and was able to get the remote IP before getting data from it.


SocketsHttpHandler handler = new SocketsHttpHandler();

handler.ConnectCallback = async (context, cancellationToken) =>
{
    Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

    try
    {
        await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);

        var erp = socket.RemoteEndPoint;

        var remoteAddr = ((IPEndPoint)erp!).Address;

        var ipV4 = remoteAddr.MapToIPv4().ToString();
        Console.WriteLine($"IP V4: {ipV4}");

        var ipV6 = remoteAddr.MapToIPv6().ToString();

        Console.WriteLine($"IP V6: {ipV6}");

        //----> If the IPs is in black list, we can throw an exception here

        return new NetworkStream(socket, true);
    }
    catch
    {
        socket.Dispose();

        throw;
    }
};

var client = new HttpClient(handler);

var response = await client.GetAsync("https://stackoverflow.com");

If you do a ping stackoverflow.com it's possible to check the IPs.

Upvotes: 0

Related Questions