| | | 1 | | using System.Net; |
| | | 2 | | using System.Net.Sockets; |
| | | 3 | | using Elsa.ExternalAuthentication.Options; |
| | | 4 | | using Microsoft.Extensions.Options; |
| | | 5 | | |
| | | 6 | | namespace Elsa.ExternalAuthentication.Validation; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Resolves and validates provider-controlled destinations before any outbound request or connection is made. |
| | | 10 | | /// </summary> |
| | | 11 | | public sealed class OutboundDestinationValidator(IOptions<ExternalAuthenticationOptions> options, IOutboundDnsResolver d |
| | | 12 | | { |
| | | 13 | | public async ValueTask<IReadOnlyCollection<IPAddress>> ValidateAsync(Uri destination, CancellationToken cancellation |
| | | 14 | | { |
| | | 15 | | ValidateUri(destination); |
| | | 16 | | var addresses = destination.HostNameType is UriHostNameType.IPv4 or UriHostNameType.IPv6 |
| | | 17 | | ? [IPAddress.Parse(destination.Host)] |
| | | 18 | | : await dnsResolver.ResolveAsync(destination.DnsSafeHost, cancellationToken); |
| | | 19 | | |
| | | 20 | | if (addresses.Count == 0 || !options.Value.ProviderEgress.AllowPrivateNetworkDestinations && addresses.Any(IsUns |
| | | 21 | | throw new OutboundDestinationException(); |
| | | 22 | | |
| | | 23 | | return addresses; |
| | | 24 | | } |
| | | 25 | | |
| | | 26 | | public async ValueTask<IPAddress> ResolveApprovedAddressAsync(DnsEndPoint endpoint, CancellationToken cancellationTo |
| | | 27 | | { |
| | | 28 | | var destination = new UriBuilder(Uri.UriSchemeHttps, endpoint.Host, endpoint.Port).Uri; |
| | | 29 | | var addresses = await ValidateAsync(destination, cancellationToken); |
| | | 30 | | return addresses.First(); |
| | | 31 | | } |
| | | 32 | | |
| | | 33 | | public void ValidateApprovedProxy(Uri proxyUri) |
| | | 34 | | { |
| | | 35 | | if (!proxyUri.IsAbsoluteUri || |
| | | 36 | | !string.Equals(proxyUri.Scheme, "http", StringComparison.OrdinalIgnoreCase) && !string.Equals(proxyUri.Schem |
| | | 37 | | !string.IsNullOrEmpty(proxyUri.UserInfo) || |
| | | 38 | | !string.IsNullOrEmpty(proxyUri.Fragment) || |
| | | 39 | | proxyUri.HostNameType == UriHostNameType.Unknown) |
| | | 40 | | throw new OutboundDestinationException(); |
| | | 41 | | } |
| | | 42 | | |
| | | 43 | | private void ValidateUri(Uri destination) |
| | | 44 | | { |
| | | 45 | | var policy = options.Value.ProviderEgress; |
| | | 46 | | if (!destination.IsAbsoluteUri || |
| | | 47 | | policy.RequireHttps && !string.Equals(destination.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnore |
| | | 48 | | !string.IsNullOrEmpty(destination.UserInfo) || |
| | | 49 | | !string.IsNullOrEmpty(destination.Fragment) || |
| | | 50 | | destination.HostNameType == UriHostNameType.Unknown) |
| | | 51 | | throw new OutboundDestinationException(); |
| | | 52 | | |
| | | 53 | | var allowedHosts = policy.AllowedHosts ?? []; |
| | | 54 | | if (allowedHosts.Count > 0 && !allowedHosts.Any(host => string.Equals(host.TrimEnd('.'), destination.DnsSafeHost |
| | | 55 | | throw new OutboundDestinationException(); |
| | | 56 | | } |
| | | 57 | | |
| | | 58 | | private static bool IsUnsafeAddress(IPAddress address) |
| | | 59 | | { |
| | | 60 | | if (address.IsIPv4MappedToIPv6) |
| | | 61 | | address = address.MapToIPv4(); |
| | | 62 | | |
| | | 63 | | if (address.AddressFamily == AddressFamily.InterNetwork) |
| | | 64 | | { |
| | | 65 | | var bytes = address.GetAddressBytes(); |
| | | 66 | | return bytes[0] == 0 || |
| | | 67 | | bytes[0] == 10 || |
| | | 68 | | bytes[0] == 100 && bytes[1] is >= 64 and <= 127 || |
| | | 69 | | bytes[0] == 127 || |
| | | 70 | | bytes[0] == 169 && bytes[1] == 254 || |
| | | 71 | | bytes[0] == 172 && bytes[1] is >= 16 and <= 31 || |
| | | 72 | | bytes[0] == 192 && (bytes[1] == 0 || bytes[1] == 168) || |
| | | 73 | | bytes[0] == 192 && bytes[1] == 0 && bytes[2] == 2 || |
| | | 74 | | bytes[0] == 198 && (bytes[1] is 18 or 19 || bytes[1] == 51 && bytes[2] == 100) || |
| | | 75 | | bytes[0] == 203 && bytes[1] == 0 && bytes[2] == 113 || |
| | | 76 | | bytes[0] >= 224; |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | if (address.AddressFamily != AddressFamily.InterNetworkV6) |
| | | 80 | | return true; |
| | | 81 | | |
| | | 82 | | var ipv6 = address.GetAddressBytes(); |
| | | 83 | | return IPAddress.IsLoopback(address) || |
| | | 84 | | address.Equals(IPAddress.IPv6Any) || |
| | | 85 | | address.IsIPv6LinkLocal || |
| | | 86 | | address.IsIPv6SiteLocal || |
| | | 87 | | address.IsIPv6Multicast || |
| | | 88 | | ipv6[0] is 0xfc or 0xfd || |
| | | 89 | | ipv6[0] == 0x20 && ipv6[1] == 0x01 && ipv6[2] == 0x0d && ipv6[3] == 0xb8; |
| | | 90 | | } |
| | | 91 | | } |
| | | 92 | | |
| | | 93 | | public interface IOutboundDnsResolver |
| | | 94 | | { |
| | | 95 | | ValueTask<IReadOnlyCollection<IPAddress>> ResolveAsync(string host, CancellationToken cancellationToken = default); |
| | | 96 | | } |
| | | 97 | | |
| | | 98 | | public sealed class SystemOutboundDnsResolver : IOutboundDnsResolver |
| | | 99 | | { |
| | | 100 | | public async ValueTask<IReadOnlyCollection<IPAddress>> ResolveAsync(string host, CancellationToken cancellationToken |
| | | 101 | | } |
| | | 102 | | |
| | | 103 | | public sealed class OutboundDestinationException : InvalidOperationException |
| | | 104 | | { |
| | 13 | 105 | | public OutboundDestinationException() : base("The provider destination is not permitted.") |
| | | 106 | | { |
| | 13 | 107 | | } |
| | | 108 | | } |