MASTG-DEMO-0095: Attacker-Controlled Input in a WebView Leading to Unintended Navigation
Download MASTG-DEMO-0095 IPA Open MASTG-DEMO-0095 Folder Build MASTG-DEMO-0095 IPA
Sample¶
This sample demonstrates how attacker controlled input inside a WebView can alter the rendered page and trigger unintended navigation. The app loads a trusted local HTML file, but the page reads the username parameter from the URL and injects it into the DOM using innerHTML.
Although the app uses webView.loadFileURL(urlWithUsername, allowingReadAccessTo: docDir), broad file read access is not the focus of this demo. See HTML Injection in a Local WebView Leading to Local File Access for a deeper analysis of the file access aspect of this vulnerability. The issue demonstrated here is that attacker-controlled input is rendered as HTML, which allows the attacker to inject content that changes page behavior and causes unintended navigation.
When selecting payloads, note that <script> payloads usually do not execute in this case because scripts inserted through innerHTML are generally inert. However, other injected elements can still have side effects. For example, <img onerror> and <svg onload> can execute JavaScript through event handlers, and <meta http-equiv="refresh"> may also trigger navigation by instructing the page to refresh to a different URL.
Example payloads:
<meta http-equiv="refresh" content="1; url=https://evil.com"><img src=x onerror="window.location='https://evil.com'"><svg onload="window.location='https://evil.com'"></svg>
Summary of steps leading to this vulnerability.
- The app creates a trusted local HTML file and loads it into a
WKWebView. - The WebView URL includes attacker controlled input in the
usernamequery parameter. - The page reads the
usernamevalue fromwindow.location.search. - The value is inserted into the DOM using
innerHTML. - Because the input is treated as HTML instead of plain text, the attacker can inject markup.
- The injected markup introduces active behavior, such as an event handler or a refresh directive.
- As a result, the WebView navigates to an attacker-chosen destination.
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 | |
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 | |
Steps¶
- Unzip the app package and locate the main binary file ( Exploring the App Package), which in this case is
./Payload/MASTestApp.app/MASTestApp. - Open the app binary with radare2 (iOS) with the
-ioption to run this script.
| load_webview.r2 | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
| run.sh | |
|---|---|
1 2 | |
Observation¶
The output shows all cross-references and disassembled snippets.
1 2 3 4 5 6 7 8 | |
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 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | |
Evaluation¶
The test case fails because the username parameter (attacker-controlled) is inserted into the WebView URL without validation, and the page then assigns it directly to innerHTML. Because the browser treats the value as HTML rather than plain text, an attacker can inject markup that triggers unintended navigation.
The code also contains a separate issue where the WebView is granted read access to the entire Documents directory, which contains sensitive files. However, the focus of this demo is on the HTML injection and unintended navigation aspect of the vulnerability. See HTML Injection in a Local WebView Leading to Local File Access for a deeper analysis of the file access aspect.
AI-Decompiled Code Analysis¶
About ai-decompiled.swift
The ai-decompiled.swift file is an AI-assisted reconstruction derived from showWebView.asm, docDir-init.asm, and fileURL-init.asm and is provided only as a convenience for understanding the logic. It may be inaccurate or incomplete; the assembly and the original binary are the authoritative sources for analysis.
- On line 27, the function constructs a URL string by concatenating
fileURL.absoluteString,"?username=", and the attacker-controlledusernameargument without any validation. - On line 29, the concatenated string is passed to
URL(string:)to create aURLobject without validating the resulting scheme, host, path, or structure. - On line 35, the constructed URL is passed to
WKWebView.loadFileURL, allowing a user who can alterusernameto influence the URL that is ultimately loaded.
Disassembly Analysis¶
The loadFileURL:allowingReadAccessToURL: call is at 0x100004f20 in the username completion closure at 0x100004d24 (all addresses below are from showWebView.asm). The two arguments are built as follows.
Step 1 — Build the URL string with the attacker-controlled username:
At 0x100004df8, the fileURL static property is projected and its absoluteString is fetched. The query suffix ?username= is then encoded inline as immediate character constants and appended to form the final URL string:
0x100004e04 bl sym.imp.Foundation.URL.absoluteString_...vg_ ; get fileURL as a string
0x100004e0c mov x0, 0x753f ; '?u'
0x100004e10 movk x0, 0x6573, lsl 16 ; 'se'
0x100004e14 movk x0, 0x6e72, lsl 32 ; 'rn'
0x100004e18 movk x0, 0x6d61, lsl 48 ; 'am'
0x100004e20 mov x1, 0x3d65 ; 'e='
0x100004e28 bl sym.imp.append_...ySSF_ ; append "?username"
0x100004e2c mov x0, x27 ; load attacker-controlled username value
0x100004e30 mov x1, x26
0x100004e34 bl sym.imp.append_...ySSF_ ; append username directly (no validation)
The attacker-controlled value (passed in as username) is appended at 0x100004e34 without any sanitization or encoding.
The concatenated string is then passed to Foundation.URL.string(_:) at 0x100004e44 to produce a URL object. If that URL is successfully created (the b.ne check at 0x100004e74 succeeds), execution falls through to the loadFileURL call.
Step 2 — Call loadFileURL:allowingReadAccessToURL::
0x100004f10 ldr x1, [x8, 0x118] ; reloc.fixup.loadFileURL:allowingReadAccessToURL:
0x100004f14 mov x0, x25 ; WKWebView instance
0x100004f18 mov x2, x26 ; fileURL+?username=<value> (attacker influenced)
0x100004f1c mov x3, x20 ; docDir (Documents directory)
0x100004f20 bl sym.imp.objc_msgSend
x2 holds the constructed URL containing the unvalidated username value. x3 holds the docDir lazy static, which resolves to the app's Documents directory (see HTML Injection in a Local WebView Leading to Local File Access for the analysis of that initializer).
Step 3 — innerHTML injection (HTML/JS side):
The innerHTML injection does not appear as a Swift evaluateJavaScript call. Instead, the HTML template is statically embedded in the binary as a string literal. We can confirm this by searching for innerHTML with r2, which reports a hit at 0x10000b261 in output.txt. The HTML written to disk embeds the following JavaScript:
const name = new URLSearchParams(window.location.search).get('username');
document.getElementById('username').innerHTML = name;
Because the username query parameter is read directly from window.location.search and assigned to innerHTML without escaping, any HTML or JavaScript event handler the attacker places in the username value is rendered as markup by the WebView.
How to Fix¶
Replace innerHTML with textContent in the embedded HTML template so that the username value is always treated as plain text, not markup. This prevents the browser from parsing attacker-controlled input as HTML and eliminates the unintended navigation vector.
| fix.diff | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 | |
To apply the fix to MastgTest.swift, run the following command from this demo directory:
patch MastgTest.swift -o MastgTest-fixed.swift < fix.diff
This code has other issues (e.g. the URL construction is still unsafe), but this change alone is sufficient to prevent the specific vulnerability shown in this demo.