From 224732d215ecd45d8c7c5651a67ae3b8e64815f9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:47:11 +0000 Subject: [PATCH] Fix critical global hostname verification vulnerability Removed the `setDefaultHostnameVerifier` call in `BaseJsoupSource` which was globally disabling hostname verification when `ignoreSslErrors` was enabled. Refactored `NovelFireSource` to use `OkHttpClient` for search queries to avoid reliance on the vulnerable Jsoup connection path. Added a regression test to verify that the global verifier is no longer compromised. Co-authored-by: Aatricks <113598245+Aatricks@users.noreply.github.com> --- .../data/repository/source/BaseJsoupSource.kt | 3 - .../data/repository/source/NovelFireSource.kt | 11 ++-- .../source/BaseJsoupSourceSecurityTest.kt | 55 +++++++++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 app/src/test/java/io/aatricks/novelscraper/data/repository/source/BaseJsoupSourceSecurityTest.kt diff --git a/app/src/main/java/io/aatricks/novelscraper/data/repository/source/BaseJsoupSource.kt b/app/src/main/java/io/aatricks/novelscraper/data/repository/source/BaseJsoupSource.kt index 3da24ae..d7d1f54 100644 --- a/app/src/main/java/io/aatricks/novelscraper/data/repository/source/BaseJsoupSource.kt +++ b/app/src/main/java/io/aatricks/novelscraper/data/repository/source/BaseJsoupSource.kt @@ -60,8 +60,6 @@ abstract class BaseJsoupSource( val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, trustAllCerts, java.security.SecureRandom()) connection.sslSocketFactory(sslContext.socketFactory) - // Note: setDefaultHostnameVerifier is global and might affect other parts of the app - javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier { _, _ -> true } } return connection.get() @@ -86,7 +84,6 @@ abstract class BaseJsoupSource( val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, trustAllCerts, java.security.SecureRandom()) connection.sslSocketFactory(sslContext.socketFactory) - javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier { _, _ -> true } } return connection diff --git a/app/src/main/java/io/aatricks/novelscraper/data/repository/source/NovelFireSource.kt b/app/src/main/java/io/aatricks/novelscraper/data/repository/source/NovelFireSource.kt index 441d1a0..a38b89f 100644 --- a/app/src/main/java/io/aatricks/novelscraper/data/repository/source/NovelFireSource.kt +++ b/app/src/main/java/io/aatricks/novelscraper/data/repository/source/NovelFireSource.kt @@ -76,10 +76,13 @@ class NovelFireSource @Inject constructor( val url = "$baseUrl/ajax/searchLive?inputContent=$encodedQuery" runCatching { - val response = connect(url) - .ignoreContentType(true) - .execute() - .body() + val request = okhttp3.Request.Builder() + .url(url) + .header("User-Agent", userAgent) + .header("Referer", baseUrl) + .build() + + val response = okHttpClient.newCall(request).execute().use { it.body?.string() ?: "" } val json = JSONObject(response) val data = json.getJSONArray("data") diff --git a/app/src/test/java/io/aatricks/novelscraper/data/repository/source/BaseJsoupSourceSecurityTest.kt b/app/src/test/java/io/aatricks/novelscraper/data/repository/source/BaseJsoupSourceSecurityTest.kt new file mode 100644 index 0000000..b8aa023 --- /dev/null +++ b/app/src/test/java/io/aatricks/novelscraper/data/repository/source/BaseJsoupSourceSecurityTest.kt @@ -0,0 +1,55 @@ +package io.aatricks.novelscraper.data.repository.source + +import io.aatricks.novelscraper.data.local.PreferencesManager +import io.aatricks.novelscraper.data.model.ExploreItem +import okhttp3.OkHttpClient +import org.junit.Assert.assertFalse +import org.junit.Assert.fail +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLSession + +class BaseJsoupSourceSecurityTest { + + class TestSource( + preferencesManager: PreferencesManager, + okHttpClient: OkHttpClient? + ) : BaseJsoupSource(preferencesManager, okHttpClient) { + override val name = "Test" + override val baseUrl = "https://example.com" + override suspend fun getPopularNovels(page: Int, tags: List) = emptyList() + override suspend fun searchNovels(query: String, page: Int) = emptyList() + override suspend fun getNovelDetails(url: String) = ExploreItem(title = "", url = "", source = "Test") + + fun testConnect(url: String) = connect(url) + // Helper to access protected getDocument + fun testGetDocument(url: String) = getDocument(url) + } + + @Test + fun `connect does NOT set global hostname verifier when ignoreSslErrors is true`() { + val originalVerifier = HttpsURLConnection.getDefaultHostnameVerifier() + try { + val prefs = mock(PreferencesManager::class.java) + `when`(prefs.ignoreSslErrors).thenReturn(true) + + val source = TestSource(prefs, null) + + // Trigger the code path + source.testConnect("https://example.com") + + val currentVerifier = HttpsURLConnection.getDefaultHostnameVerifier() + val mockSession = mock(SSLSession::class.java) + + // A secure verifier should reject this + val accepted = currentVerifier.verify("evil.com", mockSession) + + assertFalse("Global HostnameVerifier was modified to accept invalid hostnames!", accepted) + + } finally { + HttpsURLConnection.setDefaultHostnameVerifier(originalVerifier) + } + } +}