Skip to content

Commit 6a393a5

Browse files
authored
Add some validation to UriBuilder.Host setter (#121083)
1 parent 59b0175 commit 6a393a5

File tree

2 files changed

+74
-8
lines changed

2 files changed

+74
-8
lines changed

src/libraries/System.Private.Uri/src/System/UriBuilder.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,16 +158,40 @@ public string Password
158158
}
159159
}
160160

161+
/// <summary>
162+
/// Problematic characters that could result in the Host component escaping into other components like the Path.
163+
/// </summary>
164+
private static readonly SearchValues<char> s_hostReservedChars = SearchValues.Create(@":/\?#@[]");
165+
private static readonly SearchValues<char> s_hostReservedCharsExceptColon = SearchValues.Create(@"/\?#@[]");
166+
161167
[AllowNull]
162168
public string Host
163169
{
164170
get => _host;
165171
set
166172
{
167-
if (!string.IsNullOrEmpty(value) && value.Contains(':') && value[0] != '[')
173+
if (!string.IsNullOrEmpty(value) && value.AsSpan().ContainsAny(s_hostReservedChars))
168174
{
169-
//probable ipv6 address - Note: this is only supported for cases where the authority is inet-based.
170-
value = "[" + value + "]";
175+
if (value.Contains(':'))
176+
{
177+
if (!value.StartsWith('[') || !value.EndsWith(']'))
178+
{
179+
// probable ipv6 address - Note: this is only supported for cases where the authority is inet-based.
180+
value = $"[{value}]";
181+
}
182+
183+
if (value.AsSpan(1, value.Length - 2).ContainsAny(s_hostReservedCharsExceptColon))
184+
{
185+
// Reject inputs like "[::]/path" or "::]/path".
186+
throw new ArgumentException(SR.net_uri_BadHostName, nameof(value));
187+
}
188+
}
189+
else
190+
{
191+
// Reject inputs like "contoso.com/path" or "[email protected]".
192+
// Nonsensical inputs will only be allowed by a custom parser with GenericUriParserOptions.GenericAuthority set.
193+
throw new ArgumentException(SR.net_uri_BadHostName, nameof(value));
194+
}
171195
}
172196

173197
_host = value ?? string.Empty;
@@ -365,7 +389,7 @@ public override string ToString()
365389
}
366390
}
367391

368-
var path = Path;
392+
string path = Path;
369393
if (path.Length != 0)
370394
{
371395
if (!path.StartsWith('/') && host.Length != 0)

src/libraries/System.Private.Uri/tests/FunctionalTests/UriBuilderTests.cs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ public void Ctor_Uri_Null()
8888
[InlineData("http", "host", "http", "host")]
8989
[InlineData("HTTP", "host", "http", "host")]
9090
[InlineData("http", "[::1]", "http", "[::1]")]
91-
[InlineData("https", "::1]", "https", "[::1]]")]
9291
[InlineData("http", "::1", "http", "[::1]")]
9392
[InlineData("http1:http2", "host", "http1", "host")]
9493
[InlineData("http", "", "http", "")]
@@ -107,7 +106,6 @@ public void Ctor_String_String(string? schemeName, string? hostName, string expe
107106
[InlineData("http", "host", 0, "http", "host")]
108107
[InlineData("HTTP", "host", 20, "http", "host")]
109108
[InlineData("http", "[::1]", 40, "http", "[::1]")]
110-
[InlineData("https", "::1]", 60, "https", "[::1]]")]
111109
[InlineData("http", "::1", 80, "http", "[::1]")]
112110
[InlineData("http1:http2", "host", 100, "http1", "host")]
113111
[InlineData("http", "", 120, "http", "")]
@@ -126,7 +124,6 @@ public void Ctor_String_String_Int(string? scheme, string? host, int port, strin
126124
[InlineData("http", "host", 0, "/path", "http", "host", "/path")]
127125
[InlineData("HTTP", "host", 20, "/path1/path2", "http", "host", "/path1/path2")]
128126
[InlineData("http", "[::1]", 40, "/", "http", "[::1]", "/")]
129-
[InlineData("https", "::1]", 60, "/path1/", "https", "[::1]]", "/path1/")]
130127
[InlineData("http", "::1", 80, null, "http", "[::1]", "/")]
131128
[InlineData("http1:http2", "host", 100, "path1", "http1", "host", "path1")]
132129
[InlineData("http", "", 120, "path1/path2", "http", "", "path1/path2")]
@@ -145,7 +142,6 @@ public void Ctor_String_String_Int_String(string? schemeName, string? hostName,
145142
[InlineData("http", "host", 0, "/path", "?query#fragment", "http", "host", "/path", "?query", "#fragment")]
146143
[InlineData("HTTP", "host", 20, "/path1/path2", "?query&query2=value#fragment", "http", "host", "/path1/path2", "?query&query2=value", "#fragment")]
147144
[InlineData("http", "[::1]", 40, "/", "#fragment?query", "http", "[::1]", "/", "", "#fragment?query")]
148-
[InlineData("https", "::1]", 60, "/path1/", "?query", "https", "[::1]]", "/path1/", "?query", "")]
149145
[InlineData("http", "::1", 80, null, "#fragment", "http", "[::1]", "/", "", "#fragment")]
150146
[InlineData("http", "", 120, "path1/path2", "?#", "http", "", "path1/path2", "", "")]
151147
[InlineData("", "host", 140, "path1/path2/path3/", "?", "", "host", "path1/path2/path3/", "", "")]
@@ -368,6 +364,7 @@ public static IEnumerable<object[]> ToString_TestData()
368364
yield return new object[] { new UriBuilder() { Host = "host", Query = "query" }, "http://host/?query" };
369365
yield return new object[] { new UriBuilder() { Host = "host", Fragment = "fragment" }, "http://host/#fragment" };
370366
yield return new object[] { new UriBuilder() { Host = "host", Query = "query", Fragment = "fragment" }, "http://host/?query#fragment" };
367+
yield return new object[] { new UriBuilder() { Host = "::%foo" }, "http://[::%foo]/" };
371368
}
372369

373370
[Theory]
@@ -401,6 +398,51 @@ public void ToString_EncodingUserInfo(string username, string password, string e
401398
Assert.Equal(password, uriBuilder.Password);
402399
}
403400

401+
public static IEnumerable<object[]> InvalidHostStrings_TestData()
402+
{
403+
// Bool indicates whether the exception is expected to be thrown by the Host setter (true) or when creating the Uri (false)
404+
405+
// Presence of ':' is treated as a likely IPv6 address and the input is transformed into [host:80], which is invalid
406+
yield return ["host:", false];
407+
yield return ["host:80", false];
408+
409+
// Trailing space after IPv6 address
410+
yield return ["[::] ", true];
411+
412+
// Invalid characters escape into other Uri components
413+
yield return ["host/path", true];
414+
yield return ["host?query", true];
415+
yield return ["host#fragment", true];
416+
yield return ["user@host", true];
417+
418+
yield return ["[::]/path", true];
419+
yield return ["::]/path", true];
420+
yield return ["::/path", true];
421+
yield return ["::?query", true];
422+
yield return ["::#fragment", true];
423+
yield return ["[0::%/64]/path", true];
424+
425+
yield return ["[host]", true];
426+
yield return ["[127.0.0.1]", true];
427+
}
428+
429+
[Theory]
430+
[MemberData(nameof(InvalidHostStrings_TestData))]
431+
public void Host_Set_InvalidHostStrings_ThrowsArgumentException(string invalidHost, bool caughtByHostSetter)
432+
{
433+
var builder = new UriBuilder();
434+
435+
if (caughtByHostSetter)
436+
{
437+
AssertExtensions.Throws<ArgumentException>("value", () => builder.Host = invalidHost);
438+
}
439+
else
440+
{
441+
builder.Host = invalidHost;
442+
Assert.Throws<UriFormatException>(() => builder.Uri);
443+
}
444+
}
445+
404446
private static void VerifyUriBuilder(UriBuilder uriBuilder, string scheme, string userName, string password, string host, int port, string path, string query, string fragment)
405447
{
406448
Assert.Equal(scheme, uriBuilder.Scheme);

0 commit comments

Comments
 (0)