w4c-workflows-api/Services/Security/EgressGuard.cs
Vitali sharp8n 42ffcb9adc workflows
2026-09-13 19:28:47 +03:00

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);
}
}