From 00081f3aab15a1384acf7ea7492b11d12e4fc90f Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Sun, 20 Sep 2026 19:54:06 +0530 Subject: [PATCH] split the authority before IDN conversion in isValidAuthority isValidAuthority converted the whole authority with IDN.toASCII and split the result. Nameprep folds the fullwidth '@' and ':' to ASCII and punycode encodes a whole label, so the conversion invented delimiters that are not in the URL and pulled the userinfo or port into the host label. Match the raw authority instead and let DomainValidator convert the host alone. --- .../validator/routines/DomainValidator.java | 2 +- .../commons/validator/routines/UrlValidator.java | 12 +++++++----- .../validator/routines/UrlValidatorTest.java | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apache/commons/validator/routines/DomainValidator.java b/src/main/java/org/apache/commons/validator/routines/DomainValidator.java index 3a0a1121c..7497b80dc 100644 --- a/src/main/java/org/apache/commons/validator/routines/DomainValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/DomainValidator.java @@ -1995,7 +1995,7 @@ private static boolean isOnlyASCII(final String input) { * @param input The string to convert, not null. * @return converted input, or original input if conversion fails. */ - // Needed by UrlValidator + // package protected for unit test access static String unicodeToASCII(final String input) { if (isOnlyASCII(input)) { // skip possibly expensive processing return input; diff --git a/src/main/java/org/apache/commons/validator/routines/UrlValidator.java b/src/main/java/org/apache/commons/validator/routines/UrlValidator.java index 3cead7bfe..1d7d84307 100644 --- a/src/main/java/org/apache/commons/validator/routines/UrlValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/UrlValidator.java @@ -117,7 +117,9 @@ public class UrlValidator implements Serializable { // Drop numeric, and "+-." for now // TODO does not allow for optional userinfo. // Validation of character set is done by isValidAuthority - private static final String AUTHORITY_CHARS_REGEX = "\\p{Alnum}\\-\\."; // allows for IPV4 but not IPV6 + // Non-ASCII characters are admitted so an IDN host can be split from the userinfo and port before it is converted + // to ASCII; DomainValidator then converts and checks the host on its own. + private static final String AUTHORITY_CHARS_REGEX = "\\p{Alnum}\\-\\.\\P{ASCII}"; // allows for IPV4 but not IPV6 // Captured inside [ ] in AUTHORITY_REGEX and validated by InetAddressValidator.isValidInet6Address, so the // dot is allowed for IPv4-mapped/embedded forms (for example ::ffff:1.2.3.4 or 2001:db8::1.2.3.4), not just ::FFFF: private static final String IPV6_REGEX = "[0-9a-fA-F:.]+"; // the brackets remove the port-prefix ':' ambiguity @@ -421,10 +423,10 @@ protected boolean isValidAuthority(final String authority) { if (authorityValidator != null && authorityValidator.isValid(authority)) { return true; } - // convert to ASCII if possible - final String authorityASCII = DomainValidator.unicodeToASCII(authority); - - final Matcher authorityMatcher = AUTHORITY_PATTERN.matcher(authorityASCII); + // Split the authority before any IDN conversion. IDN.toASCII folds compatibility characters such as the + // fullwidth '@' and ':' to their ASCII forms and punycodes a whole label, so converting the authority first + // would invent delimiters that are not in the URL and encode the userinfo or port into the host label. + final Matcher authorityMatcher = AUTHORITY_PATTERN.matcher(authority); if (!authorityMatcher.matches()) { return false; } diff --git a/src/test/java/org/apache/commons/validator/routines/UrlValidatorTest.java b/src/test/java/org/apache/commons/validator/routines/UrlValidatorTest.java index 7b247209f..ea95557b6 100644 --- a/src/test/java/org/apache/commons/validator/routines/UrlValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/UrlValidatorTest.java @@ -216,6 +216,21 @@ void testFragments() { assertTrue(urlValidator.isValid("http://apache.org/a/b/c#frag")); } + @Test + void testIdnAuthority() { + final UrlValidator urlValidator = new UrlValidator(); + // the userinfo and port are split off before the IDN conversion, so neither is punycoded into a host label + assertTrue(urlValidator.isValid("http://президент.рф:8080/")); + assertTrue(urlValidator.isValid("http://user:pass@президент.рф:8080/index.html")); + assertTrue(urlValidator.isValidAuthority("user@www.b\u00fccher.ch")); + assertFalse(urlValidator.isValidAuthority("президент.рф:65536")); + // nameprep folds the fullwidth commercial at (U+FF20) and the fullwidth colon (U+FF1A) to '@' and ':'; + // neither is a delimiter in the URL as given, so it must not be read as the userinfo or port separator + assertFalse(urlValidator.isValid("http://example.com\uFF20apache.org/")); + assertFalse(urlValidator.isValid("http://user\uFF1Apass\uFF20apache.org/")); + assertFalse(urlValidator.isValid("http://apache.org\uFF1A80/")); + } + @Test void testIpv6EmbeddedIpv4() { final UrlValidator urlValidator = new UrlValidator();