Android WebView — source audit
TL;DR: WebView is a small browser shipped inside your app. It runs HTML/JS with the app’s privileges. Source-audit risks:
addJavascriptInterfaceexposing app objects to JS,setAllowFileAccess*allowingfile://to read app data, mixed-content / cleartext upgrades, custom URL handlers that pass URLs to other components without validation, andshouldOverrideUrlLoadingthat opens arbitrary schemes.
The dangerous WebSettings
1
2
3
4
5
grep -rnE 'setJavaScriptEnabled\(true\)' src/
grep -rnE 'addJavascriptInterface' src/
grep -rnE 'setAllowFileAccess\(true\)|setAllowFileAccessFromFileURLs\(true\)|setAllowUniversalAccessFromFileURLs\(true\)' src/
grep -rnE 'setAllowContentAccess\(true\)|setMixedContentMode\(MIXED_CONTENT_ALWAYS_ALLOW\)' src/
grep -rnE 'setWebContentsDebuggingEnabled\(true\)' src/
| Setting | Why bad |
|---|---|
setJavaScriptEnabled(true) + remote URL | XSS becomes app-context code |
addJavascriptInterface(obj, "name") | JS can call methods on obj — see below |
setAllowFileAccessFromFileURLs(true) | file://x.html reads any other file:// — data exfil |
setAllowUniversalAccessFromFileURLs(true) | file:// reads HTTP — even worse |
setAllowContentAccess(true) | WebView can fetch from content:// providers |
setMixedContentMode(MIXED_CONTENT_ALWAYS_ALLOW) | HTTP injected into HTTPS page |
setWebContentsDebuggingEnabled(true) in release | chrome://inspect attaches to any device |
addJavascriptInterface — the classic bug
1
webView.addJavascriptInterface(new AppBridge(), "Android");
In the loaded page:
1
Android.openExternalUrl(maliciousUrl);
If AppBridge exposes any method that touches sensitive state (start activities, read files, hit private APIs), JS in the WebView reaches them.
Pre-API-17 the implementation also exposed Object#getClass() reflectively → arbitrary code execution via reflection. Modern Android requires @JavascriptInterface annotation, which limits to annotated methods only. But the bridge still has whatever powers you annotate.
Source audit:
1
2
grep -rn 'addJavascriptInterface' src/ -B2 -A5
grep -rn '@JavascriptInterface' src/
For each annotated method: trace what app data or APIs it reaches.
loadUrl with attacker-controlled URL
1
2
val url = intent.getStringExtra("url") ?: return
webView.loadUrl(url) // BAD: attacker-controlled URL, any scheme
Mitigations:
- Validate the URL is HTTPS and host-matches an allowlist.
- Reject
file://,content://,javascript:,data:. - Reject URLs with
..segments after canonicalisation.
shouldOverrideUrlLoading
This callback decides whether the WebView handles a URL or hands it to the OS. A common bug:
1
2
3
4
5
6
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url
val intent = Intent(Intent.ACTION_VIEW, url)
startActivity(intent) // BAD: any scheme reachable, including intent://
return true
}
Custom schemes like intent://#Intent;..., market://, whatsapp://, and javascript: can be turned into surprising side effects.
file://, content://, and the SOP
WebView treats every file:// URL as a unique origin (so JS can’t read other files) — but only if setAllowFileAccessFromFileURLs is false. Setting it true collapses file:// origins together. This is the canonical Android “local file read from a WebView” bug pattern.
1
grep -rn 'loadUrl\("file://\|loadUrl\(".*file://' src/
Token leakage via WebView
A common SSO pattern: load the OAuth provider in a WebView, capture the redirect URL after auth. Bugs:
- Reading the access-token from the URL fragment via
evaluateJavascript("window.location.hash")exposes the token to anything injected into that page. - Persisting cookies cross-app if
CookieManageris shared.
1
2
grep -rn 'CookieManager\.getInstance\|setAcceptCookie\|setAcceptThirdPartyCookies' src/
grep -rn 'evaluateJavascript' src/
TLS errors handled silently
1
2
3
4
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed(); // BAD: accept any cert
}
A handler.proceed() in onReceivedSslError makes the WebView blind to MITM. Sometimes added “for testing” and shipped.
1
grep -rn 'onReceivedSslError\|handler\.proceed' src/
URL allowlist patterns
1
2
3
4
5
6
7
8
9
private val allowedHosts = setOf("app.example.com", "auth.example.com")
override fun shouldOverrideUrlLoading(view: WebView, req: WebResourceRequest): Boolean {
val host = req.url.host ?: return true
if (host !in allowedHosts) {
Log.w("WV", "blocked $host"); return true
}
return false // let WebView load it
}
Audit-time: does the allowlist include subdomain wildcards? Are there subdomains under your control that host user content (jsfiddle-style)?
AndroidX WebViewAssetLoader
The modern recommendation. Loads local assets via an internal https://appassets.androidplatform.net/ origin, isolating from file:// issues.
If you see this, the app is doing the right thing. If you don’t, and setAllowFileAccess(true) is set, flag for review.
Source-audit checklist
- No
setAllowFileAccessFromFileURLs(true)/setAllowUniversalAccessFromFileURLs(true). - No
setMixedContentMode(MIXED_CONTENT_ALWAYS_ALLOW)in production builds. - Every
addJavascriptInterfacebridge has a documented, minimal API surface. shouldOverrideUrlLoadingrejects non-HTTPS or applies a host allowlist.- No
handler.proceed()inonReceivedSslError. setWebContentsDebuggingEnabled(true)is wrapped inBuildConfig.DEBUG.