From f2457d167dc51adff2fb698856f1e1c14eb16578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Wed, 26 Apr 2017 10:41:39 -0700 Subject: [PATCH] Port fix for URL helper redirect Fixes #6910 --- .../Routing/UrlHelper.cs | 46 ++++++++++++++++--- .../LocalRedirectResultTest.cs | 32 ++++++++++++- .../Routing/UrlHelperTest.cs | 34 ++++++++++++++ 3 files changed, 105 insertions(+), 7 deletions(-) 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() {