Skip to content

MASTG-DEMO-0158: Runtime Use of WebViewClient URL Loading Handlers with Frida

Download MASTG-DEMO-0158 APK Open MASTG-DEMO-0158 Folder Build MASTG-DEMO-0158 APK

Sample

This sample demonstrates how to dynamically analyze the runtime behavior of WebViewClient URL interception methods using Frida to understand how the app handles URL loading in WebViews.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package org.owasp.mastestapp

import android.content.Context
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient

// SUMMARY: This sample demonstrates a WebView with a custom WebViewClient that intercepts URL loading without proper validation.

class MastgTestWebView(private val context: Context) {

    fun mastgTest(webView: WebView): String {
        // Configure WebView settings
        webView.settings.apply {
            javaScriptEnabled = true
        }

        // FAIL: [MASTG-TEST-0398] Custom WebViewClient intercepts URL loading without proper validation
        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
                val url = request?.url?.toString()
                // No URL validation is performed - any URL will be loaded
                Log.d("MastgTest", "Loading URL: $url")
                return false // Allow the WebView to load the URL
            }

            override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): android.webkit.WebResourceResponse? {
                val url = request?.url?.toString()
                Log.d("MastgTest", "Intercepting request: $url")
                // No validation - allow all requests
                return super.shouldInterceptRequest(view, request)
            }
        }

        // Load a trusted page initially
        webView.loadUrl("https://mas.owasp.org/")

        return "WebView configured with custom URL handling"
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MASTestApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivityWebView"
            android:exported="true"
            android:windowSoftInputMode="adjustResize"
            android:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

The code configures a WebView with a custom WebViewClient that intercepts URL loading via shouldOverrideUrlLoading and shouldInterceptRequest methods. The implementation does not perform proper URL validation, potentially allowing navigation to untrusted content.

Steps

  1. Install the app on a device ( Installing Apps).
  2. Make sure you have Frida (Android) installed on your machine and the frida-server running on the device.
  3. Run run.sh to spawn the app with Frida.
  4. Optionally, interact with the app and tap links in the WebView to trigger further navigation.
  5. Stop the script by pressing Ctrl+C and/or q to quit the Frida CLI.
1
2
#!/bin/bash
frida -U -f org.owasp.mastestapp -l script.js -o output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
Java.perform(function() {
    console.log("[*] Hooking WebViewClient URL loading handlers...");

    var WebView = Java.use("android.webkit.WebView");
    var WebViewClient = Java.use("android.webkit.WebViewClient");
    var Uri = Java.use("android.net.Uri");

    // Per-thread bookkeeping so we can tell which URL-inspection calls happen
    // *while* one of the app's URL handlers is executing (handlers may run on
    // several threads concurrently).
    var handlerActive = {};   // threadId -> bool
    var inspectionLog = {};   // threadId -> array of method names

    function tid() { return Process.getCurrentThreadId(); }

    // A real allowlist/validation check reads the URL's host/scheme/path. We hook
    // those accessors and record any call made from inside a handler. If a handler
    // never calls them, it makes no host-based validation decision.
    ["getHost", "getScheme", "getPath"].forEach(function(name) {
        Uri[name].implementation = function() {
            var t = tid();
            if (handlerActive[t]) {
                inspectionLog[t].push(name);
            }
            return this[name]();
        };
    });

    function reportInspection(t) {
        var calls = inspectionLog[t] || [];
        if (calls.length === 0) {
            console.log("    URL inspection during handler: NONE (no host/scheme/path check -> no validation)");
        } else {
            console.log("    URL inspection during handler: " + calls.join(", "));
        }
    }

    // Reveal the WebViewClient the app registers and which handlers it overrides.
    WebView.setWebViewClient.implementation = function(client) {
        this.setWebViewClient(client);
        var cls = client.getClass();
        console.log("\n[*] setWebViewClient called");
        console.log("    WebViewClient implementation: " + cls.getName());
        var methods = cls.getDeclaredMethods();
        for (var i = 0; i < methods.length; i++) {
            var n = methods[i].getName();
            if (n === "shouldOverrideUrlLoading" || n === "shouldInterceptRequest") {
                console.log("    [!] Overrides " + n + " (custom URL handling)");
            }
        }
    };

    WebViewClient.shouldOverrideUrlLoading.overload('android.webkit.WebView', 'android.webkit.WebResourceRequest').implementation = function(view, request) {
        var t = tid();
        var url = request.getUrl().toString();
        handlerActive[t] = true;
        inspectionLog[t] = [];
        var result = this.shouldOverrideUrlLoading(view, request);
        handlerActive[t] = false;
        console.log("\n[shouldOverrideUrlLoading] URL: " + url);
        reportInspection(t);
        console.log("    -> returned " + result + " (false = URL loaded without validation)");
        return result;
    };

    WebViewClient.shouldInterceptRequest.overload('android.webkit.WebView', 'android.webkit.WebResourceRequest').implementation = function(view, request) {
        var t = tid();
        var url = request.getUrl().toString();
        handlerActive[t] = true;
        inspectionLog[t] = [];
        var result = this.shouldInterceptRequest(view, request);
        handlerActive[t] = false;
        console.log("\n[shouldInterceptRequest] URL: " + url);
        reportInspection(t);
        console.log("    -> " + (result != null ? "custom response returned" : "default loading (no validation)"));
        return result;
    };

    console.log("[*] WebViewClient hooks installed successfully");
});

The Frida script does three things:

  • It hooks setWebViewClient to reveal, at launch, the WebViewClient implementation the app registers and which URL loading handlers it overrides. This requires no navigation.
  • It hooks the shouldOverrideUrlLoading and shouldInterceptRequest methods to log each URL they receive at runtime and the value they return.
  • It hooks the URL inspection accessors Uri.getHost, Uri.getScheme, and Uri.getPath and records any call made while a handler is executing. A genuine allowlist check must read the host (and usually the scheme) to decide whether a URL is trusted, so if a handler runs without ever calling these, it performs no host-based validation.

Observation

The output shows the custom WebViewClient registered by the app, the URL loading handlers it overrides, the URLs intercepted at runtime along with their return values, and whether each handler inspected the URL host, scheme, or path.

output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[*] Hooking WebViewClient URL loading handlers...
[*] WebViewClient hooks installed successfully

[*] setWebViewClient called
    WebViewClient implementation: org.owasp.mastestapp.MastgTestWebView$mastgTest$2
    [!] Overrides shouldInterceptRequest (custom URL handling)
    [!] Overrides shouldOverrideUrlLoading (custom URL handling)

[shouldInterceptRequest] URL: https://mas.owasp.org/
    URL inspection during handler: NONE (no host/scheme/path check -> no validation)
    -> default loading (no validation)

[*] setWebViewClient called
    WebViewClient implementation: org.owasp.mastestapp.MastgTestWebView$mastgTest$2
    [!] Overrides shouldInterceptRequest (custom URL handling)
    [!] Overrides shouldOverrideUrlLoading (custom URL handling)

[shouldInterceptRequest] URL: https://mas.owasp.org/
    URL inspection during handler: NONE (no host/scheme/path check -> no validation)
    -> default loading (no validation)

Evaluation

The test case fails because the app registers a custom WebViewClient (MastgTestWebView$mastgTest$2) that overrides shouldInterceptRequest and shouldOverrideUrlLoading, but neither handler validates the requested URL:

  • Every intercepted request falls through to the default loading behavior, including requests to third-party hosts such as fonts.googleapis.com and cdn.datatables.net.
  • For every handler invocation, the URL inspection during handler line reports NONE: the handler returned without ever reading the URL's host, scheme, or path. Since an allowlist check must inspect at least the host to decide whether a URL is trusted, this shows the handlers make no host-based validation decision rather than merely failing to block the specific URLs observed.
  • When shouldOverrideUrlLoading is exercised (by tapping a link), it returns false, so every URL the user is directed to is loaded.

Because the handlers perform no host/scheme/path inspection, the WebView loads content from any host, which could allow navigation to untrusted content or open redirect vulnerabilities.

Note: this technique observes host-, scheme-, and path-based validation, which covers the standard allowlist pattern. It would not detect validation performed purely by matching the raw URL string (for example with String.contains). Combine it with the static analysis in Uses of WebViewClient URL Loading Handlers with semgrep for full certainty.