Harden WebView nav + add test suite + fix lint for production
Build APK / build (push) Successful in 1m29s
Build APK / build (push) Successful in 1m29s
Security: - LoginActivity WebView now enforces a host allow-list in shouldOverrideUrlLoading: only claude.ai + required SSO/CDN hosts (Google, Apple, Cloudflare, gstatic, recaptcha) can navigate; everything else is blocked. market://intent:// still blocked; about:/data: allowed. Device-verified: claude.ai login + Cloudflare challenge still load. Tests (33, pure-JVM JUnit4, no device needed): - Extracted shouldRecordHistory() pure throttle decision (regression guard for the empty-history-chart bug) + HistoryThrottleTest. - UsageDataTest (mergedWith last-good/partial-union, computed props), PaceCalcTest, PeakHoursTest. - Added junit:junit:4.13.2 as testImplementation only. Build quality: - widget_layout.xml: suppress false-positive UseAppTint lint on the widget refresh button (app:tint doesn't work in RemoteViews; android:tint is correct here) so lintDebug is clean. Verified locally: 33 unit tests pass, lintDebug 0 errors, signed assembleRelease OK (apksigner verified, signer identity unchanged), emulator smoke test launches + renders without crash.
This commit is contained in:
@@ -85,9 +85,19 @@ class LoginActivity : AppCompatActivity() {
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
val url = request.url.toString()
|
||||
val uri = request.url
|
||||
val url = uri.toString()
|
||||
// Keep blocking app-redirect schemes outright.
|
||||
if (url.startsWith("market://") || url.startsWith("intent://")) return true
|
||||
return false
|
||||
val scheme = uri.scheme?.lowercase()
|
||||
// about:blank and data: URIs are used by the login page itself (e.g. blank
|
||||
// bootstrap frames, inline images). They have no host to allow-list, so let them load.
|
||||
if (scheme == "about" || scheme == "data") return false
|
||||
// Host ALLOW-LIST: only navigate to claude.ai and its SSO/CDN providers.
|
||||
// OAuth runs as full-page redirects in THIS WebView (multiple windows disabled,
|
||||
// JS-opened windows disabled), so the list MUST include the Google/Apple SSO
|
||||
// and Cloudflare challenge hosts or sign-in breaks. Anything else is blocked.
|
||||
return !isHostAllowed(uri.host)
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: android.graphics.Bitmap?) {
|
||||
@@ -169,4 +179,35 @@ class LoginActivity : AppCompatActivity() {
|
||||
binding.webView.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* True if [host] belongs to an allowed domain (exact match or a subdomain via dot-suffix).
|
||||
* A null/blank host is never allowed. accounts.google.com, challenges.cloudflare.com, etc. are
|
||||
* covered by their parent-domain suffixes (google.com, cloudflare.com).
|
||||
*/
|
||||
private fun isHostAllowed(host: String?): Boolean {
|
||||
if (host.isNullOrBlank()) return false
|
||||
val h = host.lowercase()
|
||||
return ALLOWED_DOMAINS.any { d -> h == d || h.endsWith(".$d") }
|
||||
}
|
||||
|
||||
companion object {
|
||||
// WebView host allow-list. Login navigation is restricted to claude.ai and the SSO/CDN
|
||||
// hosts it redirects through. This MUST include the SSO providers (Google, Apple) and
|
||||
// Cloudflare challenge hosts — OAuth happens as same-WebView full-page redirects, so
|
||||
// dropping any of these breaks sign-in. Subdomains are matched by dot-suffix.
|
||||
private val ALLOWED_DOMAINS = listOf(
|
||||
"claude.ai",
|
||||
"anthropic.com",
|
||||
"google.com", // covers accounts.google.com
|
||||
"gstatic.com",
|
||||
"googleusercontent.com",
|
||||
"googleapis.com",
|
||||
"apple.com",
|
||||
"icloud.com",
|
||||
"cloudflare.com", // covers challenges.cloudflare.com
|
||||
"cloudflareinsights.com",
|
||||
"recaptcha.net"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user