From 9e3559ef931576256f73d8f36ee8cf9a8dd71ca3 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Mon, 6 Jan 2025 10:15:31 +0100 Subject: [PATCH] HTTPCLIENT-2353: Fix IDN hostname mismatch by normalizing identity with IDN.toUnicode before comparison so that Unicode and punycode forms match correctly. (#607) --- .../http/ssl/DefaultHostnameVerifier.java | 28 +++++-- .../http/ssl/TestDefaultHostnameVerifier.java | 76 +++++++++++++++++++ 2 files changed, 98 insertions(+), 6 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 3d013865f..9b7418bf3 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 @@ public final class DefaultHostnameVerifier implements HttpClientHostnameVerifier final PublicSuffixMatcher publicSuffixMatcher, final DomainType domainType, final boolean strict) { + + final String normalizedIdentity; + try { + // Convert only the identity to its Unicode form + normalizedIdentity = IDN.toUnicode(identity); + } catch (final IllegalArgumentException e) { + return false; + } + + // Public suffix check on the Unicode identity if (publicSuffixMatcher != null && host.contains(".")) { - if (publicSuffixMatcher.getDomainRoot(identity, domainType) == null) { + if (publicSuffixMatcher.getDomainRoot(normalizedIdentity, domainType) == null) { return false; } } @@ -239,10 +250,11 @@ public final class DefaultHostnameVerifier implements HttpClientHostnameVerifier // 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 = normalizedIdentity.indexOf('*'); if (asteriskIdx != -1) { - final String prefix = identity.substring(0, asteriskIdx); - final String suffix = identity.substring(asteriskIdx + 1); + final String prefix = normalizedIdentity.substring(0, asteriskIdx); + final String suffix = normalizedIdentity.substring(asteriskIdx + 1); + if (!prefix.isEmpty() && !host.startsWith(prefix)) { return false; } @@ -252,12 +264,16 @@ public final class DefaultHostnameVerifier implements HttpClientHostnameVerifier // 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()); + prefix.length(), + host.length() - suffix.length() + ); return !remainder.contains("."); } return true; } - return host.equalsIgnoreCase(identity); + + // Direct Unicode comparison + return host.equalsIgnoreCase(normalizedIdentity); } 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 51501e4ea..6ca2ac488 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,80 @@ class TestDefaultHostnameVerifier { publicSuffixMatcher)); } + @Test + void testMatchIdentity() { + // Test 1: IDN matching punycode + final String unicodeHost1 = "поиск-слов.рф"; + final String punycodeHost1 = "xn----dtbqigoecuc.xn--p1ai"; + + // These should now match, thanks to IDN.toASCII(): + Assertions.assertTrue( + DefaultHostnameVerifier.matchIdentity(unicodeHost1, punycodeHost1), + "Expected the Unicode host and its punycode to match" + ); + + // ‘example.com’ vs. an unrelated punycode domain should fail: + Assertions.assertFalse( + DefaultHostnameVerifier.matchIdentity("example.com", punycodeHost1), + "Expected mismatch between example.com and xn----dtbqigoecuc.xn--p1ai" + ); + + // Test 2: Unicode host and Unicode identity + final String unicodeHost2 = "пример.рф"; + final String unicodeIdentity2 = "пример.рф"; + Assertions.assertTrue( + DefaultHostnameVerifier.matchIdentity(unicodeHost2, unicodeIdentity2), + "Expected Unicode host and Unicode identity to match" + ); + + // Test 3: Punycode host and Unicode identity + final String unicodeHost3 = "пример.рф"; + final String punycodeIdentity3 = "xn--e1afmkfd.xn--p1ai"; + Assertions.assertTrue( + DefaultHostnameVerifier.matchIdentity(unicodeHost3, punycodeIdentity3), + "Expected Unicode host and punycode identity to match" + ); + + // Test 4: Wildcard matching in the left-most label + final String unicodeHost4 = "sub.пример.рф"; + final String unicodeIdentity4 = "*.пример.рф"; + Assertions.assertTrue( + DefaultHostnameVerifier.matchIdentity(unicodeHost4, unicodeIdentity4), + "Expected wildcard to match subdomain" + ); + + // Test 5: Invalid host + final String invalidHost = "invalid_host"; + final String unicodeIdentity5 = "пример.рф"; + Assertions.assertFalse( + DefaultHostnameVerifier.matchIdentity(invalidHost, unicodeIdentity5), + "Expected invalid host to not match" + ); + + // Test 6: Invalid identity + final String unicodeHost4b = "пример.рф"; + final String invalidIdentity = "xn--invalid-punycode"; + Assertions.assertFalse( + DefaultHostnameVerifier.matchIdentity(unicodeHost4b, invalidIdentity), + "Expected invalid identity to not match" + ); + + // Test 7: Mixed case comparison + final String unicodeHost5 = "ПрИмеР.рф"; + final String unicodeIdentity6 = "пример.рф"; + Assertions.assertTrue( + DefaultHostnameVerifier.matchIdentity(unicodeHost5, unicodeIdentity6), + "Expected case-insensitive Unicode comparison to match" + ); + + + // Test 8: Wildcard in the middle label (per RFC 2818, should match) + final String unicodeHost6 = "sub.пример.рф"; + final String unicodeIdentity8 = "sub.*.рф"; + Assertions.assertTrue( + DefaultHostnameVerifier.matchIdentity(unicodeHost6, unicodeIdentity8), + "Expected wildcard in the middle label to match" + ); + } + } \ No newline at end of file