diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs
index 418eea5360..31a8b4a9fd 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs
@@ -102,14 +102,48 @@ public virtual string Action(UrlActionContext actionContext)
///
public virtual bool IsLocalUrl(string url)
{
- return
- !string.IsNullOrEmpty(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 true;
+ }
+
+ 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;
+ }
- // Allows "/" or "/foo" but not "//" or "/\".
- ((url[0] == '/' && (url.Length == 1 || (url[1] != '/' && url[1] != '\\'))) ||
+ // url doesn't start with "~//" or "~/\"
+ if (url[2] != '/' && url[2] != '\\')
+ {
+ return true;
+ }
- // Allows "~/" or "~/foo".
- (url.Length > 1 && url[0] == '~' && url[1] == '/'));
+ return false;
+ }
+
+ return false;
}
///
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs
index f12ae0e51d..ebbbac82a7 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs
@@ -7,7 +7,6 @@
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
-using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
@@ -87,6 +86,10 @@ public async Task Execute_ReturnsExpectedValues()
}
[Theory]
+ [InlineData("", "//", "/test")]
+ [InlineData("", "/\\", "/test")]
+ [InlineData("", "//foo", "/test")]
+ [InlineData("", "/\\foo", "/test")]
[InlineData("", "Home/About", "/Home/About")]
[InlineData("/myapproot", "http://www.example.com", "/test")]
public async Task Execute_Throws_ForNonLocalUrl(
@@ -111,6 +114,33 @@ public async Task Execute_Throws_ForNonLocalUrl(
exception.Message);
}
+ [Theory]
+ [InlineData("", "~//", "//")]
+ [InlineData("", "~/\\", "/\\")]
+ [InlineData("", "~//foo", "//foo")]
+ [InlineData("", "~/\\foo", "/\\foo")]
+ public async Task Execute_Throws_ForNonLocalUrlTilde(
+ string appRoot,
+ string contentPath,
+ string expectedPath)
+ {
+ // Arrange
+ var httpResponse = new Mock();
+ httpResponse.Setup(o => o.Redirect(expectedPath, false))
+ .Verifiable();
+
+ var httpContext = GetHttpContext(appRoot, contentPath, expectedPath, httpResponse.Object);
+ var actionContext = GetActionContext(httpContext);
+ var result = new LocalRedirectResult(contentPath);
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(() => result.ExecuteResultAsync(actionContext));
+ Assert.Equal(
+ "The supplied URL is not local. A URL with an absolute path is considered local if it does not " +
+ "have a host/authority part. URLs using virtual paths ('~/') are also local.",
+ exception.Message);
+ }
+
private static ActionContext GetActionContext(HttpContext httpContext)
{
var routeData = new RouteData();
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTest.cs
index 80bda3ff04..2dd726042c 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTest.cs
@@ -261,6 +261,40 @@ public void IsLocalUrl_RejectsInvalidUrls(string url)
Assert.False(result);
}
+ [Theory]
+ [InlineData("~//www.example.com")]
+ [InlineData("~//www.example.com?")]
+ [InlineData("~//www.example.com:80")]
+ [InlineData("~//www.example.com/foobar.html")]
+ [InlineData("~///www.example.com")]
+ [InlineData("~//////www.example.com")]
+ public void IsLocalUrl_RejectsTokenUrlsWithMissingSchemeName(string url)
+ {
+ // Arrange
+ var helper = CreateUrlHelper("www.mysite.com");
+
+ // Act
+ var result = helper.IsLocalUrl(url);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Theory]
+ [InlineData("~/\\")]
+ [InlineData("~/\\foo")]
+ public void IsLocalUrl_RejectsInvalidTokenUrls(string url)
+ {
+ // Arrange
+ var helper = CreateUrlHelper("www.mysite.com");
+
+ // Act
+ var result = helper.IsLocalUrl(url);
+
+ // Assert
+ Assert.False(result);
+ }
+
[Fact]
public void RouteUrlWithDictionary()
{