parent
bf7c9e053e
commit
c4400d22d4
|
|
@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UrlHelper : IUrlHelper
|
public class UrlHelper : IUrlHelper
|
||||||
{
|
{
|
||||||
|
internal const string UseRelaxedLocalRedirectValidationSwitch = "Switch.Microsoft.AspNetCore.Mvc.UseRelaxedLocalRedirectValidation";
|
||||||
|
|
||||||
// Perf: Share the StringBuilder object across multiple calls of GenerateURL for this UrlHelper
|
// Perf: Share the StringBuilder object across multiple calls of GenerateURL for this UrlHelper
|
||||||
private StringBuilder _stringBuilder;
|
private StringBuilder _stringBuilder;
|
||||||
|
|
@ -102,14 +103,53 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual bool IsLocalUrl(string url)
|
public virtual bool IsLocalUrl(string url)
|
||||||
{
|
{
|
||||||
return
|
if (string.IsNullOrEmpty(url))
|
||||||
!string.IsNullOrEmpty(url) &&
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Allows "/" or "/foo" but not "//" or "/\".
|
// Allows "/" or "/foo" but not "//" or "/\".
|
||||||
((url[0] == '/' && (url.Length == 1 || (url[1] != '/' && url[1] != '\\'))) ||
|
if (url[0] == '/')
|
||||||
|
{
|
||||||
|
// url is exactly "/"
|
||||||
|
if (url.Length == 1)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Allows "~/" or "~/foo".
|
// url doesn't start with "//" or "/\"
|
||||||
(url.Length > 1 && url[0] == '~' && url[1] == '/'));
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// url doesn't start with "~//" or "~/\"
|
||||||
|
if (url[2] != '/' && url[2] != '\\')
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppContext.TryGetSwitch(UseRelaxedLocalRedirectValidationSwitch, out var relaxedLocalRedirectValidation) && relaxedLocalRedirectValidation)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
[InlineData("", "//", "/test")]
|
||||||
|
[InlineData("", "/\\", "/test")]
|
||||||
|
[InlineData("", "//foo", "/test")]
|
||||||
|
[InlineData("", "/\\foo", "/test")]
|
||||||
[InlineData("", "Home/About", "/Home/About")]
|
[InlineData("", "Home/About", "/Home/About")]
|
||||||
[InlineData("/myapproot", "http://www.example.com", "/test")]
|
[InlineData("/myapproot", "http://www.example.com", "/test")]
|
||||||
public void Execute_Throws_ForNonLocalUrl(
|
public void Execute_Throws_ForNonLocalUrl(
|
||||||
|
|
@ -109,6 +113,44 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
exception.Message);
|
exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", "~//", "//")]
|
||||||
|
[InlineData("", "~/\\", "/\\")]
|
||||||
|
[InlineData("", "~//foo", "//foo")]
|
||||||
|
[InlineData("", "~/\\foo", "/\\foo")]
|
||||||
|
public void Execute_Throws_ForNonLocalUrlTilde(
|
||||||
|
string appRoot,
|
||||||
|
string contentPath,
|
||||||
|
string expectedPath)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpResponse = new Mock<HttpResponse>();
|
||||||
|
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);
|
||||||
|
|
||||||
|
var relaxedLocalRedirectValidation = false;
|
||||||
|
var success = AppContext.TryGetSwitch(UrlHelper.UseRelaxedLocalRedirectValidationSwitch, out relaxedLocalRedirectValidation);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
if (relaxedLocalRedirectValidation)
|
||||||
|
{
|
||||||
|
result.ExecuteResult(actionContext);
|
||||||
|
httpResponse.Verify();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => result.ExecuteResult(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)
|
private static ActionContext GetActionContext(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
var routeData = new RouteData();
|
var routeData = new RouteData();
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,60 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
Assert.False(result);
|
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
|
||||||
|
var relaxedLocalRedirectValidation = false;
|
||||||
|
var success = AppContext.TryGetSwitch(UrlHelper.UseRelaxedLocalRedirectValidationSwitch, out relaxedLocalRedirectValidation);
|
||||||
|
|
||||||
|
if (relaxedLocalRedirectValidation)
|
||||||
|
{
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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
|
||||||
|
var relaxedLocalRedirectValidation = false;
|
||||||
|
var success = AppContext.TryGetSwitch(UrlHelper.UseRelaxedLocalRedirectValidationSwitch, out relaxedLocalRedirectValidation);
|
||||||
|
|
||||||
|
if (relaxedLocalRedirectValidation)
|
||||||
|
{
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RouteUrlWithDictionary()
|
public void RouteUrlWithDictionary()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue