From 83bdee31b8998901c5cf762f969c6c74e10f34d4 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Sat, 4 Jan 2025 13:49:17 +0100 Subject: [PATCH] HTTPCLIENT-2353: Fix IDN hostname mismatch by normalizing host and identity with IDN.toASCII before comparison so that Unicode and punycode forms match correctly. --- .../http/ssl/DefaultHostnameVerifier.java | 29 +++++++++++++------ .../http/ssl/TestDefaultHostnameVerifier.java | 18 ++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultHostnameVerifier.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultHostnameVerifier.java index 3d013865f8..e315f68011 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultHostnameVerifier.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultHostnameVerifier.java @@ -27,6 +27,7 @@ package org.apache.hc.client5.http.ssl; +import java.net.IDN; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.cert.Certificate; @@ -228,8 +229,18 @@ private static boolean matchIdentity(final String host, final String identity, final PublicSuffixMatcher publicSuffixMatcher, final DomainType domainType, final boolean strict) { + + final String punycodeHost; + final String punycodeIdentity; + try { + punycodeHost = IDN.toASCII(host); + punycodeIdentity = IDN.toASCII(identity); + } catch (final IllegalArgumentException e) { + return false; + } + if (publicSuffixMatcher != null && host.contains(".")) { - if (publicSuffixMatcher.getDomainRoot(identity, domainType) == null) { + if (publicSuffixMatcher.getDomainRoot(punycodeIdentity, domainType) == null) { return false; } } @@ -239,25 +250,25 @@ private static boolean matchIdentity(final String host, final String identity, // character * which is considered to match any single domain name // component or component fragment..." // Based on this statement presuming only singular wildcard is legal - final int asteriskIdx = identity.indexOf('*'); + final int asteriskIdx = punycodeIdentity.indexOf('*'); if (asteriskIdx != -1) { - final String prefix = identity.substring(0, asteriskIdx); - final String suffix = identity.substring(asteriskIdx + 1); - if (!prefix.isEmpty() && !host.startsWith(prefix)) { + final String prefix = punycodeIdentity.substring(0, asteriskIdx); + final String suffix = punycodeIdentity.substring(asteriskIdx + 1); + if (!prefix.isEmpty() && !punycodeHost.startsWith(prefix)) { return false; } - if (!suffix.isEmpty() && !host.endsWith(suffix)) { + if (!suffix.isEmpty() && !punycodeHost.endsWith(suffix)) { return false; } // Additional sanity checks on content selected by wildcard can be done here if (strict) { - final String remainder = host.substring( - prefix.length(), host.length() - suffix.length()); + final String remainder = punycodeHost.substring( + prefix.length(), punycodeHost.length() - suffix.length()); return !remainder.contains("."); } return true; } - return host.equalsIgnoreCase(identity); + return punycodeHost.equalsIgnoreCase(punycodeIdentity); } static boolean matchIdentity(final String host, final String identity, diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/TestDefaultHostnameVerifier.java b/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/TestDefaultHostnameVerifier.java index 51501e4eaf..6bee8d2c5c 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/TestDefaultHostnameVerifier.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/TestDefaultHostnameVerifier.java @@ -472,4 +472,22 @@ void testMatchDNSName() throws Exception { publicSuffixMatcher)); } + @Test + void testMatchIdentityWithIDN() { + final String unicodeHost = "поиск-слов.рф"; + final String punycodeHost = "xn----dtbqigoecuc.xn--p1ai"; + + // These should now match, thanks to IDN.toASCII(): + Assertions.assertTrue( + DefaultHostnameVerifier.matchIdentity(unicodeHost, punycodeHost), + "Expected the Unicode host and its punycode to match" + ); + + // ‘example.com’ vs. an unrelated punycode domain should fail: + Assertions.assertFalse( + DefaultHostnameVerifier.matchIdentity("example.com", punycodeHost), + "Expected mismatch between example.com and xn----dtbqigoecuc.xn--p1ai" + ); + } + } \ No newline at end of file