57 lines
1.9 KiB
C#
57 lines
1.9 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
|
|
namespace w4c_workflows.Services.Security;
|
|
|
|
/// <summary>
|
|
/// Authorises an outbound <see cref="Uri"/> before a workflow node sends it.
|
|
/// Names are resolved up front and every returned address is checked, so a host
|
|
/// that maps to both a public and a private address is rejected rather than
|
|
/// accepted. The connect-time half of the defence is
|
|
/// <see cref="EgressPinning"/>: egress clients re-resolve and re-validate the
|
|
/// address in their <c>ConnectCallback</c>, so a name that answers differently on
|
|
/// the second lookup (DNS rebinding) cannot reach a blocked address.
|
|
/// Literal IPs skip DNS entirely.
|
|
/// </summary>
|
|
public sealed class EgressGuard
|
|
{
|
|
private readonly EgressPolicy _policy;
|
|
private readonly IHostAddressResolver _resolver;
|
|
|
|
public EgressGuard(EgressPolicy policy, IHostAddressResolver resolver)
|
|
{
|
|
_policy = policy;
|
|
_resolver = resolver;
|
|
}
|
|
|
|
/// <summary>Ceiling the HTTP node applies to its own redirect setting.</summary>
|
|
public int MaxRedirects => _policy.MaxRedirects;
|
|
|
|
public async Task<PolicyDecision> AuthorizeAsync(Uri uri, CancellationToken ct)
|
|
{
|
|
var hostDecision = _policy.CheckHost(uri);
|
|
if (hostDecision != null)
|
|
return hostDecision;
|
|
|
|
var host = uri.DnsSafeHost;
|
|
if (IPAddress.TryParse(host, out var literal))
|
|
return _policy.CheckAddresses(host, new[] { literal });
|
|
|
|
IReadOnlyList<IPAddress> addresses;
|
|
try
|
|
{
|
|
addresses = await _resolver.ResolveAsync(host, ct);
|
|
}
|
|
catch (SocketException ex)
|
|
{
|
|
return PolicyDecision.Deny("dns_failure", $"could not resolve host '{host}': {ex.Message}");
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return PolicyDecision.Deny("dns_failure", $"could not resolve host '{host}': {ex.Message}");
|
|
}
|
|
|
|
return _policy.CheckAddresses(host, addresses);
|
|
}
|
|
}
|