diff --git a/CHANGELOG.md b/CHANGELOG.md index 790ad124d..4ef23ed17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,15 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co ### Added - Add Category filtering for the Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#3589](https://github.com/mihonapp/mihon/pull/3589)) +### Changed +- Update default user agent to Chrome 149 ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678)) + +### Improved +- Spoof `Sec-CH-UA` client hints in WebView to match the user agent ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678)) + ### Fixed - Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3676](https://github.com/mihonapp/mihon/pull/3676)) +- Fixed `X-Requested-With` spoofing leaking to unrelated callers ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678)) ## [v0.20.2] - 2026-08-01 ### Added diff --git a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt index 5254cde3d..7f34a8a05 100644 --- a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt +++ b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt @@ -51,6 +51,7 @@ import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.getHtml import eu.kanade.tachiyomi.util.system.setDefaultSettings +import eu.kanade.tachiyomi.util.system.setUserAgent import kotlinx.coroutines.launch import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold @@ -72,6 +73,7 @@ fun WebViewScreenContent( onNavigateUp: () -> Unit, initialTitle: String?, url: String, + defaultUserAgentProvider: () -> String, onShare: (String) -> Unit, onOpenInBrowser: (String) -> Unit, onClearCookies: (String) -> Unit, @@ -340,9 +342,7 @@ fun WebViewScreenContent( WebView.setWebContentsDebuggingEnabled(true) } - headers["user-agent"]?.let { - webView.settings.userAgentString = it - } + webView.setUserAgent(headers["user-agent"] ?: defaultUserAgentProvider()) }, onDispose = { webView -> val window = windowStack.items.find { it.webView == webView } diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 94539f49c..d76f57915 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -8,7 +8,6 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build -import android.os.Looper import android.webkit.WebView import androidx.core.content.ContextCompat import androidx.lifecycle.DefaultLifecycleObserver @@ -231,7 +230,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor override fun getPackageName(): String { try { // Override the value passed as X-Requested-With in WebView requests - val stackTrace = Looper.getMainLooper().thread.stackTrace + val stackTrace = Thread.currentThread().stackTrace val isChromiumCall = stackTrace.any { trace -> trace.className.lowercase() in setOf("org.chromium.base.buildinfo", "org.chromium.base.apkinfo") && trace.methodName.lowercase() in setOf("getall", "getpackagename", "") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index 67a7e9b4f..8506fa8fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -72,6 +72,7 @@ class WebViewActivity : BaseActivity() { initialTitle = intent.extras?.getString(TITLE_KEY), url = url, headers = headers, + defaultUserAgentProvider = network::defaultUserAgentProvider, onUrlChange = { assistUrl = it }, onShare = this::shareWebpage, onOpenInBrowser = this::openInBrowser, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt index 0ee47f234..385ecd95b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt @@ -36,6 +36,7 @@ class WebViewScreen( initialTitle = initialTitle, url = url, headers = viewModel.headers, + defaultUserAgentProvider = viewModel::defaultUserAgentProvider, onUrlChange = { assistUrl = it }, onShare = { viewModel.shareWebpage(context, it) }, onOpenInBrowser = { viewModel.openInBrowser(context, it) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt index d0fe87c6e..f9c00b213 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt @@ -67,4 +67,6 @@ class WebViewViewModel( logcat { "Cleared $cleared cookies for: $url" } } } + + fun defaultUserAgentProvider() = network.defaultUserAgentProvider() } diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index fd89abece..e5b8eb7e0 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { api(libs.kotlinx.serialization.jsonOkio) api(libs.androidx.preference) + implementation(libs.androidx.webkit) implementation(libs.jsoup) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt index 8b853daf8..5df70e46d 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt @@ -14,6 +14,6 @@ class NetworkPreferences( val defaultUserAgent: Preference = preferenceStore.getString( "default_user_agent", - "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Mobile Safari/537.36", ) } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt index 1223d51a4..547459d72 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt @@ -8,6 +8,7 @@ import android.widget.Toast import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.setDefaultSettings +import eu.kanade.tachiyomi.util.system.setUserAgent import eu.kanade.tachiyomi.util.system.toast import okhttp3.Headers import okhttp3.Interceptor @@ -84,7 +85,7 @@ abstract class WebViewInterceptor( return WebView(context).apply { setDefaultSettings() // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty - settings.userAgentString = request.header("User-Agent") ?: defaultUserAgentProvider() + setUserAgent(request.header("User-Agent") ?: defaultUserAgentProvider()) } } } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt index 39bebcbae..00fed0391 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt @@ -6,6 +6,9 @@ import android.content.pm.PackageManager import android.webkit.CookieManager import android.webkit.WebSettings import android.webkit.WebView +import androidx.webkit.UserAgentMetadata +import androidx.webkit.WebSettingsCompat +import androidx.webkit.WebViewFeature import kotlinx.coroutines.suspendCancellableCoroutine import logcat.LogPriority import tachiyomi.core.common.util.system.logcat @@ -99,6 +102,53 @@ fun WebView.setDefaultSettings() { CookieManager.getInstance().acceptThirdPartyCookies(this) } +/** + * Sets the user agent along with the matching user agent metadata, which Chromium uses to populate + * the `Sec-CH-UA` client hints. Without this the hints keep advertising the real WebView brand and + * version, contradicting the spoofed user agent. + */ +fun WebView.setUserAgent(userAgent: String) { + settings.userAgentString = userAgent + + if (!WebViewFeature.isFeatureSupported(WebViewFeature.USER_AGENT_METADATA)) return + + val versionMatch = CHROME_VERSION_REGEX.find(userAgent) ?: return + val majorVersion = versionMatch.groupValues[1] + val fullVersion = majorVersion + versionMatch.groupValues[2].ifEmpty { ".0.0.0" } + + try { + val metadata = WebSettingsCompat.getUserAgentMetadata(settings) + val brandVersionList = metadata.brandVersionList.map { brandVersion -> + val brand = when (brandVersion.brand) { + WEBVIEW_BRAND -> CHROME_BRAND + CHROMIUM_BRAND -> CHROMIUM_BRAND + else -> return@map brandVersion + } + + UserAgentMetadata.BrandVersion.Builder() + .setBrand(brand) + .setMajorVersion(majorVersion) + .setFullVersion(fullVersion) + .build() + } + + WebSettingsCompat.setUserAgentMetadata( + settings, + UserAgentMetadata.Builder(metadata) + .setBrandVersionList(brandVersionList) + .setFullVersion(fullVersion) + .build(), + ) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Failed to set user agent metadata" } + } +} + +private const val WEBVIEW_BRAND = "Android WebView" +private const val CHROMIUM_BRAND = "Chromium" +private const val CHROME_BRAND = "Google Chrome" +private val CHROME_VERSION_REGEX = """Chrome/(\d+)(\.[\d.]+)?""".toRegex() + private fun WebView.getWebViewMajorVersion(): Int { val uaRegexMatch = """.*Chrome/(\d+)\..*""".toRegex().matchEntire(getDefaultUserAgentString()) return if (uaRegexMatch != null && uaRegexMatch.groupValues.size > 1) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77cb7882c..5268d2d9c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ androidx-test-espresso = "3.7.0" androidx-test-junit = "1.3.0" androidx-test-uiautomator = "2.4.0" androidx-viewPager = "1.1.0" +androidx-webkit = "1.16.0" androidx-work = "2.11.2" archive = "1.1.6" coil = "3.5.0" @@ -113,6 +114,7 @@ androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" } androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-test-uiautomator" } androidx-viewPager = { module = "androidx.viewpager:viewpager", version.ref = "androidx-viewPager" } +androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "androidx-webkit" } androidx-work = { module = "androidx.work:work-runtime", version.ref = "androidx-work" } archive = { module = "me.zhanghai.android.libarchive:library", version.ref = "archive" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }