< Summary - Syki

Line coverage
0%
Covered lines: 0
Uncovered lines: 52
Coverable lines: 52
Total lines: 190
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 64
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: SsoDomainRegex()100%210%
File 2: ValidateSsoAuthority(...)0%4260%
File 2: ValidateSsoHost(...)0%110100%
File 2: NormalizeSsoDomain(...)0%4260%
File 2: ValidateSsoIpAddress(...)0%1190340%
File 2: IsPrivateSsoIpV4(...)0%7280%

File(s)

/_/Back/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/_/Back/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

/home/runner/work/syki/syki/Back/Extensions/SsoExtensions.cs

#LineLine coverage
 1using System.Net;
 2using System.Net.Sockets;
 3using System.Text.RegularExpressions;
 4
 5namespace Syki.Back.Extensions;
 6
 7public static partial class SsoExtensions
 8{
 9    extension(string value)
 10    {
 11        public SykiError? ValidateSsoAuthority()
 12        {
 13            // Must be valid URI
 014            if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
 015                return InvalidSsoAuthority.I;
 16
 17            // Must be HTTPS
 018            if (uri.Scheme != Uri.UriSchemeHttps)
 019                return SsoAuthorityMustBeHttps.I;
 20
 21            // Block URLs with userinfo (SSRF bypass: https://evil.com@169.254.169.254/)
 022            if (uri.UserInfo.HasValue())
 023                return SsoAuthorityHasUserInfo.I;
 24
 25            // Parse and validate the host
 026            return uri.Host.ValidateSsoHost();
 27        }
 28
 29        public SykiError? ValidateSsoHost()
 30        {
 31            // Try to parse as IP address
 032            if (IPAddress.TryParse(value, out var ip))
 033                return ip.ValidateSsoIpAddress();
 34
 35            // It's a hostname - check for dangerous hostnames
 036            var lowerHost = value.ToLowerInvariant();
 37
 38            // Block localhost variants (allow in dev/testing)
 039            if (lowerHost is "localhost" or "localhost.localdomain")
 040                return EnvironmentExtensions.IsDevelopmentOrTesting() ? null : SsoAuthorityLocalhostNotAllowed.I;
 41
 042            return null;
 43        }
 44
 45        public string? NormalizeSsoDomain()
 46        {
 047            if (string.IsNullOrWhiteSpace(value))
 048                return null;
 49
 050            var normalized = value.Trim().ToLowerInvariant();
 51
 52            // Remove @ if present at start
 053            if (normalized.StartsWith('@'))
 054                normalized = normalized[1..];
 55
 56            // Basic domain validation
 057            if (!SsoDomainRegex().IsMatch(normalized))
 058                return null;
 59
 060            return normalized;
 61        }
 62    }
 63
 64    extension(IPAddress ip)
 65    {
 66        public SykiError? ValidateSsoIpAddress()
 67        {
 068            var resolved = ip;
 069            var isDevOrTest = EnvironmentExtensions.IsDevelopmentOrTesting();
 70
 71            // Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1)
 072            if (resolved.IsIPv4MappedToIPv6)
 073                resolved = resolved.MapToIPv4();
 74
 75            // IPv6 checks
 076            if (resolved.AddressFamily == AddressFamily.InterNetworkV6)
 77            {
 78                // Block IPv6 loopback (::1) — allow in dev/testing
 079                if (IPAddress.IPv6Loopback.Equals(resolved))
 080                    return isDevOrTest ? null : SsoAuthorityLoopbackNotAllowed.I;
 81
 82                // Block IPv6 link-local (fe80::/10) — always blocked (cloud metadata risk)
 083                if (resolved.IsIPv6LinkLocal)
 084                    return SsoAuthorityLinkLocalNotAllowed.I;
 85
 86                // Block IPv6 unique local addresses (fc00::/7 = fc00:: and fd00::) — allow in dev/testing
 087                var bytes = resolved.GetAddressBytes();
 088                if ((bytes[0] & 0xFE) == 0xFC) // fc00::/7
 089                    return isDevOrTest ? null : SsoAuthorityPrivateIpNotAllowed.I;
 90
 091                return null;
 92            }
 93
 94            // IPv4 checks
 095            var ipBytes = resolved.GetAddressBytes();
 96
 97            // Block 0.0.0.0 — always blocked
 098            if (ipBytes[0] == 0 && ipBytes[1] == 0 && ipBytes[2] == 0 && ipBytes[3] == 0)
 099                return SsoAuthorityLoopbackNotAllowed.I;
 100
 101            // Block entire loopback range 127.0.0.0/8 — allow in dev/testing
 0102            if (ipBytes[0] == 127)
 0103                return isDevOrTest ? null : SsoAuthorityLoopbackNotAllowed.I;
 104
 105            // Block entire link-local range 169.254.0.0/16 — always blocked (cloud metadata risk)
 0106            if (ipBytes[0] == 169 && ipBytes[1] == 254)
 0107                return SsoAuthorityLinkLocalNotAllowed.I;
 108
 109            // Block private IP ranges — allow in dev/testing
 0110            if (IsPrivateSsoIpV4(ipBytes))
 0111                return isDevOrTest ? null : SsoAuthorityPrivateIpNotAllowed.I;
 112
 0113            return null;
 114        }
 115    }
 116
 117    private static bool IsPrivateSsoIpV4(byte[] bytes)
 118    {
 0119        return bytes[0] switch
 0120        {
 0121            10 => true,                                      // 10.0.0.0/8
 0122            172 => bytes[1] >= 16 && bytes[1] <= 31,         // 172.16.0.0/12
 0123            192 => bytes[1] == 168,                          // 192.168.0.0/16
 0124            _ => false
 0125        };
 126    }
 127
 128    [GeneratedRegex(@"^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$")]
 129    private static partial Regex SsoDomainRegex();
 130}