[API Proposal]: Add IP Validation Callback to `SocketsHttpHandler`
- 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
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