Fix out-of-bounds read for truncated percent-escape in opaque host

The opaque-host percent-escape validation guard reads
input.codePointAt(i + 2) after only checking 'input.length() - i < 2',
so an input such as 'foo://%4' throws StringIndexOutOfBoundsException
instead of reporting a validation error.

Fix the bounds guard to require two code points after '%' and check
ASCII hex digits rather than ASCII digits, matching the URL spec, where
invalid percent-escapes in opaque hosts are validation errors, not
failures.

Signed-off-by: Sagar Chanchal <Sagarr2112@gmail.com>
This commit is contained in:
Sagar Chanchal
2026-09-18 17:26:03 +02:00
committed by Brian Clozel
parent 2aa36fea64
commit cb9ce4d9e2
2 changed files with 20 additions and 2 deletions
@@ -2241,8 +2241,8 @@ final class WhatWgUrlParser {
// If input contains a U+0025 (%) and the two code points following it
// are not ASCII hex digits, invalid-URL-unit validation error.
if (p.validate() && ch == '%' &&
(input.length() - i < 2 || !isAsciiDigit(input.codePointAt(i + 1)) ||
!isAsciiDigit(input.codePointAt(i + 2)))) {
(input.length() - i < 3 || !isAsciiHexDigit(input.codePointAt(i + 1)) ||
!isAsciiHexDigit(input.codePointAt(i + 2)))) {
p.validationError("Code point \"" + ch + "\" is not a URL unit.");
}
}
@@ -16,6 +16,9 @@
package org.springframework.web.util;
import java.util.ArrayList;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
@@ -63,6 +66,21 @@ class WhatWgUrlParserTests {
testParse("file://xn--/p", "file", "xn--", null, "/p", null, null);
}
@Test
void parseOpaqueHostTruncatedPercentEscape() {
// A truncated or non-hex percent-escape in an opaque host is a validation error,
// not a failure, and must not read past the end of the input
// (see https://url.spec.whatwg.org/#concept-opaque-host-parser).
List<String> errors = new ArrayList<>();
WhatWgUrlParser.UrlRecord record = WhatWgUrlParser.parse("foo://%4", EMPTY_URL_RECORD, null, errors::add);
assertThat(record.host().toString()).isEqualTo("%4");
record = WhatWgUrlParser.parse("foo://%4x", EMPTY_URL_RECORD, null, errors::add);
assertThat(record.host().toString()).isEqualTo("%4x");
assertThat(errors).isNotEmpty();
}
private void testParse(String input, String scheme, @Nullable String host, @Nullable String port, String path, @Nullable String query, @Nullable String fragment) {
WhatWgUrlParser.UrlRecord result = WhatWgUrlParser.parse(input, EMPTY_URL_RECORD, null, null);
assertThat(result.scheme()).as("Invalid scheme").isEqualTo(scheme);