Skip to content

Commit 758ad8d

Browse files
committed
HTTPCLIENT-2353: Fix IDN hostname mismatch by normalizing identity and host with IDN.toUnicode before comparison so that Unicode and punycode forms match correctly.
1 parent 4b2a365 commit 758ad8d

File tree

2 files changed

+118
-10
lines changed

2 files changed

+118
-10
lines changed

httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultHostnameVerifier.java

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
package org.apache.hc.client5.http.ssl;
2929

30+
import java.net.IDN;
3031
import java.net.InetAddress;
3132
import java.net.UnknownHostException;
3233
import java.security.cert.Certificate;
@@ -228,8 +229,19 @@ private static boolean matchIdentity(final String host, final String identity,
228229
final PublicSuffixMatcher publicSuffixMatcher,
229230
final DomainType domainType,
230231
final boolean strict) {
231-
if (publicSuffixMatcher != null && host.contains(".")) {
232-
if (publicSuffixMatcher.getDomainRoot(identity, domainType) == null) {
232+
233+
final String normalizedHost;
234+
final String normalizedIdentity;
235+
try {
236+
normalizedHost = IDN.toUnicode(host);
237+
normalizedIdentity = IDN.toUnicode(identity);
238+
} catch (final IllegalArgumentException e) {
239+
return false;
240+
}
241+
242+
// Public suffix check on the Unicode identity
243+
if (publicSuffixMatcher != null && normalizedHost.contains(".")) {
244+
if (publicSuffixMatcher.getDomainRoot(normalizedIdentity, domainType) == null) {
233245
return false;
234246
}
235247
}
@@ -239,25 +251,30 @@ private static boolean matchIdentity(final String host, final String identity,
239251
// character * which is considered to match any single domain name
240252
// component or component fragment..."
241253
// Based on this statement presuming only singular wildcard is legal
242-
final int asteriskIdx = identity.indexOf('*');
254+
final int asteriskIdx = normalizedIdentity.indexOf('*');
243255
if (asteriskIdx != -1) {
244-
final String prefix = identity.substring(0, asteriskIdx);
245-
final String suffix = identity.substring(asteriskIdx + 1);
246-
if (!prefix.isEmpty() && !host.startsWith(prefix)) {
256+
final String prefix = normalizedIdentity.substring(0, asteriskIdx);
257+
final String suffix = normalizedIdentity.substring(asteriskIdx + 1);
258+
259+
if (!prefix.isEmpty() && !normalizedHost.startsWith(prefix)) {
247260
return false;
248261
}
249-
if (!suffix.isEmpty() && !host.endsWith(suffix)) {
262+
if (!suffix.isEmpty() && !normalizedHost.endsWith(suffix)) {
250263
return false;
251264
}
252265
// Additional sanity checks on content selected by wildcard can be done here
253266
if (strict) {
254-
final String remainder = host.substring(
255-
prefix.length(), host.length() - suffix.length());
267+
final String remainder = normalizedHost.substring(
268+
prefix.length(),
269+
normalizedHost.length() - suffix.length()
270+
);
256271
return !remainder.contains(".");
257272
}
258273
return true;
259274
}
260-
return host.equalsIgnoreCase(identity);
275+
276+
// Direct Unicode comparison
277+
return normalizedHost.equalsIgnoreCase(normalizedIdentity);
261278
}
262279

263280
static boolean matchIdentity(final String host, final String identity,

httpclient5/src/test/java/org/apache/hc/client5/http/ssl/TestDefaultHostnameVerifier.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,4 +472,95 @@ void testMatchDNSName() throws Exception {
472472
publicSuffixMatcher));
473473
}
474474

475+
@Test
476+
void testMatchIdentity() {
477+
// Test 1: IDN matching punycode
478+
final String unicodeHost1 = "поиск-слов.рф";
479+
final String punycodeHost1 = "xn----dtbqigoecuc.xn--p1ai";
480+
481+
// These should now match, thanks to IDN.toASCII():
482+
Assertions.assertTrue(
483+
DefaultHostnameVerifier.matchIdentity(unicodeHost1, punycodeHost1),
484+
"Expected the Unicode host and its punycode to match"
485+
);
486+
487+
// ‘example.com’ vs. an unrelated punycode domain should fail:
488+
Assertions.assertFalse(
489+
DefaultHostnameVerifier.matchIdentity("example.com", punycodeHost1),
490+
"Expected mismatch between example.com and xn----dtbqigoecuc.xn--p1ai"
491+
);
492+
493+
// Test 2: Unicode host and Unicode identity
494+
final String unicodeHost2 = "пример.рф";
495+
final String unicodeIdentity2 = "пример.рф";
496+
Assertions.assertTrue(
497+
DefaultHostnameVerifier.matchIdentity(unicodeHost2, unicodeIdentity2),
498+
"Expected Unicode host and Unicode identity to match"
499+
);
500+
501+
// Test 3: Punycode host and Unicode identity
502+
final String punycodeHost2 = "xn--e1afmkfd.xn--p1ai";
503+
final String unicodeIdentity3 = "пример.рф";
504+
Assertions.assertTrue(
505+
DefaultHostnameVerifier.matchIdentity(punycodeHost2, unicodeIdentity3),
506+
"Expected punycode host and Unicode identity to match"
507+
);
508+
509+
// Test 4: Unicode host and Punycode identity
510+
final String unicodeHost3 = "пример.рф";
511+
final String punycodeIdentity3 = "xn--e1afmkfd.xn--p1ai";
512+
Assertions.assertTrue(
513+
DefaultHostnameVerifier.matchIdentity(unicodeHost3, punycodeIdentity3),
514+
"Expected Unicode host and punycode identity to match"
515+
);
516+
517+
// Test 5: Wildcard matching in the left-most label
518+
final String unicodeHost4 = "sub.пример.рф";
519+
final String unicodeIdentity4 = "*.пример.рф";
520+
Assertions.assertTrue(
521+
DefaultHostnameVerifier.matchIdentity(unicodeHost4, unicodeIdentity4),
522+
"Expected wildcard to match subdomain"
523+
);
524+
525+
// Test 6: Invalid host
526+
final String invalidHost = "invalid_host";
527+
final String unicodeIdentity5 = "пример.рф";
528+
Assertions.assertFalse(
529+
DefaultHostnameVerifier.matchIdentity(invalidHost, unicodeIdentity5),
530+
"Expected invalid host to not match"
531+
);
532+
533+
// Test 7: Invalid identity
534+
final String unicodeHost4b = "пример.рф";
535+
final String invalidIdentity = "xn--invalid-punycode";
536+
Assertions.assertFalse(
537+
DefaultHostnameVerifier.matchIdentity(unicodeHost4b, invalidIdentity),
538+
"Expected invalid identity to not match"
539+
);
540+
541+
// Test 8: Mixed case comparison
542+
final String unicodeHost5 = "ПрИмеР.рф";
543+
final String unicodeIdentity6 = "пример.рф";
544+
Assertions.assertTrue(
545+
DefaultHostnameVerifier.matchIdentity(unicodeHost5, unicodeIdentity6),
546+
"Expected case-insensitive Unicode comparison to match"
547+
);
548+
549+
// Test 9: Wildcard with punycode host
550+
final String punycodeHost3 = "xn--a-7h.xn--e1afmkfd.xn--p1ai";
551+
final String unicodeIdentity7 = "*.пример.рф";
552+
Assertions.assertTrue(
553+
DefaultHostnameVerifier.matchIdentity(punycodeHost3, unicodeIdentity7),
554+
"Expected wildcard to match punycode-encoded host"
555+
);
556+
557+
// Test 10: Wildcard in the middle label (per RFC 2818, should match)
558+
final String unicodeHost6 = "sub.пример.рф";
559+
final String unicodeIdentity8 = "sub.*.рф";
560+
Assertions.assertTrue(
561+
DefaultHostnameVerifier.matchIdentity(unicodeHost6, unicodeIdentity8),
562+
"Expected wildcard in the middle label to match"
563+
);
564+
}
565+
475566
}

0 commit comments

Comments
 (0)