From 007248f847a8de5cbc6d98650d2d2ae7e3faae7e Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 26 Nov 2025 07:28:32 -0500 Subject: [PATCH 1/2] Add failing test for hostname verifier fallback When allowedHostnames is configured, the custom hostname verifier doesn't fall back to default OkHttp verification. This breaks SSL verification for sites using wildcard/SAN certificates. The test demonstrates that after configuring an allowed hostname override, requests to other valid SSL sites (google.com) fail with SSL errors instead of succeeding. --- .../kotlin/ApiUrlDiscoveryTest.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApiUrlDiscoveryTest.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApiUrlDiscoveryTest.kt index 314961463..57b31abfb 100644 --- a/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApiUrlDiscoveryTest.kt +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApiUrlDiscoveryTest.kt @@ -297,6 +297,31 @@ class ApiUrlDiscoveryTest { ) } + @Test + fun testAllowedHostnamesDoesNotBreakValidSites() = runTest { + val httpClient = WpHttpClient.DefaultHttpClient(emptyList()) + val executor = WpRequestExecutor(httpClient) + val loginClient = WpLoginClient(requestExecutor = executor) + + // First, configure an allowed hostname override for a specific cert/hostname pair + httpClient.addAllowedAlternativeNamesForHostname( + "vanilla.wpmt.co", + listOf("wordpress-1315525-4803651.cloudwaysapps.com") + ) + + // The override should work + assertEquals( + "https://vanilla.wpmt.co/wp-admin/authorize-application.php", + loginClient.apiDiscovery("https://wordpress-1315525-4803651.cloudwaysapps.com") + .assertSuccess().applicationPasswordsAuthenticationUrl.url() + ) + + // Other valid SSL sites should still work via fallback to default hostname verification. + // google.com uses wildcard/SAN certificates which require proper OkHttp verification. + val reason = loginClient.apiDiscovery("https://google.com").assertFailureFindApiRoot() + assertInstanceOf(FindApiRootFailure.ProbablyNotAWordPressSite::class.java, reason) + } + @Test fun testCustomOkHttpClient() = runTest { val executor = From 374e87e973ff69ae5f71e696bfd243ad7a5c2776 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 26 Nov 2025 08:09:32 -0500 Subject: [PATCH 2/2] Fall back to OkHostnameVerifier for default SSL verification The custom WpRequestExecutorHostnameVerifier now falls back to OkHostnameVerifier when the hostname is not in the allowlist. This ensures proper handling of wildcard certificates and SANs while still allowing custom hostname overrides. Changes: - Import OkHostnameVerifier from okhttp3.internal.tls - Check custom allowlist first, then fall back to default verification - Remove conditional verifier application (always use our verifier) --- .../rs/wordpress/api/kotlin/WpHttpClient.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpHttpClient.kt b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpHttpClient.kt index 27c350095..927d58cde 100644 --- a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpHttpClient.kt +++ b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpHttpClient.kt @@ -2,6 +2,7 @@ package rs.wordpress.api.kotlin import okhttp3.Interceptor import okhttp3.OkHttpClient +import okhttp3.internal.tls.OkHostnameVerifier import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSession @@ -25,9 +26,7 @@ sealed class WpHttpClient { private fun buildClient(): OkHttpClient { return OkHttpClient.Builder().apply { this@DefaultHttpClient.interceptors.forEach { addInterceptor(it) } - if (allowedHostnames.isNotEmpty()) { - hostnameVerifier(WpRequestExecutorHostnameVerifier(allowedHostnames)) - } + hostnameVerifier(WpRequestExecutorHostnameVerifier(allowedHostnames)) }.build() } @@ -41,9 +40,12 @@ sealed class WpHttpClient { private class WpRequestExecutorHostnameVerifier(private val allowedHostnames: Map>) : HostnameVerifier { - override fun verify(hostname: String?, session: SSLSession?): Boolean = - session?.let { - val peerPrincipalName = it.peerPrincipal.name.replace("CN=", "") - peerPrincipalName == hostname || allowedHostnames[peerPrincipalName]?.contains(hostname) ?: false - } ?: false + override fun verify(hostname: String?, session: SSLSession?): Boolean { + if (hostname == null || session == null) return false + + // Check our custom allowlist first, then fall back to default OkHttp verification + val peerPrincipalName = session.peerPrincipal.name.replace("CN=", "") + val customMatch = allowedHostnames[peerPrincipalName]?.contains(hostname) ?: false + return customMatch || OkHostnameVerifier.verify(hostname, session) + } }