File: Utils\UrlValidationHelper.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
 
namespace Aspire.Dashboard.Utils;
 
/// <summary>
/// Helpers for validating redirect URLs.
/// </summary>
internal static class UrlValidationHelper
{
    /// <summary>
    /// Checks whether a URL is safe to use as a redirect target.
    /// The URL must be a valid URI and a local path (not absolute or protocol-relative).
    /// </summary>
    internal static bool IsSafeRedirectUrl([NotNullWhen(true)] string? url)
    {
        return Uri.TryCreate(url, UriKind.Relative, out _) && IsLocalUrl(url);
    }
 
    // Copied from ASP.NET Core's IsLocalUrl implementation:
    // https://github.com/dotnet/aspnetcore/blob/7cbda0e023075490b4365a0754ca410ce6eff59a/src/Shared/ResultsHelpers/SharedUrlHelper.cs#L33
    internal static bool IsLocalUrl([NotNullWhen(true)] string? url)
    {
        if (string.IsNullOrEmpty(url))
        {
            return false;
        }
 
        // Allows "/" or "/foo" but not "//" or "/\".
        if (url[0] == '/')
        {
            // url is exactly "/"
            if (url.Length == 1)
            {
                return true;
            }
 
            // url doesn't start with "//" or "/\"
            if (url[1] != '/' && url[1] != '\\')
            {
                return !HasControlCharacter(url.AsSpan(1));
            }
 
            return false;
        }
 
        // Allows "~/" or "~/foo" but not "~//" or "~/\".
        if (url[0] == '~' && url.Length > 1 && url[1] == '/')
        {
            // url is exactly "~/"
            if (url.Length == 2)
            {
                return true;
            }
 
            // url doesn't start with "~//" or "~/\"
            if (url[2] != '/' && url[2] != '\\')
            {
                return !HasControlCharacter(url.AsSpan(2));
            }
 
            return false;
        }
 
        return false;
 
        static bool HasControlCharacter(ReadOnlySpan<char> readOnlySpan)
        {
            // URLs may not contain ASCII control characters.
            for (var i = 0; i < readOnlySpan.Length; i++)
            {
                if (char.IsControl(readOnlySpan[i]))
                {
                    return true;
                }
            }
 
            return false;
        }
    }
}