Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -445,17 +445,52 @@ await Events.RedirectToReturnUrl(
}
}

// Mirror of Microsoft.AspNetCore.Internal.SharedUrlHelper.IsLocalUrl
// (src/Shared/ResultsHelpers/SharedUrlHelper.cs). Keep the two in sync.
// Intentional difference: cookie auth rejects "~/" paths because ApplyHeaders
// writes the value verbatim into the Location header and never resolves
// PathBase like IUrlHelper.Content does — so the "~/..." branch from
// IsLocalUrl is omitted here.
Comment thread
rokonec marked this conversation as resolved.
Outdated
private static bool IsHostRelative(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
if (path.Length == 1)

// Allows "/" or "/foo" but not "//" or "/\".
if (path[0] == '/')
{
// path is exactly "/"
if (path.Length == 1)
{
return true;
}

// path doesn't start with "//" or "/\"
if (path[1] != '/' && path[1] != '\\')
{
return !HasControlCharacter(path.AsSpan(1));
}

return false;
}

return false;

static bool HasControlCharacter(ReadOnlySpan<char> readOnlySpan)
{
return path[0] == '/';
// URLs may not contain ASCII control characters.
for (var i = 0; i < readOnlySpan.Length; i++)
{
if (char.IsControl(readOnlySpan[i]))
{
return true;
}
}

return false;
}
return path[0] == '/' && path[1] != '/' && path[1] != '\\';
}

/// <inheritdoc />
Expand Down
79 changes: 79 additions & 0 deletions src/Security/Authentication/test/CookieTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,85 @@ public async Task AjaxLogoutRedirectToReturnUrlTurnsInto200WithLocationHeader()
Assert.StartsWith("/", responded.Single());
}

[Theory]
[InlineData("/\tfoo")]
[InlineData("/\rfoo")]
[InlineData("/\nfoo")]
[InlineData("/\r\nfoo")]
[InlineData("/\0foo")]
[InlineData("/\u0001foo")]
[InlineData("/\u001Ffoo")]
[InlineData("/\u007Ffoo")]
[InlineData("/foo\r\nLocation:%20evil")]
[InlineData("/foo\tbar")]
public async Task LogoutReturnUrlWithControlCharacterIsRejected(string returnUrl)
{
using var host = await CreateHost(o => o.LogoutPath = "/signout");
using var server = host.GetTestServer();
var transaction = await SendAsync(
server,
"http://example.com/signout?ReturnUrl=" + Uri.EscapeDataString(returnUrl));

Assert.False(transaction.Response.Headers.Contains("Location"));
}

[Theory]
[InlineData("/")]
[InlineData("/foo")]
[InlineData("/foo/bar")]
[InlineData("/foo?x=1#y")]
public async Task LogoutReturnUrlWithoutControlCharacterIsHonored(string returnUrl)
{
using var host = await CreateHost(o => o.LogoutPath = "/signout");
using var server = host.GetTestServer();
var transaction = await SendAsync(
server,
"http://example.com/signout?ReturnUrl=" + Uri.EscapeDataString(returnUrl));

var location = Assert.Single(transaction.Response.Headers.GetValues("Location"));
Assert.Equal(returnUrl, location);
}

[Theory]
// Protocol-relative URLs (path[1] == '/').
[InlineData("//www.example.com")]
[InlineData("//www.example.com/foobar.html")]
[InlineData("///www.example.com")]
[InlineData("//////www.example.com")]
// Forward-then-backslash (path[1] == '\\').
[InlineData("/\\")]
[InlineData("/\\foo")]
[InlineData("/\\evil.com")]
// Application-relative (~) is rejected — cookie auth never resolves PathBase like MVC's IUrlHelper.Content does.
[InlineData("~/")]
[InlineData("~/foo")]
[InlineData("~/foo.html")]
// Absolute URLs (path[0] != '/').
[InlineData("http://www.example.com")]
[InlineData("https://www.example.com")]
[InlineData("HtTpS://www.example.com")]
[InlineData("http://localhost/foobar.html")]
[InlineData("javascript:alert(1)")]
// Relative URLs (path[0] != '/').
[InlineData("foo.html")]
[InlineData("../foo.html")]
[InlineData("fold/foo.html")]
// Single-slash scheme confusion.
[InlineData("http:/foo.html")]
[InlineData("hTtP:foo.html")]
// Whitespace-only (length 1, path[0] != '/').
[InlineData(" ")]
public async Task LogoutReturnUrlNonHostRelativeIsRejected(string returnUrl)
{
using var host = await CreateHost(o => o.LogoutPath = "/signout");
using var server = host.GetTestServer();
var transaction = await SendAsync(
server,
"http://example.com/signout?ReturnUrl=" + Uri.EscapeDataString(returnUrl));

Assert.False(transaction.Response.Headers.Contains("Location"));
}

[Fact]
public async Task AjaxChallengeRedirectTurnsInto200WithLocationHeader()
{
Expand Down
Loading