Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down