From f6d08ea48d7a1d09e79e965620a5971d4dcb8f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Mon, 22 Jun 2026 16:53:29 +0300 Subject: [PATCH] Reject hostnames with unpaired UTF-16 surrogates isAscii() treated a lone UTF-16 surrogate as ASCII because okio encodes it as one `?` byte. Scan the string so any non-ASCII code unit is detected. Fixes #6357 --- .../okhttp3/internal/tls/OkHostnameVerifier.kt | 9 +++++++-- .../okhttp3/internal/tls/HostnameVerifierTest.kt | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/tls/OkHostnameVerifier.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/tls/OkHostnameVerifier.kt index 0431b28e3ba4..d3625bf6e975 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/tls/OkHostnameVerifier.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/tls/OkHostnameVerifier.kt @@ -24,7 +24,6 @@ import javax.net.ssl.SSLException import javax.net.ssl.SSLSession import okhttp3.internal.canParseAsIpAddress import okhttp3.internal.toCanonicalHost -import okio.utf8Size /** * A HostnameVerifier consistent with [RFC 2818][rfc_2818]. @@ -96,7 +95,13 @@ object OkHostnameVerifier : HostnameVerifier { } /** Returns true if the [String] is ASCII encoded (0-127). */ - private fun String.isAscii() = length == utf8Size().toInt() + private fun String.isAscii(): Boolean { + for (i in 0 until length) { + val c = this[i] + if (c >= '\u0080') return false + } + return true + } /** * Returns true if [hostname] matches the domain name [pattern]. diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/internal/tls/HostnameVerifierTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/internal/tls/HostnameVerifierTest.kt index 070fc13ff57a..56ca182ce4ff 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/internal/tls/HostnameVerifierTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/internal/tls/HostnameVerifierTest.kt @@ -803,6 +803,21 @@ class HostnameVerifierTest { assertThat(localVerifier.verify("\uD83D\uDCA9.com", session)).isFalse() } + @Test fun verifyMalformedSurrogateHostname() { + // A hostname with an unpaired UTF-16 surrogate is not ASCII and must be rejected, not + // matched against the wildcard. https://github.com/square/okhttp/issues/6357 + val heldCertificate = + HeldCertificate + .Builder() + .commonName("Foo Corp") + .addSubjectAlternativeName("*.com") + .build() + val session = session(heldCertificate.certificatePem()) + assertThat(verifier.verify("example.com", session)).isTrue() + assertThat(verifier.verify("\uD800.com", session)).isFalse() + assertThat(verifier.verify("\uDC00.com", session)).isFalse() + } + @Test fun verifyAsIpAddress() { // IPv4 assertThat("127.0.0.1".canParseAsIpAddress()).isTrue()