From d17f3928f0af51d9b5b0a6018f5aab4bf31ba208 Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Thu, 16 Jul 2026 18:06:52 +0530 Subject: [PATCH] reject leading and trailing label hyphens in unicodeToASCII --- .../validator/routines/DomainValidator.java | 41 +++++++++++++++++++ .../routines/DomainValidatorTest.java | 9 ++-- 2 files changed, 47 insertions(+), 3 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 505409671..1534cac9e 100644 --- a/src/main/java/org/apache/commons/validator/routines/DomainValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/DomainValidator.java @@ -1919,6 +1919,38 @@ public static synchronized String[] getTLDEntries(final ArrayType table) { return Arrays.copyOf(array, array.length); // clone the array } + /* + * Tests whether any label in the input begins or ends with an ASCII hyphen, which is not + * permitted in a host name label. Labels are separated by the dot characters recognized in + * RFC 3490 section 3.1. + */ + private static boolean hasLabelBoundaryHyphen(final String input) { + boolean labelStart = true; + for (int i = 0; i < input.length(); i++) { + final char ch = input.charAt(i); + if (isLabelSeparator(ch)) { + if (i > 0 && input.charAt(i - 1) == '-') { + return true; // label ends with a hyphen + } + labelStart = true; + } else { + if (labelStart && ch == '-') { + return true; // label begins with a hyphen + } + labelStart = false; + } + } + final int last = input.length() - 1; + return last >= 0 && input.charAt(last) == '-'; + } + + /* + * Tests whether the character is one of the label separators recognized in RFC 3490 section 3.1. + */ + private static boolean isLabelSeparator(final char ch) { + return ch == '.' || ch == '\u3002' || ch == '\uFF0E' || ch == '\uFF61'; + } + /* * Tests whether input contains only ASCII. Treats null as all ASCII. */ @@ -1956,6 +1988,15 @@ static String unicodeToASCII(final String input) { } i += Character.charCount(codePoint); } + // A label must not begin or end with a hyphen (RFC 1123, and RFC 5891 for IDN labels). + // IDN.toASCII with the default flags does not enforce this: it punycode-encodes such a + // label (for example a leading-hyphen "-tést" becomes "xn---tst-cpa") to a form that + // then satisfies the label regex, so the hyphen slips through on a non-ASCII label although + // the all-ASCII form is rejected. Keep the original here and let the label regex reject it + // (VALIDATOR-501). + if (hasLabelBoundaryHyphen(input)) { + return input; + } try { final String ascii = IDN.toASCII(input); if (IDNBUGHOLDER.IDN_TOASCII_PRESERVES_TRAILING_DOTS) { 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 0b3e385a8..58ab7db76 100644 --- a/src/test/java/org/apache/commons/validator/routines/DomainValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/DomainValidatorTest.java @@ -47,7 +47,6 @@ import org.apache.commons.validator.routines.DomainValidator.ArrayType; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; /** @@ -506,11 +505,15 @@ void testInvalidDomains() { } @Test - @Disabled void testInvalidDomains501() { - // VALIDATOR-501 + // VALIDATOR-501: a non-ASCII label starting or ending with a hyphen is punycode-encoded by + // IDN.toASCII to a form that passes the label regex, so it slipped through although the + // all-ASCII form ("-test.fr" / "test-.fr") is rejected in testInvalidDomains above. assertFalse(validator.isValid("-tést.fr")); assertFalse(validator.isValid("tést-.fr")); + assertFalse(validator.isValid("-é.fr"), "single non-ASCII label starting with a hyphen shouldn't validate"); + // an interior hyphen on a non-ASCII label is still allowed + assertTrue(validator.isValid("a-é.fr"), "interior hyphen on a non-ASCII label should validate"); } // Check if IDN.toASCII is broken or not