dotnet / dotnet/runtime

[API Proposal]: Add IP Validation Callback to `SocketsHttpHandler`

Open
#132,403 5 comments 0 reactions 0 assignees View on GitHub
api-suggestion area-System.Net.Http
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### 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](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery).

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](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)).

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

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

public class SocketsHttpHandler
{
...
public Func>? IpValidation { get; set; }
...
}
```

### API Usage

```csharp

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:

```csharp
var client = new HttpClient
{
BlockRequestsToLocalIpAddresses = true
}
```

### Alternative Designs

My current workaround is this:

```csharp
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 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.

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the SocketsHttpHandler API proposal and the alternative ConnectCallback implementation in the issue, then locate the existing SocketsHttpHandler and connection setup entry points in the runtime. Done means reaching agreement on the API design, integrating IP validation into connection handling, and adding coverage that verifies blocked addresses and relevant connection behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend-api-design, networking, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.