From 27f3faf6d3cb01aa2cf374bdb90a05ea822e9823 Mon Sep 17 00:00:00 2001 From: one-kash <26795040+one-kash@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:48:53 +0000 Subject: [PATCH] Fix isAscii returning true for malformed surrogates The previous implementation used `length == utf8Size()` to determine if a string was ASCII. This was incorrect because Okio's utf8Size() maps malformed surrogates (unpaired high/low surrogates) to the replacement character `?` which is 1 byte in UTF-8. This caused strings like "\uD800.com" (unpaired high surrogate) to incorrectly report as ASCII since: - length = 5 (one char for surrogate + 4 for ".com") - utf8Size() = 5 (surrogate mapped to '?' = 1 byte + 4 for ".com") The fix iterates through each character and checks if its code point is within the ASCII range (0-127). Fixes #6357 --- .../internal/tls/OkHostnameVerifier.kt | 3 +-- .../internal/tls/HostnameVerifierTest.kt | 27 +++++++++++++++++++ 2 files changed, 28 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 d70e0959c3a8..2b5610c2d439 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]. @@ -94,7 +93,7 @@ object OkHostnameVerifier : HostnameVerifier { } /** Returns true if the [String] is ASCII encoded (0-127). */ - private fun String.isAscii() = length == utf8Size().toInt() + private fun String.isAscii() = all { it.code <= 127 } /** * 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..64b1f38ac403 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/internal/tls/HostnameVerifierTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/internal/tls/HostnameVerifierTest.kt @@ -803,6 +803,33 @@ class HostnameVerifierTest { assertThat(localVerifier.verify("\uD83D\uDCA9.com", session)).isFalse() } + /** + * Test that malformed surrogates (unpaired high/low surrogates) are correctly + * identified as non-ASCII and rejected. + * + * https://github.com/square/okhttp/issues/6357 + */ + @Test fun malformedSurrogatesAreNotAscii() { + val heldCertificate = + HeldCertificate + .Builder() + .commonName("Foo Corp") + .addSubjectAlternativeName("foo.com") + .build() + val session = session(heldCertificate.certificatePem()) + + // Unpaired high surrogate - should not match any hostname + assertThat(verifier.verify("\uD800.com", session)).isFalse() + assertThat(verifier.verify("foo\uD800.com", session)).isFalse() + + // Unpaired low surrogate - should not match any hostname + assertThat(verifier.verify("\uDC00.com", session)).isFalse() + assertThat(verifier.verify("foo\uDC00.com", session)).isFalse() + + // Valid hostname should still work + assertThat(verifier.verify("foo.com", session)).isTrue() + } + @Test fun verifyAsIpAddress() { // IPv4 assertThat("127.0.0.1".canParseAsIpAddress()).isTrue()