diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Http/UrlHelpers.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Http/UrlHelpers.cs new file mode 100644 index 0000000000..30e5e81be2 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Http/UrlHelpers.cs @@ -0,0 +1,28 @@ +using System; + +namespace Volo.Abp.Http; + +public static class UrlHelpers +{ + private const string WildcardSubdomain = "*."; + + public static bool IsSubdomainOf(string subdomain, string domain) + { + if (Uri.TryCreate(subdomain, UriKind.Absolute, out var subdomainUri) && + Uri.TryCreate(domain.Replace(WildcardSubdomain, string.Empty), UriKind.Absolute, out var domainUri)) + { + return domainUri == subdomainUri || IsSubdomainOf(subdomainUri, domainUri); + } + + return false; + } + + public static bool IsSubdomainOf(Uri subdomain, Uri domain) + { + return subdomain.IsAbsoluteUri + && domain.IsAbsoluteUri + && subdomain.Scheme == domain.Scheme + && subdomain.Port == domain.Port + && subdomain.Host.EndsWith($".{domain.Host}", StringComparison.Ordinal); + } +} diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Urls/AppUrlProvider.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Urls/AppUrlProvider.cs index e7b113c8b5..2991e84331 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Urls/AppUrlProvider.cs +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Urls/AppUrlProvider.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; +using Volo.Abp.Http; using Volo.Abp.MultiTenancy; namespace Volo.Abp.UI.Navigation.Urls; @@ -43,7 +44,8 @@ public class AppUrlProvider : IAppUrlProvider, ITransientDependency { redirectAllowedUrls.Add((await NormalizeUrlAsync(redirectAllowedUrl))!); } - var allow = redirectAllowedUrls.Any(x => url.StartsWith(x, StringComparison.CurrentCultureIgnoreCase)); + var allow = redirectAllowedUrls.Any(x => url.StartsWith(x, StringComparison.CurrentCultureIgnoreCase) || + UrlHelpers.IsSubdomainOf(url, x)); if (!allow) { Logger.LogError($"Invalid RedirectUrl: {url}, Use {nameof(AppUrlProvider)} to configure it!"); diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Http/UrlHelpers_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Http/UrlHelpers_Tests.cs new file mode 100644 index 0000000000..51251f84d4 --- /dev/null +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Http/UrlHelpers_Tests.cs @@ -0,0 +1,43 @@ +using Shouldly; +using Xunit; + +namespace Volo.Abp.Http; + +public class UrlHelpers_Tests +{ + [Theory] + [InlineData(null)] + [InlineData("null")] + [InlineData("http://")] + [InlineData("http://*")] + [InlineData("http://.domain")] + [InlineData("http://.domain/hello")] + public void IsSubdomainOf_ReturnsFalseIfDomainIsMalformedUri(string domain) + { + var actual = UrlHelpers.IsSubdomainOf("http://*.domain", domain); + actual.ShouldBeFalse(); + } + + [Theory] + [InlineData("http://sub.domain", "http://domain")] + [InlineData("http://sub.domain", "http://*.domain")] + [InlineData("http://sub.sub.domain", "http://*.domain")] + [InlineData("http://sub.sub.domain", "http://*.sub.domain")] + [InlineData("http://sub.domain:4567", "http://*.domain:4567")] + public void IsSubdomainOf_ReturnsTrue_WhenASubdomain(string subdomain, string domain) + { + var actual = UrlHelpers.IsSubdomainOf(subdomain, domain); + actual.ShouldBeTrue(); + } + + [Theory] + [InlineData("http://sub.domain:1234", "http://*.domain:5678")] + [InlineData("http://sub.domain", "http://domain.*")] + [InlineData("http://sub.domain.hacker", "http://*.domain")] + [InlineData("https://sub.domain", "http://*.domain")] + public void IsSubdomainOf_ReturnsFalse_WhenNotASubdomain(string subdomain, string domain) + { + var actual = UrlHelpers.IsSubdomainOf(subdomain, domain); + actual.ShouldBeFalse(); + } +} diff --git a/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/AppUrlProvider_Tests.cs b/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/AppUrlProvider_Tests.cs index 5b448aa683..eb3698d23b 100644 --- a/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/AppUrlProvider_Tests.cs +++ b/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/AppUrlProvider_Tests.cs @@ -40,7 +40,8 @@ public class AppUrlProvider_Tests : AbpIntegratedTest "https://wwww.volosoft.com", "https://wwww.aspnetzero.com", "https://{{tenantName}}.abp.io", - "https://{{tenantId}}.abp.io" + "https://{{tenantId}}.abp.io", + "https://*.demo.myabp.io" }); options.Applications["BLAZOR"].RootUrl = "https://{{tenantId}}.abp.io"; @@ -101,12 +102,16 @@ public class AppUrlProvider_Tests : AbpIntegratedTest [Fact] public async Task IsRedirectAllowedUrlAsync() { - (await _appUrlProvider.IsRedirectAllowedUrlAsync("https://community.abp.io")).ShouldBeFalse(); (await _appUrlProvider.IsRedirectAllowedUrlAsync("https://wwww.volosoft.com")).ShouldBeTrue(); + (await _appUrlProvider.IsRedirectAllowedUrlAsync("https://wwww.demo.myabp.io")).ShouldBeTrue(); + (await _appUrlProvider.IsRedirectAllowedUrlAsync("https://demo.myabp.io")).ShouldBeTrue(); + (await _appUrlProvider.IsRedirectAllowedUrlAsync("https://api.demo.myabp.io")).ShouldBeTrue(); + (await _appUrlProvider.IsRedirectAllowedUrlAsync("https://test.api.demo.myabp.io")).ShouldBeTrue(); + (await _appUrlProvider.IsRedirectAllowedUrlAsync("https://volosoft.com/demo.myabp.io")).ShouldBeFalse(); + (await _appUrlProvider.IsRedirectAllowedUrlAsync("https://wwww.myabp.io")).ShouldBeFalse(); using (_currentTenant.Change(null)) { - (await _appUrlProvider.IsRedirectAllowedUrlAsync("https://www.abp.io")).ShouldBeFalse(); (await _appUrlProvider.IsRedirectAllowedUrlAsync("https://abp.io")).ShouldBeTrue(); }