84 lines
3.1 KiB
C#
84 lines
3.1 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
|
|
namespace w4c_workflows.Services.Security;
|
|
|
|
/// <summary>
|
|
/// Builds the <see cref="SocketsHttpHandler"/> used for workflow egress with a
|
|
/// <see cref="SocketsHttpHandler.ConnectCallback"/> that resolves the target and
|
|
/// re-validates every address against the <see cref="EgressPolicy"/> at the moment
|
|
/// of connecting.
|
|
///
|
|
/// This closes the DNS-rebinding (TOCTOU) hole: the pre-flight
|
|
/// <see cref="EgressGuard.AuthorizeAsync"/> check and the actual socket connect no
|
|
/// longer rely on two independent DNS resolutions that an attacker-controlled
|
|
/// name could answer differently. The socket is opened against a vetted IP while
|
|
/// the request keeps its original <c>Host</c>/SNI, and a host that resolves to any
|
|
/// blocked address is refused outright — matching
|
|
/// <see cref="EgressPolicy.CheckAddresses"/> semantics.
|
|
/// </summary>
|
|
public static class EgressPinning
|
|
{
|
|
public static SocketsHttpHandler CreateHandler(
|
|
EgressPolicy policy,
|
|
IHostAddressResolver resolver,
|
|
bool allowAutoRedirect = false,
|
|
bool allowInsecureTls = false)
|
|
{
|
|
var handler = new SocketsHttpHandler
|
|
{
|
|
AllowAutoRedirect = allowAutoRedirect,
|
|
ConnectCallback = async (context, ct) =>
|
|
{
|
|
var endpoint = context.DnsEndPoint;
|
|
|
|
IReadOnlyList<IPAddress> addresses;
|
|
if (IPAddress.TryParse(endpoint.Host, out var literal))
|
|
addresses = new[] { literal };
|
|
else
|
|
addresses = await resolver.ResolveAsync(endpoint.Host, ct);
|
|
|
|
// Re-validate at connect time; an empty set fails closed.
|
|
if (!policy.SkipsAddressChecks(endpoint.Host))
|
|
{
|
|
var decision = policy.CheckAddresses(endpoint.Host, addresses);
|
|
if (!decision.Allowed)
|
|
throw new HttpRequestException($"egress blocked: {decision.Reason}");
|
|
}
|
|
|
|
Exception? last = null;
|
|
foreach (var address in addresses)
|
|
{
|
|
var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
|
|
{
|
|
NoDelay = true,
|
|
};
|
|
try
|
|
{
|
|
await socket.ConnectAsync(new IPEndPoint(address, endpoint.Port), ct);
|
|
return new NetworkStream(socket, ownsSocket: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
socket.Dispose();
|
|
last = ex;
|
|
}
|
|
}
|
|
|
|
throw new HttpRequestException(
|
|
$"egress blocked: could not connect to a permitted address for '{endpoint.Host}'", last);
|
|
},
|
|
};
|
|
|
|
if (allowInsecureTls)
|
|
{
|
|
handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions
|
|
{
|
|
RemoteCertificateValidationCallback = (_, _, _, _) => true,
|
|
};
|
|
}
|
|
|
|
return handler;
|
|
}
|
|
}
|