From c8b6e0218c287afdf4e07bfd1a5a5389869adef2 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Tue, 11 Jun 2024 16:53:15 -0500 Subject: [PATCH 1/6] Added failing tests that show IsLocalUrl bug --- .../Validation/IsLocalUrl.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 test/IdentityServer.UnitTests/Validation/IsLocalUrl.cs diff --git a/test/IdentityServer.UnitTests/Validation/IsLocalUrl.cs b/test/IdentityServer.UnitTests/Validation/IsLocalUrl.cs new file mode 100644 index 000000000..a311ef4d1 --- /dev/null +++ b/test/IdentityServer.UnitTests/Validation/IsLocalUrl.cs @@ -0,0 +1,96 @@ +using Duende.IdentityServer.Configuration; +using Duende.IdentityServer.Extensions; +using Duende.IdentityServer.Services; +using Duende.IdentityServer.Validation; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using UnitTests.Common; +using UnitTests.Endpoints.Authorize; +using Xunit; + +public class IsLocalUrlTests +{ + private const string queryParameters = "?client_id=mvc.code" + + "&redirect_uri=https%3A%2F%2Flocalhost%3A44302%2Fsignin-oidc" + + "&response_type=code" + + "&scope=openid%20profile%20email%20custom.profile%20resource1.scope1%20resource2.scope1%20offline_access" + + "&code_challenge=LcJN1shWmezC0J5EU7QOi7N_amBuvMDb6PcTY0sB2YY" + + "&code_challenge_method=S256" + + "&response_mode=form_post" + + "&nonce=nonce" + + "&state=state"; + private const string ExternalWithControlCharacters = + "/ /evil.com/connect/authorize/callback" + // Note tab character between slashes + queryParameters; + private const string ExternalWithoutControlCharacters = + "//evil.com/" + + queryParameters; + private const string Local = + "/connect/authorize/callback" + + queryParameters; + + [Fact] + public void IsLocalUrl() + { + Local.IsLocalUrl().Should().BeTrue(); + ExternalWithoutControlCharacters.IsLocalUrl().Should().BeFalse(); + ExternalWithControlCharacters.IsLocalUrl().Should().BeFalse(); + } + + [Fact] + public void GetIdentityServerRelativeUrl() + { + var serverUrls = new MockServerUrls + { + Origin = "https://localhost:5001", + BasePath = "/" + }; + + serverUrls.GetIdentityServerRelativeUrl(Local).Should().NotBeNull(); + serverUrls.GetIdentityServerRelativeUrl(ExternalWithoutControlCharacters).Should().BeNull(); + serverUrls.GetIdentityServerRelativeUrl(ExternalWithControlCharacters).Should().BeNull(); + } + + [Fact] + public async void OidcReturnUrlParser() + { + var oidcParser = GetOidcParser(); + + (await oidcParser.ParseAsync(Local)).Should().NotBeNull(); + oidcParser.IsValidReturnUrl(Local).Should().BeTrue(); + (await oidcParser.ParseAsync(ExternalWithoutControlCharacters)).Should().BeNull(); + oidcParser.IsValidReturnUrl(ExternalWithoutControlCharacters).Should().BeFalse(); + (await oidcParser.ParseAsync(ExternalWithControlCharacters)).Should().BeNull(); + oidcParser.IsValidReturnUrl(ExternalWithControlCharacters).Should().BeFalse(); + } + + [Fact] + public async void ReturnUrlParser() + { + var oidcParser = GetOidcParser(); + var parser = new ReturnUrlParser([oidcParser]); + + (await parser.ParseAsync(Local)).Should().NotBeNull(); + parser.IsValidReturnUrl(Local).Should().BeTrue(); + (await parser.ParseAsync(ExternalWithoutControlCharacters)).Should().BeNull(); + parser.IsValidReturnUrl(ExternalWithoutControlCharacters).Should().BeFalse(); + (await parser.ParseAsync(ExternalWithControlCharacters)).Should().BeNull(); + parser.IsValidReturnUrl(ExternalWithControlCharacters).Should().BeFalse(); + } + + private static OidcReturnUrlParser GetOidcParser() + { + return new OidcReturnUrlParser( + new IdentityServerOptions(), + new StubAuthorizeRequestValidator + { + Result = new AuthorizeRequestValidationResult + ( + new ValidatedAuthorizeRequest() + ) + }, + new MockUserSession(), + new MockServerUrls(), + new LoggerFactory().CreateLogger()); + } +} \ No newline at end of file From ce3ccbd3096a7775854d13d47b5c331d5d9ba350 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Tue, 11 Jun 2024 20:20:45 -0500 Subject: [PATCH 2/6] Add more failing tests of IsLocalUrl --- test/EntityFramework.Tests/IsLocalUrlTests.cs | 39 +++++++++++++++++++ .../{IsLocalUrl.cs => IsLocalUrlTests.cs} | 28 +++++++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 test/EntityFramework.Tests/IsLocalUrlTests.cs rename test/IdentityServer.UnitTests/Validation/{IsLocalUrl.cs => IsLocalUrlTests.cs} (75%) diff --git a/test/EntityFramework.Tests/IsLocalUrlTests.cs b/test/EntityFramework.Tests/IsLocalUrlTests.cs new file mode 100644 index 000000000..8beec5e63 --- /dev/null +++ b/test/EntityFramework.Tests/IsLocalUrlTests.cs @@ -0,0 +1,39 @@ +using Duende.IdentityServer.Configuration; +using Duende.IdentityServer.Extensions; +using Duende.IdentityServer.Services; +using Duende.IdentityServer.Validation; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using UnitTests.Common; +using UnitTests.Endpoints.Authorize; +using Xunit; + +public class IsLocalUrlTests +{ + private const string queryParameters = "?client_id=mvc.code" + + "&redirect_uri=https%3A%2F%2Flocalhost%3A44302%2Fsignin-oidc" + + "&response_type=code" + + "&scope=openid%20profile%20email%20custom.profile%20resource1.scope1%20resource2.scope1%20offline_access" + + "&code_challenge=LcJN1shWmezC0J5EU7QOi7N_amBuvMDb6PcTY0sB2YY" + + "&code_challenge_method=S256" + + "&response_mode=form_post" + + "&nonce=nonce" + + "&state=state"; + private const string ExternalWithControlCharacters = + "/ /evil.com/connect/authorize/callback" + // Note tab character between slashes + queryParameters; + private const string ExternalWithoutControlCharacters = + "//evil.com/" + + queryParameters; + private const string Local = + "/connect/authorize/callback" + + queryParameters; + + [Fact] + public void IsLocalUrl() + { + Local.IsLocalUrl().Should().BeTrue(); + ExternalWithoutControlCharacters.IsLocalUrl().Should().BeFalse(); + ExternalWithControlCharacters.IsLocalUrl().Should().BeFalse(); + } +} \ No newline at end of file diff --git a/test/IdentityServer.UnitTests/Validation/IsLocalUrl.cs b/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs similarity index 75% rename from test/IdentityServer.UnitTests/Validation/IsLocalUrl.cs rename to test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs index a311ef4d1..bfe2c67b2 100644 --- a/test/IdentityServer.UnitTests/Validation/IsLocalUrl.cs +++ b/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs @@ -30,6 +30,18 @@ public class IsLocalUrlTests + queryParameters; [Fact] + public async void GetAuthorizationContextAsync() + { + var interactionService = new DefaultIdentityServerInteractionService(null, null, null, null, null, null, null, + GetReturnUrlParser(), new LoggerFactory().CreateLogger()); + + (await interactionService.GetAuthorizationContextAsync(Local)).Should().NotBeNull(); + (await interactionService.GetAuthorizationContextAsync(ExternalWithoutControlCharacters)).Should().BeNull(); + (await interactionService.GetAuthorizationContextAsync(ExternalWithControlCharacters)).Should().BeNull(); + } + + [Fact] + // TODO - Test the duplicated method in the EF package in later release branches public void IsLocalUrl() { Local.IsLocalUrl().Should().BeTrue(); @@ -54,7 +66,7 @@ public void GetIdentityServerRelativeUrl() [Fact] public async void OidcReturnUrlParser() { - var oidcParser = GetOidcParser(); + var oidcParser = GetOidcReturnUrlParser(); (await oidcParser.ParseAsync(Local)).Should().NotBeNull(); oidcParser.IsValidReturnUrl(Local).Should().BeTrue(); @@ -67,8 +79,7 @@ public async void OidcReturnUrlParser() [Fact] public async void ReturnUrlParser() { - var oidcParser = GetOidcParser(); - var parser = new ReturnUrlParser([oidcParser]); + var parser = GetReturnUrlParser(); (await parser.ParseAsync(Local)).Should().NotBeNull(); parser.IsValidReturnUrl(Local).Should().BeTrue(); @@ -78,7 +89,14 @@ public async void ReturnUrlParser() parser.IsValidReturnUrl(ExternalWithControlCharacters).Should().BeFalse(); } - private static OidcReturnUrlParser GetOidcParser() + private static ReturnUrlParser GetReturnUrlParser() + { + var oidcParser = GetOidcReturnUrlParser(); + var parser = new ReturnUrlParser(new IReturnUrlParser[] { oidcParser }); + return parser; + } + + private static OidcReturnUrlParser GetOidcReturnUrlParser() { return new OidcReturnUrlParser( new IdentityServerOptions(), @@ -93,4 +111,6 @@ private static OidcReturnUrlParser GetOidcParser() new MockServerUrls(), new LoggerFactory().CreateLogger()); } + + } \ No newline at end of file From fa6d0554ab203c332bc3888cc5dbc6041ae1066b Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Tue, 11 Jun 2024 20:23:11 -0500 Subject: [PATCH 3/6] Use tab instead of spaces in test data --- test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs b/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs index bfe2c67b2..cb0992a9e 100644 --- a/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs +++ b/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs @@ -20,7 +20,7 @@ public class IsLocalUrlTests "&nonce=nonce" + "&state=state"; private const string ExternalWithControlCharacters = - "/ /evil.com/connect/authorize/callback" + // Note tab character between slashes + "/ /evil.com/connect/authorize/callback" + // Note tab character between slashes queryParameters; private const string ExternalWithoutControlCharacters = "//evil.com/" From 8be6dee4e978c3582e448adca60b8faeb68e4822 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 12 Jun 2024 14:25:42 -0500 Subject: [PATCH 4/6] Refactored IsLocalUrl tests into memberdata, added many more cases --- test/EntityFramework.Tests/IsLocalUrlTests.cs | 39 ----- .../Validation/IsLocalUrlTests.cs | 139 ++++++++++++------ 2 files changed, 97 insertions(+), 81 deletions(-) delete mode 100644 test/EntityFramework.Tests/IsLocalUrlTests.cs diff --git a/test/EntityFramework.Tests/IsLocalUrlTests.cs b/test/EntityFramework.Tests/IsLocalUrlTests.cs deleted file mode 100644 index 8beec5e63..000000000 --- a/test/EntityFramework.Tests/IsLocalUrlTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Duende.IdentityServer.Configuration; -using Duende.IdentityServer.Extensions; -using Duende.IdentityServer.Services; -using Duende.IdentityServer.Validation; -using FluentAssertions; -using Microsoft.Extensions.Logging; -using UnitTests.Common; -using UnitTests.Endpoints.Authorize; -using Xunit; - -public class IsLocalUrlTests -{ - private const string queryParameters = "?client_id=mvc.code" + - "&redirect_uri=https%3A%2F%2Flocalhost%3A44302%2Fsignin-oidc" + - "&response_type=code" + - "&scope=openid%20profile%20email%20custom.profile%20resource1.scope1%20resource2.scope1%20offline_access" + - "&code_challenge=LcJN1shWmezC0J5EU7QOi7N_amBuvMDb6PcTY0sB2YY" + - "&code_challenge_method=S256" + - "&response_mode=form_post" + - "&nonce=nonce" + - "&state=state"; - private const string ExternalWithControlCharacters = - "/ /evil.com/connect/authorize/callback" + // Note tab character between slashes - queryParameters; - private const string ExternalWithoutControlCharacters = - "//evil.com/" - + queryParameters; - private const string Local = - "/connect/authorize/callback" - + queryParameters; - - [Fact] - public void IsLocalUrl() - { - Local.IsLocalUrl().Should().BeTrue(); - ExternalWithoutControlCharacters.IsLocalUrl().Should().BeFalse(); - ExternalWithControlCharacters.IsLocalUrl().Should().BeFalse(); - } -} \ No newline at end of file diff --git a/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs b/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs index cb0992a9e..1c3e0b352 100644 --- a/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs +++ b/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Duende.IdentityServer.Configuration; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Services; @@ -8,6 +9,8 @@ using UnitTests.Endpoints.Authorize; using Xunit; +namespace UnitTests.Validation; + public class IsLocalUrlTests { private const string queryParameters = "?client_id=mvc.code" + @@ -19,74 +22,126 @@ public class IsLocalUrlTests "&response_mode=form_post" + "&nonce=nonce" + "&state=state"; - private const string ExternalWithControlCharacters = - "/ /evil.com/connect/authorize/callback" + // Note tab character between slashes - queryParameters; - private const string ExternalWithoutControlCharacters = - "//evil.com/" - + queryParameters; - private const string Local = - "/connect/authorize/callback" - + queryParameters; - [Fact] - public async void GetAuthorizationContextAsync() + public static IEnumerable TestCases => + new List + { + new object[] { "/connect/authorize/callback" + queryParameters, true }, + new object[] { "//evil.com/" + queryParameters, false }, + // Tab character + new object[] { "/\t/evil.com/connect/authorize/callback" + queryParameters, false }, + // Spaces + //new object[] { "/ /evil.com/connect/authorize/callback" + queryParameters, false }, + //new object[] { "/ /evil.com/connect/authorize/callback" + queryParameters, false }, + //new object[] { "/ /evil.com/connect/authorize/callback" + queryParameters, false }, + // Tabs and Spaces + new object[] { "/ \t/evil.com/connect/authorize/callback" + queryParameters, false }, + new object[] { "/ \t/evil.com/connect/authorize/callback" + queryParameters, false }, + new object[] { "/ \t/evil.com/connect/authorize/callback" + queryParameters, false }, + new object[] { "/\t /evil.com/connect/authorize/callback" + queryParameters, false }, + new object[] { "/\t /evil.com/connect/authorize/callback" + queryParameters, false }, + new object[] { "/\t /evil.com/connect/authorize/callback" + queryParameters, false }, + // Various new line related things + new object[] { "/\n/evil.com/" + queryParameters, false }, + new object[] { "/\n\n/evil.com/" + queryParameters, false }, + new object[] { "/\r/evil.com/" + queryParameters, false }, + new object[] { "/\r\r/evil.com/" + queryParameters, false }, + new object[] { "/\r\n/evil.com/" + queryParameters, false }, + new object[] { "/\r\n\r\n/evil.com/" + queryParameters, false }, + }; + + [Theory] + [MemberData(nameof(TestCases))] + public async void GetAuthorizationContextAsync(string returnUrl, bool expected) { var interactionService = new DefaultIdentityServerInteractionService(null, null, null, null, null, null, null, GetReturnUrlParser(), new LoggerFactory().CreateLogger()); - - (await interactionService.GetAuthorizationContextAsync(Local)).Should().NotBeNull(); - (await interactionService.GetAuthorizationContextAsync(ExternalWithoutControlCharacters)).Should().BeNull(); - (await interactionService.GetAuthorizationContextAsync(ExternalWithControlCharacters)).Should().BeNull(); + var actual = await interactionService.GetAuthorizationContextAsync(returnUrl); + if (expected) + { + actual.Should().NotBeNull(); + } + else + { + actual.Should().BeNull(); + } } - [Fact] + [Theory] + [MemberData(nameof(TestCases))] // TODO - Test the duplicated method in the EF package in later release branches - public void IsLocalUrl() + public void IsLocalUrl(string returnUrl, bool expected) { - Local.IsLocalUrl().Should().BeTrue(); - ExternalWithoutControlCharacters.IsLocalUrl().Should().BeFalse(); - ExternalWithControlCharacters.IsLocalUrl().Should().BeFalse(); + returnUrl.IsLocalUrl().Should().Be(expected); } - [Fact] - public void GetIdentityServerRelativeUrl() + [Theory] + [MemberData(nameof(TestCases))] + public void GetIdentityServerRelativeUrl(string returnUrl, bool expected) { var serverUrls = new MockServerUrls { Origin = "https://localhost:5001", BasePath = "/" }; - - serverUrls.GetIdentityServerRelativeUrl(Local).Should().NotBeNull(); - serverUrls.GetIdentityServerRelativeUrl(ExternalWithoutControlCharacters).Should().BeNull(); - serverUrls.GetIdentityServerRelativeUrl(ExternalWithControlCharacters).Should().BeNull(); + var actual = serverUrls.GetIdentityServerRelativeUrl(returnUrl); + if (expected) + { + actual.Should().NotBeNull(); + } + else + { + actual.Should().BeNull(); + } } - [Fact] - public async void OidcReturnUrlParser() + [Theory] + [MemberData(nameof(TestCases))] + public async void OidcReturnUrlParser_ParseAsync(string returnUrl, bool expected) { var oidcParser = GetOidcReturnUrlParser(); + var actual = await oidcParser.ParseAsync(returnUrl); + if (expected) + { + actual.Should().NotBeNull(); + } + else + { + actual.Should().BeNull(); + } + } - (await oidcParser.ParseAsync(Local)).Should().NotBeNull(); - oidcParser.IsValidReturnUrl(Local).Should().BeTrue(); - (await oidcParser.ParseAsync(ExternalWithoutControlCharacters)).Should().BeNull(); - oidcParser.IsValidReturnUrl(ExternalWithoutControlCharacters).Should().BeFalse(); - (await oidcParser.ParseAsync(ExternalWithControlCharacters)).Should().BeNull(); - oidcParser.IsValidReturnUrl(ExternalWithControlCharacters).Should().BeFalse(); + [Theory] + [MemberData(nameof(TestCases))] + public void OidcReturnUrlParser_IsValidReturnUrl(string returnUrl, bool expected) + { + var oidcParser = GetOidcReturnUrlParser(); + oidcParser.IsValidReturnUrl(returnUrl).Should().Be(expected); } - [Fact] - public async void ReturnUrlParser() + + [Theory] + [MemberData(nameof(TestCases))] + public void ReturnUrlParser_IsValidReturnUrl(string returnUrl, bool expected) { var parser = GetReturnUrlParser(); + parser.IsValidReturnUrl(returnUrl).Should().Be(expected); + } - (await parser.ParseAsync(Local)).Should().NotBeNull(); - parser.IsValidReturnUrl(Local).Should().BeTrue(); - (await parser.ParseAsync(ExternalWithoutControlCharacters)).Should().BeNull(); - parser.IsValidReturnUrl(ExternalWithoutControlCharacters).Should().BeFalse(); - (await parser.ParseAsync(ExternalWithControlCharacters)).Should().BeNull(); - parser.IsValidReturnUrl(ExternalWithControlCharacters).Should().BeFalse(); + [Theory] + [MemberData(nameof(TestCases))] + public async void ReturnUrlParser_ParseAsync(string returnUrl, bool expected) + { + var parser = GetReturnUrlParser(); + var actual = await parser.ParseAsync(returnUrl); + if (expected) + { + actual.Should().NotBeNull(); + } + else + { + actual.Should().BeNull(); + } } private static ReturnUrlParser GetReturnUrlParser() From 97b298fe62133d5c53d2aaba0de127151a3625fa Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 12 Jun 2024 14:47:51 -0500 Subject: [PATCH 5/6] Sync IsLocalUrl with Microsoft.AspNetCore.Mvc.Routing.UrlHelperBase --- .../Extensions/StringsExtensions.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/IdentityServer/Extensions/StringsExtensions.cs b/src/IdentityServer/Extensions/StringsExtensions.cs index 290191889..8dff245e6 100644 --- a/src/IdentityServer/Extensions/StringsExtensions.cs +++ b/src/IdentityServer/Extensions/StringsExtensions.cs @@ -146,6 +146,8 @@ public static string CleanUrlPath(this string url) [DebuggerStepThrough] public static bool IsLocalUrl(this string url) { + // This implementation is a copy of a https://github.com/dotnet/aspnetcore/blob/3f1acb59718cadf111a0a796681e3d3509bb3381/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs#L315 + // We originally copied that code to avoid a dependency, but we could potentially remove this entirely by switching to the Microsoft.NET.Sdk.Web sdk. if (string.IsNullOrEmpty(url)) { return false; @@ -163,7 +165,7 @@ public static bool IsLocalUrl(this string url) // url doesn't start with "//" or "/\" if (url[1] != '/' && url[1] != '\\') { - return true; + return !HasControlCharacter(url.AsSpan(1)); } return false; @@ -181,13 +183,27 @@ public static bool IsLocalUrl(this string url) // url doesn't start with "~//" or "~/\" if (url[2] != '/' && url[2] != '\\') { - return true; + return !HasControlCharacter(url.AsSpan(2)); } return false; } return false; + + static bool HasControlCharacter(ReadOnlySpan readOnlySpan) + { + // URLs may not contain ASCII control characters. + for (var i = 0; i < readOnlySpan.Length; i++) + { + if (char.IsControl(readOnlySpan[i])) + { + return true; + } + } + + return false; + } } [DebuggerStepThrough] From 5af78623ab61afb0c3f056fc2a61ff75252447d6 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 12 Jun 2024 15:21:20 -0500 Subject: [PATCH 6/6] Clean up and add more test cases for IsLocalUrl --- .../Validation/IsLocalUrlTests.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs b/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs index 1c3e0b352..d9835cfcf 100644 --- a/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs +++ b/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs @@ -30,10 +30,6 @@ public class IsLocalUrlTests new object[] { "//evil.com/" + queryParameters, false }, // Tab character new object[] { "/\t/evil.com/connect/authorize/callback" + queryParameters, false }, - // Spaces - //new object[] { "/ /evil.com/connect/authorize/callback" + queryParameters, false }, - //new object[] { "/ /evil.com/connect/authorize/callback" + queryParameters, false }, - //new object[] { "/ /evil.com/connect/authorize/callback" + queryParameters, false }, // Tabs and Spaces new object[] { "/ \t/evil.com/connect/authorize/callback" + queryParameters, false }, new object[] { "/ \t/evil.com/connect/authorize/callback" + queryParameters, false }, @@ -48,6 +44,19 @@ public class IsLocalUrlTests new object[] { "/\r\r/evil.com/" + queryParameters, false }, new object[] { "/\r\n/evil.com/" + queryParameters, false }, new object[] { "/\r\n\r\n/evil.com/" + queryParameters, false }, + // Tabs and Newlines + new object[] { "/\t\n/evil.com/" + queryParameters, false }, + new object[] { "/\t\n\n/evil.com/" + queryParameters, false }, + new object[] { "/\t\r/evil.com/" + queryParameters, false }, + new object[] { "/\t\r\r/evil.com/" + queryParameters, false }, + new object[] { "/\t\r\n/evil.com/" + queryParameters, false }, + new object[] { "/\t\r\n\r\n/evil.com/" + queryParameters, false }, + new object[] { "/\n/evil.com\t/" + queryParameters, false }, + new object[] { "/\n\n/evil.com\t/" + queryParameters, false }, + new object[] { "/\r/evil.com\t/" + queryParameters, false }, + new object[] { "/\r\r/evil.com\t/" + queryParameters, false }, + new object[] { "/\r\n/evil.com\t/" + queryParameters, false }, + new object[] { "/\r\n\r\n/evil.com\t/" + queryParameters, false }, }; [Theory]