Skip to content

[API Proposal]: Add IP Validation Callback to SocketsHttpHandler #132403

Description

@winnerlein

Background and motivation

When sending http requests to attacker controlled domains, the attacker can resolve their domain to an IP address in the local network, which wouldn't be reachable from the public internet. If an application doesn't block such requests, it suffers from an Server-Side-Request-Forgery (SSRF) vulnerability.

A use-case where this applies directly are webhooks, since untrusted users can determine the target url. But this can also happen when contacting trusted services, if these get their DNS compromised.

It is not possible to prevent this by validating the url. Not only do you need to consider redirects, but the DNS server can return different data at the time the url is validated and the time the http client connects (a TOCTOU vulnerability).

Since reliably preventing this is difficult using the current API, I think it would be a good idea to extend the API to handle IP address validation natively. I think an IP address validation callback on SocketsHttpHandler would be the right low-level place for that. But it could make sense to additionally add a high level API like bool BlockRequestsToLocalIpAddresses on either SocketsHttpHandler or HttpClient.

API Proposal

public class SocketsHttpIpValidationContext
{
    public IpAddress IpAddress { get; }
}

public class SocketsHttpHandler
{
    ...
    public Func<SocketsHttpIpValidationContext, CancellationToken, ValueTask<SocketsHttpIpValidationError?>>? IpValidation { get; set; }
    ...
}

API Usage

async SocketsHttpIpValidationError? ValidateAddress(SocketsHttpIpValidationContext context, CancellationToken cancellationToken)
{
    if(IsBlocked(context.IpAddress))
        return new SocketsHttpIpValidationError("This IP is blocked");
    return null;
}

var client = new HttpClient(new SocketsHttpHandler
{
    IpValidation = ValidateAddress,
});

or if the high level API is implemented:

var client = new HttpClient
{
    BlockRequestsToLocalIpAddresses = true
}

Alternative Designs

My current workaround is this:

var client = new HttpClient(new SocketsHttpHandler
{
    ConnectCallback = RemoteIpBlocker.HttpClientConnectCallback,
});

// Ideally we'd validate every remote IP address before the socket connects to it
// However doing that properly would require duplicating complex code from Socket (connecting to every IP address returned by DNS in turn)
// So instead I decided to combine two solutions which are flawed in different way.
// 1. Querying DNS and validating each IP it returns
//    This is flawed, because when the client queries DNS again, it might return different results
//    This is known as a Time-of-check to time-of-use (ToCToU) vulnerability
//    DNS being cached means exploiting this race condition is very difficult in practice
// 2. Validating the IP address the socket connected to, after it connects, but before any data is sent
//    This reliably blocks SSRF requests from having an actual effect
//    However it does still leak that the IP/port were reachable
// Combining these results in a difficult to exploit vulnerability with a small impact.
public static async ValueTask<Stream> HttpClientConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
    var ipAddresses = await Dns.GetHostAddressesAsync(context.DnsEndPoint.Host, context.DnsEndPoint.AddressFamily, cancellationToken).ConfigureAwait(false);

    foreach (var ip in ipAddresses)
        ValidateIp(ip);

    var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
    {
        NoDelay = true,
    };

    try
    {
        await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
        var remoteIp = ((IPEndPoint)socket.RemoteEndPoint!).Address;
        ValidateIp(remoteIp);
        return new NetworkStream(socket, true);
    }
    catch (Exception)
    {
        socket.Dispose();
        throw;
    }
}

But's that's rather complex, and still doesn't fully fix the vulnerability.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.Net.HttpuntriagedNew issue has not been triaged by the area owner

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions