diff --git a/src/libraries/System.Private.Uri/src/System/UriBuilder.cs b/src/libraries/System.Private.Uri/src/System/UriBuilder.cs index e890e9aa45e998..35575e022e1d8c 100644 --- a/src/libraries/System.Private.Uri/src/System/UriBuilder.cs +++ b/src/libraries/System.Private.Uri/src/System/UriBuilder.cs @@ -158,16 +158,41 @@ public string Password } } + /// + /// Problematic characters that could result in the Host component escaping into other components like the Path. + private static readonly SearchValues s_hostReservedChars = + SearchValues.Create(@":/\?#@"); + [AllowNull] public string Host { get => _host; set { - if (!string.IsNullOrEmpty(value) && value.Contains(':') && value[0] != '[') + if (!string.IsNullOrEmpty(value) && value.AsSpan().ContainsAny(s_hostReservedChars)) { - //probable ipv6 address - Note: this is only supported for cases where the authority is inet-based. - value = "[" + value + "]"; + if (value.Contains(':')) + { + if (!value.StartsWith('[')) + { + // probable ipv6 address - Note: this is only supported for cases where the authority is inet-based. + value = "[" + value + "]"; + } + + if (value.AsSpan(0, value.Length - 1).Contains(']')) + { + // Reject inputs like "[::]/path" or "::]/path". + throw new ArgumentException(SR.net_uri_BadHostName, nameof(value)); + } + } + else + { + // Reject inputs like "contoso.com/path" or "user@contoso.com". + // We don't take this branch if the input is an IPv6 address because those can contain '/' characters. + // If the input is an IPv6 address with invalid characters, Uri parsing will catch it later. + // Nonsensical inputs will only be allowed by a custom parser with GenericUriParserOptions.GenericAuthority set. + throw new ArgumentException(SR.net_uri_BadHostName, nameof(value)); + } } _host = value ?? string.Empty; @@ -365,7 +390,7 @@ public override string ToString() } } - var path = Path; + string path = Path; if (path.Length != 0) { if (!path.StartsWith('/') && host.Length != 0) diff --git a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriBuilderTests.cs b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriBuilderTests.cs index 5e35c4c1e146c7..4ff6d51f2bf896 100644 --- a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriBuilderTests.cs +++ b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriBuilderTests.cs @@ -88,7 +88,6 @@ public void Ctor_Uri_Null() [InlineData("http", "host", "http", "host")] [InlineData("HTTP", "host", "http", "host")] [InlineData("http", "[::1]", "http", "[::1]")] - [InlineData("https", "::1]", "https", "[::1]]")] [InlineData("http", "::1", "http", "[::1]")] [InlineData("http1:http2", "host", "http1", "host")] [InlineData("http", "", "http", "")] @@ -107,7 +106,6 @@ public void Ctor_String_String(string? schemeName, string? hostName, string expe [InlineData("http", "host", 0, "http", "host")] [InlineData("HTTP", "host", 20, "http", "host")] [InlineData("http", "[::1]", 40, "http", "[::1]")] - [InlineData("https", "::1]", 60, "https", "[::1]]")] [InlineData("http", "::1", 80, "http", "[::1]")] [InlineData("http1:http2", "host", 100, "http1", "host")] [InlineData("http", "", 120, "http", "")] @@ -126,7 +124,6 @@ public void Ctor_String_String_Int(string? scheme, string? host, int port, strin [InlineData("http", "host", 0, "/path", "http", "host", "/path")] [InlineData("HTTP", "host", 20, "/path1/path2", "http", "host", "/path1/path2")] [InlineData("http", "[::1]", 40, "/", "http", "[::1]", "/")] - [InlineData("https", "::1]", 60, "/path1/", "https", "[::1]]", "/path1/")] [InlineData("http", "::1", 80, null, "http", "[::1]", "/")] [InlineData("http1:http2", "host", 100, "path1", "http1", "host", "path1")] [InlineData("http", "", 120, "path1/path2", "http", "", "path1/path2")] @@ -145,7 +142,6 @@ public void Ctor_String_String_Int_String(string? schemeName, string? hostName, [InlineData("http", "host", 0, "/path", "?query#fragment", "http", "host", "/path", "?query", "#fragment")] [InlineData("HTTP", "host", 20, "/path1/path2", "?query&query2=value#fragment", "http", "host", "/path1/path2", "?query&query2=value", "#fragment")] [InlineData("http", "[::1]", 40, "/", "#fragment?query", "http", "[::1]", "/", "", "#fragment?query")] - [InlineData("https", "::1]", 60, "/path1/", "?query", "https", "[::1]]", "/path1/", "?query", "")] [InlineData("http", "::1", 80, null, "#fragment", "http", "[::1]", "/", "", "#fragment")] [InlineData("http", "", 120, "path1/path2", "?#", "http", "", "path1/path2", "", "")] [InlineData("", "host", 140, "path1/path2/path3/", "?", "", "host", "path1/path2/path3/", "", "")] @@ -368,6 +364,7 @@ public static IEnumerable ToString_TestData() yield return new object[] { new UriBuilder() { Host = "host", Query = "query" }, "http://host/?query" }; yield return new object[] { new UriBuilder() { Host = "host", Fragment = "fragment" }, "http://host/#fragment" }; yield return new object[] { new UriBuilder() { Host = "host", Query = "query", Fragment = "fragment" }, "http://host/?query#fragment" }; + yield return new object[] { new UriBuilder() { Host = "::%foo" }, "http://[::%foo]/" }; } [Theory] @@ -401,6 +398,47 @@ public void ToString_EncodingUserInfo(string username, string password, string e Assert.Equal(password, uriBuilder.Password); } + public static IEnumerable InvalidHostStrings_TestData() + { + // Bool indicates whether the exception is expected to be thrown by the Host setter (true) or when creating the Uri (false) + + // Presence of ':' is treated as a likely IPv6 address and the input is transformer into [host:80], which is invalid + yield return ["host:", false]; + yield return ["host:80", false]; + + // Trailing space after IPv6 address + yield return ["[::] ", true]; + + // Invalid characters escape into other Uri components + yield return ["host/path", true]; + yield return ["host?query", true]; + yield return ["host#fragment", true]; + yield return ["user@host", true]; + + yield return ["[::]/path", true]; + yield return ["::]/path", true]; + yield return ["::/path", false]; + yield return ["::?query", false]; + yield return ["::#fragment", false]; + } + + [Theory] + [MemberData(nameof(InvalidHostStrings_TestData))] + public void Host_Set_InvalidHostStrings_ThrowsArgumentException(string invalidHost, bool caughtByHostSetter) + { + var builder = new UriBuilder(); + + if (caughtByHostSetter) + { + AssertExtensions.Throws("value", () => builder.Host = invalidHost); + } + else + { + builder.Host = invalidHost; + Assert.Throws(() => builder.Uri); + } + } + private static void VerifyUriBuilder(UriBuilder uriBuilder, string scheme, string userName, string password, string host, int port, string path, string query, string fragment) { Assert.Equal(scheme, uriBuilder.Scheme);