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();