Improve WebView spoofing (#3678)

- Spoof `Sec-CH-UA` client hints to match the user agent
- Fix `X-Requested-With` spoofing leaking to unrelated callers
- Update default user agent to Chrome 149
This commit is contained in:
AntsyLich
2026-08-02 20:05:21 +06:00
committed by GitHub
parent d7f3ceef5c
commit 55be95dd5d
11 changed files with 71 additions and 7 deletions
+1
View File
@@ -41,6 +41,7 @@ dependencies {
api(libs.kotlinx.serialization.jsonOkio)
api(libs.androidx.preference)
implementation(libs.androidx.webkit)
implementation(libs.jsoup)
@@ -14,6 +14,6 @@ class NetworkPreferences(
val defaultUserAgent: Preference<String> = 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",
)
}
@@ -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())
}
}
}
@@ -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) {