< Summary

Information
Class: Elsa.ExternalAuthentication.Validation.OutboundDestinationValidator
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Validation/OutboundDestinationValidator.cs
Line coverage
94%
Covered lines: 53
Uncovered lines: 3
Coverable lines: 56
Total lines: 108
Line coverage: 94.6%
Branch coverage
74%
Covered branches: 86
Total branches: 116
Branch coverage: 74.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ValidateAsync()80%1010100%
ResolveApprovedAddressAsync()100%11100%
ValidateApprovedProxy(...)50%121285.71%
ValidateUri(...)94.44%1818100%
IsUnsafeAddress(...)72.36%797692%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Validation/OutboundDestinationValidator.cs

#LineLine coverage
 1using System.Net;
 2using System.Net.Sockets;
 3using Elsa.ExternalAuthentication.Options;
 4using Microsoft.Extensions.Options;
 5
 6namespace Elsa.ExternalAuthentication.Validation;
 7
 8/// <summary>
 9/// Resolves and validates provider-controlled destinations before any outbound request or connection is made.
 10/// </summary>
 4011public sealed class OutboundDestinationValidator(IOptions<ExternalAuthenticationOptions> options, IOutboundDnsResolver d
 12{
 13    public async ValueTask<IReadOnlyCollection<IPAddress>> ValidateAsync(Uri destination, CancellationToken cancellation
 14    {
 3315        ValidateUri(destination);
 3016        var addresses = destination.HostNameType is UriHostNameType.IPv4 or UriHostNameType.IPv6
 3017            ? [IPAddress.Parse(destination.Host)]
 3018            : await dnsResolver.ResolveAsync(destination.DnsSafeHost, cancellationToken);
 19
 3020        if (addresses.Count == 0 || !options.Value.ProviderEgress.AllowPrivateNetworkDestinations && addresses.Any(IsUns
 1021            throw new OutboundDestinationException();
 22
 2023        return addresses;
 2024    }
 25
 26    public async ValueTask<IPAddress> ResolveApprovedAddressAsync(DnsEndPoint endpoint, CancellationToken cancellationTo
 27    {
 228        var destination = new UriBuilder(Uri.UriSchemeHttps, endpoint.Host, endpoint.Port).Uri;
 229        var addresses = await ValidateAsync(destination, cancellationToken);
 130        return addresses.First();
 131    }
 32
 33    public void ValidateApprovedProxy(Uri proxyUri)
 34    {
 135        if (!proxyUri.IsAbsoluteUri ||
 136            !string.Equals(proxyUri.Scheme, "http", StringComparison.OrdinalIgnoreCase) && !string.Equals(proxyUri.Schem
 137            !string.IsNullOrEmpty(proxyUri.UserInfo) ||
 138            !string.IsNullOrEmpty(proxyUri.Fragment) ||
 139            proxyUri.HostNameType == UriHostNameType.Unknown)
 040            throw new OutboundDestinationException();
 141    }
 42
 43    private void ValidateUri(Uri destination)
 44    {
 3345        var policy = options.Value.ProviderEgress;
 3346        if (!destination.IsAbsoluteUri ||
 3347            policy.RequireHttps && !string.Equals(destination.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnore
 3348            !string.IsNullOrEmpty(destination.UserInfo) ||
 3349            !string.IsNullOrEmpty(destination.Fragment) ||
 3350            destination.HostNameType == UriHostNameType.Unknown)
 251            throw new OutboundDestinationException();
 52
 3153        var allowedHosts = policy.AllowedHosts ?? [];
 3254        if (allowedHosts.Count > 0 && !allowedHosts.Any(host => string.Equals(host.TrimEnd('.'), destination.DnsSafeHost
 155            throw new OutboundDestinationException();
 3056    }
 57
 58    private static bool IsUnsafeAddress(IPAddress address)
 59    {
 3160        if (address.IsIPv4MappedToIPv6)
 061            address = address.MapToIPv4();
 62
 3163        if (address.AddressFamily == AddressFamily.InterNetwork)
 64        {
 2765            var bytes = address.GetAddressBytes();
 2766            return bytes[0] == 0 ||
 2767                bytes[0] == 10 ||
 2768                bytes[0] == 100 && bytes[1] is >= 64 and <= 127 ||
 2769                bytes[0] == 127 ||
 2770                bytes[0] == 169 && bytes[1] == 254 ||
 2771                bytes[0] == 172 && bytes[1] is >= 16 and <= 31 ||
 2772                bytes[0] == 192 && (bytes[1] == 0 || bytes[1] == 168) ||
 2773                bytes[0] == 192 && bytes[1] == 0 && bytes[2] == 2 ||
 2774                bytes[0] == 198 && (bytes[1] is 18 or 19 || bytes[1] == 51 && bytes[2] == 100) ||
 2775                bytes[0] == 203 && bytes[1] == 0 && bytes[2] == 113 ||
 2776                bytes[0] >= 224;
 77        }
 78
 479        if (address.AddressFamily != AddressFamily.InterNetworkV6)
 080            return true;
 81
 482        var ipv6 = address.GetAddressBytes();
 483        return IPAddress.IsLoopback(address) ||
 484            address.Equals(IPAddress.IPv6Any) ||
 485            address.IsIPv6LinkLocal ||
 486            address.IsIPv6SiteLocal ||
 487            address.IsIPv6Multicast ||
 488            ipv6[0] is 0xfc or 0xfd ||
 489            ipv6[0] == 0x20 && ipv6[1] == 0x01 && ipv6[2] == 0x0d && ipv6[3] == 0xb8;
 90    }
 91}
 92
 93public interface IOutboundDnsResolver
 94{
 95    ValueTask<IReadOnlyCollection<IPAddress>> ResolveAsync(string host, CancellationToken cancellationToken = default);
 96}
 97
 98public sealed class SystemOutboundDnsResolver : IOutboundDnsResolver
 99{
 100    public async ValueTask<IReadOnlyCollection<IPAddress>> ResolveAsync(string host, CancellationToken cancellationToken
 101}
 102
 103public sealed class OutboundDestinationException : InvalidOperationException
 104{
 105    public OutboundDestinationException() : base("The provider destination is not permitted.")
 106    {
 107    }
 108}