Skip to content

MASTG-DEMO-0097: Sensitive Data and Functionality Exposed Through WebView JavaScript Bridges

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

Sample

The following demo demonstrates a WebView component rendering a "Contact Support" form. A native bridge class, SupportBridge, is registered via addJavascriptInterface() and exposes three methods to JavaScript: one that reads the user's name, email, and JWT from SharedPreferences and returns them as JSON, one that submits a support message, and one that writes a preference value back to SharedPreferences. JavaScript is explicitly enabled on the WebView, which means any JavaScript running in the page (including injected scripts) can call these bridge methods directly.

  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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package org.owasp.mastestapp

import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import org.json.JSONObject
import androidx.core.content.edit

class MastgTestWebView(private val context: Context) {

    private val prefs: SharedPreferences =
        context.getSharedPreferences("support_demo_prefs", Context.MODE_PRIVATE)

    // Intentionally insecure demo bridge, do not use in production.
    inner class SupportBridge {

        @JavascriptInterface
        fun getUserProfileJson(): String {
            val name = prefs.getString("name", "") ?: ""
            val email = prefs.getString("email", "") ?: ""
            val jwt = prefs.getString("jwt", "") ?: ""

            return JSONObject().apply {
                put("name", name)
                put("email", email)
                put("jwt", jwt)
            }.toString()
        }

        @JavascriptInterface
        fun submitSupportMessage(email: String, message: String): String {
            return JSONObject().apply {
                put("status", "ok")
                put("email", email)
                put("message", message)
            }.toString()
        }

        @JavascriptInterface
        fun updateSupportPreference(value: String) {
            prefs.edit { putString("support_preference", value) }
        }
    }

    @SuppressLint("SetJavaScriptEnabled")
    fun mastgTest(webView: WebView) {
        seedDemoSessionData()

        val demoHtml = """
            <!doctype html>
            <html>
            <head>
                <meta charset="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <title>Contact Support</title>
                <style>
                    body {
                        font-family: sans-serif;
                        margin: 0;
                        padding: 16px;
                        background: #ff444444;
                    }
                    .card {
                        background: white;
                        border: 1px solid #d9d9d9;
                        border-radius: 10px;
                        padding: 16px;
                        max-width: 500px;
                        margin: 0 auto;
                    }
                    h1 {
                        margin-top: 0;
                        font-size: 22px;
                    }
                    label {
                        display: block;
                        margin-top: 12px;
                        margin-bottom: 6px;
                        font-weight: 600;
                    }
                    input, textarea {
                        width: 100%;
                        box-sizing: border-box;
                        padding: 10px;
                        border: 1px solid #bbb;
                        border-radius: 8px;
                        font-size: 14px;
                    }
                    textarea {
                        min-height: 160px;
                        resize: vertical;
                    }
                    .row {
                        display: flex;
                        gap: 8px;
                        margin-top: 12px;
                    }
                    button {
                        padding: 10px 14px;
                        border: 1px solid #999;
                        border-radius: 8px;
                        background: #fff;
                        cursor: pointer;
                    }
                    .hint {
                        color: #666;
                        font-size: 13px;
                        margin-top: 8px;
                    }
                    .hidden {
                        display: none;
                    }
                    .messageBox {
                        width: 100%;
                        box-sizing: border-box;
                        min-height: 180px;
                        padding: 10px;
                        border: 1px solid #bbb;
                        border-radius: 8px;
                        font-size: 14px;
                        background: white;
                        white-space: pre-wrap;
                        word-break: break-word;
                    }
                </style>
            </head>
            <body>
                <div class="card">
                    <div id="formView">
                        <h1>Contact Support</h1>
                        <p class="hint">Send a message to the support team.</p>

                        <label for="email">Email</label>
                        <input id="email" type="text" value="[email protected]" />

                        <label for="message">Message</label>
                        <textarea id="message" placeholder="Describe your issue here"></textarea>

                        <div class="row">
                            <button onclick="sendMessage()">Send</button>
                        </div>
                    </div>

                    <div id="resultView" class="hidden">
                        <h1>Support</h1>
                        <div id="resultText" class="messageBox"></div>
                        <div class="row">
                            <button onclick="showForm()">Send another message</button>
                        </div>
                    </div>
                </div>

                <script>
                    function showForm() {
                        document.getElementById("resultView").classList.add("hidden");
                        document.getElementById("formView").classList.remove("hidden");
                        document.getElementById("email").value = "[email protected]";
                        document.getElementById("message").value = "";
                        document.getElementById("resultText").innerHTML = "";
                    }

                    function sendMessage() {
                        const email = document.getElementById("email").value;
                        const message = document.getElementById("message").value;

                        MASBridge.submitSupportMessage(email, message);

                        document.getElementById("formView").classList.add("hidden");
                        document.getElementById("resultView").classList.remove("hidden");

                        // Intentionally insecure.
                        document.getElementById("resultText").innerHTML =
                            "Message sent, you'll soon hear from us in " + email;
                    }
                </script>
            </body>
            </html>
        """.trimIndent()

        webView.apply {
            settings.javaScriptEnabled = true
            addJavascriptInterface(SupportBridge(), "MASBridge")
            webViewClient = object : WebViewClient() {}
            loadDataWithBaseURL(
                "https://appassets.androidplatform.net/assets/support/",
                demoHtml,
                "text/html",
                "utf-8",
                null
            )
        }
    }

    private fun seedDemoSessionData() {
        prefs.edit {
            putString("name", "John Doe")
                .putString("email", "[email protected]")
                .putString(
                    "jwt",
                    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzE2MjM5MDIyLCJleHAiOjE5OTk5OTk5OTksImF1ZCI6Im1hc3Rlc3RhcHAiLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20ifQ.s5vXz8Q1w4m0l9B2x3G7g8J9k1N2p3Q4r5S6t7U8v9w"
                )
        }
    }
}
 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
package org.owasp.mastestapp;

import android.content.Context;
import android.content.SharedPreferences;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.autofill.HintConstants;
import androidx.core.app.NotificationCompat;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.json.JSONException;
import org.json.JSONObject;

/* compiled from: MastgTestWebView.kt */
@Metadata(d1 = {"\u0000&\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\b\u0007\u0018\u00002\u00020\u0001:\u0001\rB\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0010\u0010\b\u001a\u00020\t2\u0006\u0010\n\u001a\u00020\u000bH\u0007J\b\u0010\f\u001a\u00020\tH\u0002R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000R\u000e\u0010\u0006\u001a\u00020\u0007X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u000e"}, d2 = {"Lorg/owasp/mastestapp/MastgTestWebView;", "", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "prefs", "Landroid/content/SharedPreferences;", "mastgTest", "", "webView", "Landroid/webkit/WebView;", "seedDemoSessionData", "SupportBridge", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
/* loaded from: classes3.dex */
public final class MastgTestWebView {
    public static final int $stable = 8;
    private final Context context;
    private final SharedPreferences prefs;

    public MastgTestWebView(Context context) {
        Intrinsics.checkNotNullParameter(context, "context");
        this.context = context;
        SharedPreferences sharedPreferences = this.context.getSharedPreferences("support_demo_prefs", 0);
        Intrinsics.checkNotNullExpressionValue(sharedPreferences, "getSharedPreferences(...)");
        this.prefs = sharedPreferences;
    }

    /* compiled from: MastgTestWebView.kt */
    @Metadata(d1 = {"\u0000\u001c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0002\b\u0004\n\u0002\u0010\u0002\n\u0002\b\u0002\b\u0086\u0004\u0018\u00002\u00020\u0001B\u0007¢\u0006\u0004\b\u0002\u0010\u0003J\b\u0010\u0004\u001a\u00020\u0005H\u0007J\u0018\u0010\u0006\u001a\u00020\u00052\u0006\u0010\u0007\u001a\u00020\u00052\u0006\u0010\b\u001a\u00020\u0005H\u0007J\u0010\u0010\t\u001a\u00020\n2\u0006\u0010\u000b\u001a\u00020\u0005H\u0007¨\u0006\f"}, d2 = {"Lorg/owasp/mastestapp/MastgTestWebView$SupportBridge;", "", "<init>", "(Lorg/owasp/mastestapp/MastgTestWebView;)V", "getUserProfileJson", "", "submitSupportMessage", NotificationCompat.CATEGORY_EMAIL, "message", "updateSupportPreference", "", "value", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
    public final class SupportBridge {
        public SupportBridge() {
        }

        @JavascriptInterface
        public final String getUserProfileJson() throws JSONException {
            String name = MastgTestWebView.this.prefs.getString(HintConstants.AUTOFILL_HINT_NAME, "");
            if (name == null) {
                name = "";
            }
            String email = MastgTestWebView.this.prefs.getString(NotificationCompat.CATEGORY_EMAIL, "");
            if (email == null) {
                email = "";
            }
            String string = MastgTestWebView.this.prefs.getString("jwt", "");
            String jwt = string != null ? string : "";
            JSONObject $this$getUserProfileJson_u24lambda_u240 = new JSONObject();
            $this$getUserProfileJson_u24lambda_u240.put(HintConstants.AUTOFILL_HINT_NAME, name);
            $this$getUserProfileJson_u24lambda_u240.put(NotificationCompat.CATEGORY_EMAIL, email);
            $this$getUserProfileJson_u24lambda_u240.put("jwt", jwt);
            String string2 = $this$getUserProfileJson_u24lambda_u240.toString();
            Intrinsics.checkNotNullExpressionValue(string2, "toString(...)");
            return string2;
        }

        @JavascriptInterface
        public final String submitSupportMessage(String email, String message) throws JSONException {
            Intrinsics.checkNotNullParameter(email, "email");
            Intrinsics.checkNotNullParameter(message, "message");
            JSONObject $this$submitSupportMessage_u24lambda_u241 = new JSONObject();
            $this$submitSupportMessage_u24lambda_u241.put(NotificationCompat.CATEGORY_STATUS, "ok");
            $this$submitSupportMessage_u24lambda_u241.put(NotificationCompat.CATEGORY_EMAIL, email);
            $this$submitSupportMessage_u24lambda_u241.put("message", message);
            String string = $this$submitSupportMessage_u24lambda_u241.toString();
            Intrinsics.checkNotNullExpressionValue(string, "toString(...)");
            return string;
        }

        @JavascriptInterface
        public final void updateSupportPreference(String value) {
            Intrinsics.checkNotNullParameter(value, "value");
            SharedPreferences $this$edit_u24default$iv = MastgTestWebView.this.prefs;
            SharedPreferences.Editor editor$iv = $this$edit_u24default$iv.edit();
            editor$iv.putString("support_preference", value);
            editor$iv.apply();
        }
    }

    public final void mastgTest(WebView webView) {
        Intrinsics.checkNotNullParameter(webView, "webView");
        seedDemoSessionData();
        webView.getSettings().setJavaScriptEnabled(true);
        webView.addJavascriptInterface(new SupportBridge(), "MASBridge");
        webView.setWebViewClient(new WebViewClient() { // from class: org.owasp.mastestapp.MastgTestWebView$mastgTest$1$1
        });
        webView.loadDataWithBaseURL("https://appassets.androidplatform.net/assets/support/", "<!doctype html>\n<html>\n<head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <title>Contact Support</title>\n    <style>\n        body {\n            font-family: sans-serif;\n            margin: 0;\n            padding: 16px;\n            background: #ff444444;\n        }\n        .card {\n            background: white;\n            border: 1px solid #d9d9d9;\n            border-radius: 10px;\n            padding: 16px;\n            max-width: 500px;\n            margin: 0 auto;\n        }\n        h1 {\n            margin-top: 0;\n            font-size: 22px;\n        }\n        label {\n            display: block;\n            margin-top: 12px;\n            margin-bottom: 6px;\n            font-weight: 600;\n        }\n        input, textarea {\n            width: 100%;\n            box-sizing: border-box;\n            padding: 10px;\n            border: 1px solid #bbb;\n            border-radius: 8px;\n            font-size: 14px;\n        }\n        textarea {\n            min-height: 160px;\n            resize: vertical;\n        }\n        .row {\n            display: flex;\n            gap: 8px;\n            margin-top: 12px;\n        }\n        button {\n            padding: 10px 14px;\n            border: 1px solid #999;\n            border-radius: 8px;\n            background: #fff;\n            cursor: pointer;\n        }\n        .hint {\n            color: #666;\n            font-size: 13px;\n            margin-top: 8px;\n        }\n        .hidden {\n            display: none;\n        }\n        .messageBox {\n            width: 100%;\n            box-sizing: border-box;\n            min-height: 180px;\n            padding: 10px;\n            border: 1px solid #bbb;\n            border-radius: 8px;\n            font-size: 14px;\n            background: white;\n            white-space: pre-wrap;\n            word-break: break-word;\n        }\n    </style>\n</head>\n<body>\n    <div class=\"card\">\n        <div id=\"formView\">\n            <h1>Contact Support</h1>\n            <p class=\"hint\">Send a message to the support team.</p>\n\n            <label for=\"email\">Email</label>\n            <input id=\"email\" type=\"text\" value=\"[email protected]\" />\n\n            <label for=\"message\">Message</label>\n            <textarea id=\"message\" placeholder=\"Describe your issue here\"></textarea>\n\n            <div class=\"row\">\n                <button onclick=\"sendMessage()\">Send</button>\n            </div>\n        </div>\n\n        <div id=\"resultView\" class=\"hidden\">\n            <h1>Support</h1>\n            <div id=\"resultText\" class=\"messageBox\"></div>\n            <div class=\"row\">\n                <button onclick=\"showForm()\">Send another message</button>\n            </div>\n        </div>\n    </div>\n\n    <script>\n        function showForm() {\n            document.getElementById(\"resultView\").classList.add(\"hidden\");\n            document.getElementById(\"formView\").classList.remove(\"hidden\");\n            document.getElementById(\"email\").value = \"[email protected]\";\n            document.getElementById(\"message\").value = \"\";\n            document.getElementById(\"resultText\").innerHTML = \"\";\n        }\n\n        function sendMessage() {\n            const email = document.getElementById(\"email\").value;\n            const message = document.getElementById(\"message\").value;\n\n            MASBridge.submitSupportMessage(email, message);\n\n            document.getElementById(\"formView\").classList.add(\"hidden\");\n            document.getElementById(\"resultView\").classList.remove(\"hidden\");\n\n            // Intentionally insecure.\n            document.getElementById(\"resultText\").innerHTML =\n                \"Message sent, you'll soon hear from us in \" + email;\n        }\n    </script>\n</body>\n</html>", "text/html", "utf-8", null);
    }

    private final void seedDemoSessionData() {
        SharedPreferences $this$edit_u24default$iv = this.prefs;
        SharedPreferences.Editor editor$iv = $this$edit_u24default$iv.edit();
        editor$iv.putString(HintConstants.AUTOFILL_HINT_NAME, "John Doe").putString(NotificationCompat.CATEGORY_EMAIL, "[email protected]").putString("jwt", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzE2MjM5MDIyLCJleHAiOjE5OTk5OTk5OTksImF1ZCI6Im1hc3Rlc3RhcHAiLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20ifQ.s5vXz8Q1w4m0l9B2x3G7g8J9k1N2p3Q4r5S6t7U8v9w");
        editor$iv.apply();
    }
}

Some payloads that could be executed by an attacker who can run JavaScript in this page include:

  • <img src=x onerror='document.getElementById("resultText").innerText = MASBridge.getUserProfileJson()'>
  • <img src=x onerror='MASBridge.updateSupportPreference("malicious value")'>

Steps

Let's run our semgrep rule against the reversed Java code.

../../../../rules/mastg-android-webview-bridges.yml
 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
rules:
  - id: mastg-android-webview-bridges-setup
    severity: WARNING
    languages: [java]
    metadata:
      summary: Detects a WebView with JavaScript enabled that also registers a JavaScript native bridge via addJavascriptInterface().
    message: '[MASVS-PLATFORM-2] WebView has JavaScript enabled and a native bridge is registered via addJavascriptInterface()'
    patterns:
      - pattern: $WEBVIEW.addJavascriptInterface($BRIDGE, $_)
      - pattern-inside: |
          $RET $METHOD(...) {
            ...
            $WEBVIEW.getSettings().setJavaScriptEnabled(true);
            ...
          }

  - id: mastg-android-webview-bridges-javascriptinterface
    severity: WARNING
    languages: [java]
    metadata:
      summary: Detects methods annotated with @JavascriptInterface, which are exposed to JavaScript in a WebView bridge.
    message: '[MASVS-PLATFORM-2] Method is exposed to JavaScript via @JavascriptInterface'
    pattern: |
      @JavascriptInterface
      $TYPE $NAME(...) {
        ...
      }
run.sh
1
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-webview-bridges.yml ./MastgTestWebView_reversed.java --max-lines-per-finding 20 > output.txt

Observation

The rule detected the SupportBridge class with three methods annotated with @JavascriptInterface, as well as the addJavascriptInterface() call that registers the bridge on a WebView with JavaScript enabled.

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
┌─────────────────┐
 4 Code Findings 
└─────────────────┘

    MastgTestWebView_reversed.java
    ❯❱ rules.mastg-android-webview-bridges-javascriptinterface
          [MASVS-PLATFORM-2] Method is exposed to JavaScript via @JavascriptInterface

           37 @JavascriptInterface
           38 public final String getUserProfileJson() throws JSONException {
           39     String name = MastgTestWebView.this.prefs.getString(HintConstants.AUTOFILL_HINT_NAME,
               "");                                                                                     
           40     if (name == null) {
           41         name = "";
           42     }
           43     String email = MastgTestWebView.this.prefs.getString(NotificationCompat.CATEGORY_EMAIL,
               "");                                                                                       
           44     if (email == null) {
           45         email = "";
           46     }
           47     String string = MastgTestWebView.this.prefs.getString("jwt", "");
           48     String jwt = string != null ? string : "";
           49     JSONObject $this$getUserProfileJson_u24lambda_u240 = new JSONObject();
           50     $this$getUserProfileJson_u24lambda_u240.put(HintConstants.AUTOFILL_HINT_NAME, name);
           51     $this$getUserProfileJson_u24lambda_u240.put(NotificationCompat.CATEGORY_EMAIL, email);
           52     $this$getUserProfileJson_u24lambda_u240.put("jwt", jwt);
           53     String string2 = $this$getUserProfileJson_u24lambda_u240.toString();
           54     Intrinsics.checkNotNullExpressionValue(string2, "toString(...)");
           55     return string2;
           56 }
            ⋮┆----------------------------------------
           58 @JavascriptInterface
           59 public final String submitSupportMessage(String email, String message) throws JSONException
               {                                                                                          
           60     Intrinsics.checkNotNullParameter(email, "email");
           61     Intrinsics.checkNotNullParameter(message, "message");
           62     JSONObject $this$submitSupportMessage_u24lambda_u241 = new JSONObject();
           63     $this$submitSupportMessage_u24lambda_u241.put(NotificationCompat.CATEGORY_STATUS,
               "ok");                                                                               
           64     $this$submitSupportMessage_u24lambda_u241.put(NotificationCompat.CATEGORY_EMAIL,
               email);                                                                             
           65     $this$submitSupportMessage_u24lambda_u241.put("message", message);
           66     String string = $this$submitSupportMessage_u24lambda_u241.toString();
           67     Intrinsics.checkNotNullExpressionValue(string, "toString(...)");
           68     return string;
           69 }
            ⋮┆----------------------------------------
           71 @JavascriptInterface
           72 public final void updateSupportPreference(String value) {
           73     Intrinsics.checkNotNullParameter(value, "value");
           74     SharedPreferences $this$edit_u24default$iv = MastgTestWebView.this.prefs;
           75     SharedPreferences.Editor editor$iv = $this$edit_u24default$iv.edit();
           76     editor$iv.putString("support_preference", value);
           77     editor$iv.apply();
           78 }

    ❯❱ rules.mastg-android-webview-bridges-setup
          [MASVS-PLATFORM-2] WebView has JavaScript enabled and a native bridge is registered via
          addJavascriptInterface()                                                               

           85 webView.addJavascriptInterface(new SupportBridge(), "MASBridge");

Evaluation

After reviewing the decompiled code at the locations reported in the output, we can conclude that the test fails because:

  • JavaScript is enabled on the WebView and a native bridge (SupportBridge) is registered via addJavascriptInterface() (line 85).
  • There are three sensitive methods annotated with @JavascriptInterface that are reachable from JavaScript running in the page, without any origin restrictions or other mitigations:
    • getUserProfileJson() (line 37) reads the user's name, email, and a JWT from SharedPreferences and returns them to JavaScript as a JSON string, exposing PII and a credential.
    • submitSupportMessage(email, message) (line 58) allows JavaScript to trigger a support-message submission on behalf of the user, affecting integrity.
    • updateSupportPreference(value) (line 71) allows JavaScript to write arbitrary values to SharedPreferences, affecting app integrity.

Two remediation paths exist for this issue:

  • One is to prevent attacker controlled input from being rendered as executable HTML or JavaScript in the WebView. This is intentionally left vulnerable in the demo to simulate an XSS style condition, but in a real app the JavaScript execution primitive could come from many other causes, not only obvious reflected input.
  • The other is to redesign the native bridge by removing the legacy addJavascriptInterface() model and using a message based bridge with explicit origin restrictions, such as WebViewCompat.addWebMessageListener() with a narrow allowedOriginRules allowlist and sender validation. In this demo, that bridge redesign alone would not visibly stop the exploit because the injected script executes in the same trusted main page origin, so it would still satisfy the origin policy. Even so, it remains an important hardening measure, and it becomes effective when attacker controlled JavaScript runs from a different origin or untrusted frame, which is the broader class of risk associated with legacy JavaScript bridges.