diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index 6e3777358beb99..2d91c884406ad5 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -2273,7 +2273,7 @@ private unsafe void CreateUriInfo(Flags cF) if ((cF & Flags.ImplicitFile) != 0) { idx = 0; - while (UriHelper.IsLWS(_string[idx])) + while (idx < _string.Length && UriHelper.IsLWS(_string[idx])) { ++idx; ++info.Offset.Scheme; @@ -2283,8 +2283,18 @@ private unsafe void CreateUriInfo(Flags cF) { // For implicit file AND Unc only idx += 2; + // Ensure idx doesn't exceed string length after increment + if (idx > _string.Length) + { + throw GetException(ParsingError.BadHostName)!; + } //skip any other slashes (compatibility with V1.0 parser) int end = (int)(cF & Flags.IndexMask); + // If end exceeds string length, URI is malformed (e.g., bidi chars were stripped) + if (end > _string.Length) + { + throw GetException(ParsingError.BadHostName)!; + } while (idx < end && (_string[idx] == '/' || _string[idx] == '\\')) { ++idx; @@ -2296,22 +2306,33 @@ private unsafe void CreateUriInfo(Flags cF) // This is NOT an ImplicitFile uri idx = _syntax.SchemeName.Length; - while (_string[idx++] != ':') + while (idx < _string.Length && _string[idx++] != ':') { ++info.Offset.Scheme; } if ((cF & Flags.AuthorityFound) != 0) { - if (_string[idx] == '\\' || _string[idx + 1] == '\\') + if (idx < _string.Length && (idx + 1) < _string.Length && + (_string[idx] == '\\' || _string[idx + 1] == '\\')) notCanonicalScheme = true; idx += 2; + // Ensure idx doesn't exceed string length after increment + if (idx > _string.Length) + { + throw GetException(ParsingError.BadHostName)!; + } if ((cF & (Flags.UncPath | Flags.DosPath)) != 0) { // Skip slashes if it was allowed during ctor time // NB: Today this is only allowed if a Unc or DosPath was found after the scheme int end = (int)(cF & Flags.IndexMask); + // If end exceeds string length, URI is malformed (e.g., bidi chars were stripped) + if (end > _string.Length) + { + throw GetException(ParsingError.BadHostName)!; + } while (idx < end && (_string[idx] == '/' || _string[idx] == '\\')) { notCanonicalScheme = true; @@ -2331,7 +2352,13 @@ private unsafe void CreateUriInfo(Flags cF) ) { //there is no Authority component defined - info.Offset.User = (int)(cF & Flags.IndexMask); + int pathIndex = (int)(cF & Flags.IndexMask); + // Validate index is within string bounds + if (pathIndex > _string.Length) + { + throw GetException(ParsingError.BadHostName)!; + } + info.Offset.User = pathIndex; info.Offset.Host = info.Offset.User; info.Offset.Path = info.Offset.User; cF &= ~Flags.IndexMask; @@ -2348,7 +2375,13 @@ private unsafe void CreateUriInfo(Flags cF) if (HostType == Flags.BasicHostType) { info.Offset.Host = idx; - info.Offset.Path = (int)(cF & Flags.IndexMask); + int pathIndex = (int)(cF & Flags.IndexMask); + // Validate index is within string bounds + if (pathIndex > _string.Length) + { + throw GetException(ParsingError.BadHostName)!; + } + info.Offset.Path = pathIndex; cF &= ~Flags.IndexMask; goto Done; } @@ -2356,11 +2389,15 @@ private unsafe void CreateUriInfo(Flags cF) if ((cF & Flags.HasUserInfo) != 0) { // we previously found a userinfo, get it again - while (_string[idx] != '@') + while (idx < _string.Length && _string[idx] != '@') + { + ++idx; + } + // Only increment if we found '@' within bounds + if (idx < _string.Length) { ++idx; } - ++idx; info.Offset.Host = idx; } else @@ -2371,6 +2408,20 @@ private unsafe void CreateUriInfo(Flags cF) //Now reload the end of the parsed host idx = (int)(cF & Flags.IndexMask); + // Handle cases where _string was modified during parsing (percent-decoding, bidi removal). + // If idx exceeds _string.Length AND we have a file:// URI with UncPath, this indicates + // bidi character removal created a malformed URI (empty host). Otherwise, clamp for IRI processing. + if (idx > _string.Length) + { + if (StaticIsFile(_syntax) && StaticInFact(cF, Flags.UncPath)) + { + // File UNC paths with out-of-bounds indices indicate malformed URIs from bidi removal + throw GetException(ParsingError.BadHostName)!; + } + // For other cases (e.g., percent-decoding in IRI), clamp to string length + idx = _string.Length; + } + //From now on we do not need IndexMask bits, and reuse the space for X_NotCanonical flags //clear them now cF &= ~Flags.IndexMask; diff --git a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriTests.cs b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriTests.cs index bf1a31d6c7927d..afe6e83019e113 100644 --- a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriTests.cs +++ b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriTests.cs @@ -1001,5 +1001,21 @@ static void Test(string uriString) Assert.Throws(() => uri.AbsoluteUri); } } + + [Theory] + [InlineData("/\\//")] + [InlineData("\\/\u200e")] + [InlineData("/\\\\-\u0100\r")] + [InlineData("\\\\\\\\\\")] + [InlineData("\\\\\u200E")] + [InlineData("\\\\\u200E:1234")] + [InlineData("\\\\\u200E//")] + [InlineData("\\\\\u200E//ab")] + public static void InvalidUriWithBidiControlCharacters_ThrowsUriFormatException(string uriString) + { + // These URIs should throw UriFormatException, not IndexOutOfRangeException + // Issue: https://github.com/dotnet/runtime/issues/18640 + Assert.Throws(() => new Uri(uriString, UriKind.Absolute)); + } } }