HTTPCLIENT-2353: Fix IDN hostname mismatch by normalizing identity with IDN.toUnicode before comparison so that Unicode and punycode forms match correctly. (#607)

This commit is contained in:
Arturo Bernal 2025-01-06 10:15:31 +01:00 committed by GitHub
parent 55cdd9e94f
commit 9e3559ef93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 98 additions and 6 deletions

View File

@ -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,

View File

@ -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"
);
}
}