From 21722b1eb43fb98f466abfb8b1d7e1790763ca8c Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Thu, 23 Jul 2026 04:29:42 +0530 Subject: [PATCH 1/2] reject nameprep-stripped code points in unicodeToASCII --- .../validator/routines/DomainValidator.java | 26 +++++++++++++++---- .../routines/DomainValidatorTest.java | 12 +++++++++ 2 files changed, 33 insertions(+), 5 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 1534cac9e..56061fcbe 100644 --- a/src/main/java/org/apache/commons/validator/routines/DomainValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/DomainValidator.java @@ -1966,6 +1966,19 @@ private static boolean isOnlyASCII(final String input) { return true; } + /* + * Tests whether the code point is one that IDNA nameprep (RFC 3454 Table B.1, "commonly mapped to + * nothing") deletes but that is not a Unicode FORMAT character, so the FORMAT check in + * unicodeToASCII does not catch it: the combining grapheme joiner, the Mongolian TODO soft hyphen + * and free variation selectors, and the variation selectors. + */ + private static boolean isNameprepMappedToNothing(final int codePoint) { + return codePoint == '\u034F' // COMBINING GRAPHEME JOINER + || codePoint == '\u1806' // MONGOLIAN TODO SOFT HYPHEN + || codePoint >= '\u180B' && codePoint <= '\u180D' // MONGOLIAN FREE VARIATION SELECTOR ONE..THREE + || codePoint >= '\uFE00' && codePoint <= '\uFE0F'; // VARIATION SELECTOR-1..16 + } + /** * Converts potentially Unicode input to punycode. If conversion fails, returns the original input. * @@ -1977,13 +1990,16 @@ static String unicodeToASCII(final String input) { if (isOnlyASCII(input)) { // skip possibly expensive processing return input; } - // IDN.toASCII silently drops default-ignorable and format code points (soft hyphen, - // zero-width spaces, the byte order mark, ...) during nameprep, so a host carrying one - // would convert to a clean label and validate. Those code points are not legal in a host - // name, so keep the original here and let the label regex reject it. + // IDN.toASCII silently drops the code points that nameprep (RFC 3454 Table B.1) maps to + // nothing - the soft hyphen, zero-width spaces, the byte order mark, the combining grapheme + // joiner, the Mongolian and variation selectors and so on - so a host carrying one would + // convert to a clean label and validate as a different host. Most are Unicode FORMAT + // characters, but the combining grapheme joiner, the Mongolian selectors and the variation + // selectors are not, so the FORMAT check alone lets them through. Reject both here and let + // the label regex reject anything else. for (int i = 0; i < input.length();) { final int codePoint = input.codePointAt(i); - if (Character.getType(codePoint) == Character.FORMAT) { + if (Character.getType(codePoint) == Character.FORMAT || isNameprepMappedToNothing(codePoint)) { return input; } i += Character.charCount(codePoint); diff --git a/src/test/java/org/apache/commons/validator/routines/DomainValidatorTest.java b/src/test/java/org/apache/commons/validator/routines/DomainValidatorTest.java index 58ab7db76..e9b6fb89d 100644 --- a/src/test/java/org/apache/commons/validator/routines/DomainValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/DomainValidatorTest.java @@ -470,6 +470,18 @@ void testIDNFormatCodePoints() { assertTrue(validator.isValid("www.b\u00fccher.ch"), "b\u00fccher.ch should still validate"); } + @Test + void testIDNMappedToNothing() { + // IDN.toASCII also strips the code points that nameprep maps to nothing but that are not + // Unicode FORMAT characters, so the format-code-point guard alone would let them collapse a + // host to a clean label. They must be rejected too. + assertFalse(validator.isValid("exa\u034Fmple.com"), "combining grapheme joiner shouldn't validate"); + assertFalse(validator.isValid("exa\uFE0Fmple.com"), "variation selector shouldn't validate"); + assertFalse(validator.isValid("exa\u1806mple.com"), "Mongolian TODO soft hyphen shouldn't validate"); + assertFalse(validator.isValid("exa\u180Bmple.com"), "Mongolian free variation selector shouldn't validate"); + assertTrue(validator.isValid("www.b\u00fccher.ch"), "b\u00fccher.ch should still validate"); + } + @Test void testIDNJava6OrLater() { // xn--d1abbgf6aiiy.xn--p1ai http://президент.рф From b608cb524f29d7f3fc45a7bb13efeb77d72aec24 Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Thu, 23 Jul 2026 14:47:12 +0530 Subject: [PATCH 2/2] cover zero-width non-joiner in testIDNFormatCodePoints --- .../apache/commons/validator/routines/DomainValidatorTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/apache/commons/validator/routines/DomainValidatorTest.java b/src/test/java/org/apache/commons/validator/routines/DomainValidatorTest.java index e9b6fb89d..18aeeaa54 100644 --- a/src/test/java/org/apache/commons/validator/routines/DomainValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/DomainValidatorTest.java @@ -465,6 +465,7 @@ void testIDNFormatCodePoints() { // Those code points are not legal in a host name and must be rejected. assertFalse(validator.isValid("exa\u00ADmple.com"), "soft hyphen shouldn't validate"); assertFalse(validator.isValid("exa\u200Bmple.com"), "zero-width space shouldn't validate"); + assertFalse(validator.isValid("exa\u200Cmple.com"), "zero-width non-joiner shouldn't validate"); assertFalse(validator.isValid("exa\u200Dmple.com"), "zero-width joiner shouldn't validate"); assertFalse(validator.isValid("\uFEFFexample.com"), "byte order mark shouldn't validate"); assertTrue(validator.isValid("www.b\u00fccher.ch"), "b\u00fccher.ch should still validate");