< Summary

Information
Class: Elsa.ExternalAuthentication.Validation.ClientReturnPathValidator
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Validation/ClientReturnPathValidator.cs
Line coverage
87%
Covered lines: 35
Uncovered lines: 5
Coverable lines: 40
Total lines: 87
Line coverage: 87.5%
Branch coverage
88%
Covered branches: 32
Total branches: 36
Branch coverage: 88.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
TryValidate(...)91.66%131278.94%
GetSafeReturnPath(...)50%22100%
TryValidateForClient(...)100%44100%
IsClientLocalPath(...)100%66100%
IsAllowedPrefix(...)80%101083.33%
StripQueryAndFragment(...)100%22100%

File(s)

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

#LineLine coverage
 1namespace Elsa.ExternalAuthentication.Validation;
 2
 3/// <summary>
 4/// Validates a browser return path supplied to the authentication broker.
 5/// Only client-local absolute paths may be used to prevent open redirects.
 6/// </summary>
 7public static class ClientReturnPathValidator
 8{
 9    public const string DefaultReturnPath = "/";
 10    public const int MaximumLength = 2048;
 11
 12    public static bool TryValidate(string? returnPath, out string validatedReturnPath)
 13    {
 4014        validatedReturnPath = DefaultReturnPath;
 15
 4016        if (string.IsNullOrWhiteSpace(returnPath) || returnPath.Length > MaximumLength)
 117            return false;
 18
 3919        var candidate = returnPath;
 8220        for (var attempt = 0; attempt < 4; attempt++)
 21        {
 4122            if (!IsClientLocalPath(candidate))
 1023                return false;
 24
 25            try
 26            {
 3127                var decoded = Uri.UnescapeDataString(candidate);
 3128                if (decoded == candidate)
 2929                    break;
 30
 231                candidate = decoded;
 232            }
 033            catch (UriFormatException)
 34            {
 035                return false;
 36            }
 37        }
 38
 2939        if (!IsClientLocalPath(candidate))
 040            return false;
 41
 2942        validatedReturnPath = returnPath;
 2943        return true;
 044    }
 45
 146    public static string GetSafeReturnPath(string? returnPath) => TryValidate(returnPath, out var validatedReturnPath) ?
 47
 48    public static bool TryValidateForClient(string? returnPath, IReadOnlySet<string> allowedPrefixes, out string validat
 49    {
 3150        ArgumentNullException.ThrowIfNull(allowedPrefixes);
 51
 3152        if (!TryValidate(returnPath, out validatedReturnPath))
 453            return false;
 54
 2755        var path = StripQueryAndFragment(validatedReturnPath);
 5456        if (allowedPrefixes.Any(prefix => IsAllowedPrefix(path, prefix)))
 2357            return true;
 58
 459        validatedReturnPath = DefaultReturnPath;
 460        return false;
 61    }
 62
 63    private static bool IsClientLocalPath(string value)
 64    {
 9765        return value.StartsWith("/", StringComparison.Ordinal)
 9766            && !value.StartsWith("//", StringComparison.Ordinal)
 9767            && value.IndexOf('\\') < 0
 9768            && !value.Any(char.IsControl);
 69    }
 70
 71    private static bool IsAllowedPrefix(string path, string prefix)
 72    {
 2773        if (!IsClientLocalPath(prefix) || prefix.IndexOfAny(['?', '#']) >= 0)
 074            return false;
 75
 2776        var normalizedPrefix = prefix.Length > 1 ? prefix.TrimEnd('/') : prefix;
 2777        return normalizedPrefix == "/"
 2778            || string.Equals(path, normalizedPrefix, StringComparison.Ordinal)
 2779            || path.StartsWith($"{normalizedPrefix}/", StringComparison.Ordinal);
 80    }
 81
 82    private static string StripQueryAndFragment(string value)
 83    {
 2784        var separatorIndex = value.IndexOfAny(['?', '#']);
 2785        return separatorIndex < 0 ? value : value[..separatorIndex];
 86    }
 87}