Skip to content

MASTG-DEMO-0142: Sensitive Data and Functionality Exposed Through a WKWebView Native Bridge

Download MASTG-DEMO-0142 IPA Open MASTG-DEMO-0142 Folder Build MASTG-DEMO-0142 IPA

Sample

This sample demonstrates a WKWebView that registers a native bridge handler named "bridge" via WKUserContentController.add(_:name:). The SecretBridgeHandler class implements WKScriptMessageHandler and handles two actions ("getSecret" and "getCredentials") that return sensitive native data directly to JavaScript without validating the message origin. Any JavaScript running in the WebView can invoke these actions using:

window.webkit.messageHandlers.bridge.postMessage({action: 'getSecret'})
window.webkit.messageHandlers.bridge.postMessage({action: 'getCredentials'})
MastgTest.swift
  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
import UIKit
import WebKit

// SUMMARY: This sample demonstrates a WKWebView with a WKScriptMessageHandler named "bridge"
// that exposes sensitive native functionality to JavaScript running inside the WebView.
// Any JavaScript in the page can call:
//   window.webkit.messageHandlers.bridge.postMessage({"action": "getSecret"})
// to invoke native code and receive a sensitive API key in response.

// FAIL: [MASTG-TEST-0376] The SecretBridgeHandler exposes sensitive native data
// (a stored API key) to any JavaScript running in the WebView without validating
// the origin or the content of the incoming message.
class SecretBridgeHandler: NSObject, WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController,
                               didReceive message: WKScriptMessage) {
        guard let body = message.body as? [String: String],
              let action = body["action"] else { return }

        switch action {
        case "getSecret":
            // FAIL: [MASTG-TEST-0376] Sensitive data (API key) is returned to JavaScript
            // without validating the origin of the message.
            let secret = "MASTG_API_KEY=072037ab-1b7b-4b3b-8b7b-1b7b4b3b8b7b"
            let js = "window.receiveSecret('\(secret)')"
            message.webView?.evaluateJavaScript(js, completionHandler: nil)
        case "getCredentials":
            // FAIL: [MASTG-TEST-0376] User credentials are returned to JavaScript
            // without any input validation or origin check.
            let credentials = "user=admin&pass=S3cr3t!"
            let js = "window.receiveCredentials('\(credentials)')"
            message.webView?.evaluateJavaScript(js, completionHandler: nil)
        default:
            break
        }
    }
}

struct MastgTest {
    @inline(never) @_optimize(none)
    public static func mastgTest(completion: @escaping (String) -> Void) {
        DispatchQueue.main.async {
            showWebView(completion: completion)
        }
    }

    private static func showWebView(completion: @escaping (String) -> Void) {
        let config = WKWebViewConfiguration()

        // FAIL: [MASTG-TEST-0376] A native bridge named "bridge" is registered via
        // add(_:name:), exposing SecretBridgeHandler to any JavaScript in the WebView.
        config.userContentController.add(SecretBridgeHandler(), name: "bridge")

        let webView = WKWebView(frame: .zero, configuration: config)

        let html = """
        <!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; }
                button { padding: 10px 20px; margin: 5px; font-size: 16px; }
                pre { text-align: left; padding: 10px; background: #f4f4f4; border-radius: 4px; }
            </style>
        </head>
        <body>
            <h1>Secure Portal</h1>
            <button onclick="getSecret()">Get API Key</button>
            <button onclick="getCredentials()">Get Credentials</button>
            <pre id="result">Press a button to invoke the native bridge.</pre>
            <script>
                window.receiveSecret = function(secret) {
                    document.getElementById('result').textContent = 'API Key: ' + secret;
                };
                window.receiveCredentials = function(creds) {
                    document.getElementById('result').textContent = 'Credentials: ' + creds;
                };
                function getSecret() {
                    window.webkit.messageHandlers.bridge.postMessage({action: 'getSecret'});
                }
                function getCredentials() {
                    window.webkit.messageHandlers.bridge.postMessage({action: 'getCredentials'});
                }
            </script>
        </body>
        </html>
        """

        webView.loadHTMLString(html, baseURL: nil)

        let vc = UIViewController()
        vc.view = webView

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

        presenter.present(vc, animated: true) {
            completion("WebView with native bridge loaded.")
        }
    }

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

Steps

  1. Use Exploring the App Package to extract the app. The main binary is ./Payload/MASTestApp.app/MASTestApp.
  2. Use radare2 (iOS) with the -i option to run this script.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
e asm.bytes=false
e scr.color=false
e scr.interactive=false
e asm.var=false
e bin.relocs.apply=true

?e List all uses of the 'addScriptMessageHandler:name:' selector:
f~addScriptMessageHandler

?e

?e xrefs to 'addScriptMessageHandler:name:':
axt @ reloc.fixup.addScriptMessageHandler:name:

?e

?e Code snippet containing the bridge registration call:
pd-- 20 @ 0x1eac
1
2
#!/bin/bash
r2 -q -e bin.relocs.apply=true -i webview_native_bridge.r2 -A MASTestApp > output.txt

Observation

The output shows the addScriptMessageHandler:name: selector used in the binary and its cross-reference back to the function that registers the bridge.

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
List all uses of the 'addScriptMessageHandler:name:' selector:
0x0001342e 30 str.addScriptMessageHandler:name:
0x0001c2e0 8 reloc.fixup.addScriptMessageHandler:name:

xrefs to 'addScriptMessageHandler:name:':
sym.MASTestApp.MastgTest.showWebView._6E8AB2C58CE173A727EF27CB85DF8CD8.completion_...FZ_ 0x1eac [DATA:r--] ldr x1, reloc.fixup.addScriptMessageHandler:name:

Code snippet containing the bridge registration call:
           0x00001e5c      bl sym.MASTestApp.SecretBridgeHandler.allocator._...cfC_ ; func.00001a94
           0x00001e60      stur x0, [x29, -0xe0]
           0x00001e64      adrp x0, 0x14000
           0x00001e68      add x0, x0, 0x98d                          ; 0x1498d ; "bridge"
           0x00001e6c      mov w8, 6
           0x00001e70      mov x1, x8
           0x00001e74      mov w8, 1
           0x00001e78      stur w8, [x29, -0x8c]
           0x00001e7c      and w2, w8, 1
           0x00001e80      bl sym.imp._builtinStringLiteral.utf8CodeUnitCount.isASCII__String:_Builtin.Word__B_...cfC_ ; _builtinStringLiteral.utf8CodeUnitCount.isASCII__String: Builtin.Word, B(...cfC)
           0x00001e84      stur x1, [x29, -0xf0]
           0x00001e88      bl sym.imp.Foundationbool_...ridgeToObjectiveCSo8NSStringCyF_ ; Foundationbool(...ridgeToObjectiveCSo8NSStringCyF)
           0x00001e8c      mov x1, x0
           0x00001e90      ldur x0, [x29, -0xf0]                      ; void *arg0
           0x00001e94      stur x1, [x29, -0xe8]
           0x00001e98      bl sym.imp.swift_bridgeObjectRelease       ; void swift_bridgeObjectRelease(void *arg0)
           0x00001e9c      ldur x3, [x29, -0xe8]
           0x00001ea0      ldur x2, [x29, -0xe0]
           0x00001ea4      ldur x0, [x29, -0xd8]                      ; void *instance
           0x00001ea8      adrp x8, sym.__PROTOCOLS__TtC10MASTestApp19SecretBridgeHandler ; 0x1c000
           0x00001eac      ldr x1, [x8, 0x2e0]                        ; [0x1342e:8]=0x7069726353646461 ; "addScriptMessageHandler:name:" ; char *selector
           0x00001eb0      bl sym.imp.objc_msgSend                    ; void *objc_msgSend(void *instance, char *selector)
           0x00001eb4      ldur x0, [x29, -0xe8]
           0x00001eb8      adrp x8, segment.__DATA_CONST              ; 0x18000
           0x00001ebc      ldr x8, [x8, 0x108]                        ; [0x18108:8]=0
                                                                      ; reloc.objc_release
           0x00001ec0      blr x8
           0x00001ec4      ldur x0, [x29, -0xe0]
           0x00001ec8      bl sym.imp.swift_unknownObjectRelease
           0x00001ecc      ldur x0, [x29, -0xd8]
           0x00001ed0      adrp x8, segment.__DATA_CONST              ; 0x18000
           0x00001ed4      ldr x8, [x8, 0x108]                        ; [0x18108:8]=0
                                                                      ; reloc.objc_release
           0x00001ed8      blr x8
           0x00001edc      ldur x0, [x29, -0xa8]                      ; int64_t arg_20h
           0x00001ee0      bl sym....sSo9WKWebViewCMa                 ; func.00002ec0
           0x00001ee4      mov x20, x0
           0x00001ee8      ldur x0, [x29, -0xd0]
           0x00001eec      adrp x8, segment.__DATA_CONST              ; 0x18000
           0x00001ef0      ldr x8, [x8, 0x110]                        ; [0x18110:8]=0
                                                                      ; reloc.objc_retain
           0x00001ef4      blr x8
           0x00001ef8      ldur x0, [x29, -0xd0]                      ; int64_t arg1

Evaluation

The test case fails because the app registers a native bridge handler named "bridge" via WKUserContentController.add(_:name:) and the corresponding SecretBridgeHandler.userContentController(_:didReceive:) exposes sensitive data to any JavaScript running in the WebView.

In the output, the addScriptMessageHandler:name: selector is found at reloc.fixup.addScriptMessageHandler:name:, and the cross-reference points to sym.MASTestApp.MastgTest.showWebView._6E8AB2C58CE173A727EF27CB85DF8CD8.completion_...FZ_. Inspecting that function (using Reviewing Disassembled Objective-C and Swift Code) reveals that:

  • The bridge named "bridge" is registered unconditionally on the WKUserContentController without any origin-based restrictions, as shown by the "bridge" string loaded at 0x00001e68 immediately before the addScriptMessageHandler:name: call at 0x00001eac.
  • The SecretBridgeHandler handles a "getSecret" action that returns a hardcoded API key string to JavaScript, and a "getCredentials" action that returns user credentials, both via evaluateJavaScript:completionHandler:.
  • No validation is performed on the incoming message content or origin, so any JavaScript in the page can call these actions.

Because the WebView loads a locally constructed HTML page that a developer controls, the immediate impact in this demo is limited. However, if the WebView were to load attacker-controlled content (for example through a deep link, open redirect, or XSS), an attacker could invoke window.webkit.messageHandlers.bridge.postMessage({action: 'getSecret'}) and receive the API key in the receiveSecret callback.