Skip to content

MASTG-DEMO-0077: Runtime Monitoring of Text Fields Eligible for Keyboard Caching with Frida

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

Sample

This demo uses the same sample as Keyboard Caching Not Prevented for Sensitive Data with r2.

../MASTG-DEMO-0076/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
import SwiftUI
import UIKit

struct MastgTest {

  static func mastgTest(completion: @escaping (String) -> Void) {
    DispatchQueue.main.async {
      // Build the alert
      let alert = UIAlertController(
        title: "Enter sensitive text",
        message: "Please fill in all fields.",
        preferredStyle: .alert
      )

      alert.addTextField { tf in
        tf.placeholder = "Name"
        tf.autocorrectionType = .default
        tf.spellCheckingType = .no
        tf.accessibilityIdentifier = "name_field"
      }
      alert.addTextField { tf in
        tf.placeholder = "E-Mail"
        tf.autocorrectionType = .no
        tf.accessibilityIdentifier = "email_field"
      }

      alert.addTextField { tf in
        tf.placeholder = "Password"
        tf.isSecureTextEntry = true
        tf.accessibilityIdentifier = "password_field"
      }

      alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
        let first = alert.textFields?[0].text ?? ""
        let second = alert.textFields?[1].text ?? ""
        let third = alert.textFields?[2].text ?? ""
        completion("Submitted values: '\(first)', '\(second)', \(third)")
      }))

      // Present from the topmost view controller
      if let presenter = topViewController() {
        presenter.present(alert, animated: true, completion: nil)
      } else {
        completion("Failed to present alert (no active view controller).")
      }
    }
  }

  // Finds the currently visible view controller to present from
  private static func topViewController(
    base: UIViewController? = {
      let scenes = UIApplication.shared.connectedScenes
        .compactMap { $0 as? UIWindowScene }
      let keyWindow = scenes
        .flatMap { $0.windows }
        .first { $0.isKeyWindow }
      return keyWindow?.rootViewController
    }()
  ) -> UIViewController? {
    if let nav = base as? UINavigationController {
      return topViewController(base: nav.visibleViewController)
    }
    if let tab = base as? UITabBarController {
      return topViewController(base: tab.selectedViewController)
    }
    if let presented = base?.presentedViewController {
      return topViewController(base: presented)
    }
    return base
  }
}

Steps

  1. Install the app on a device ( Installing Apps).
  2. Make sure you have Frida for iOS installed on your machine and the frida-server running on the device.
  3. Run run.sh to spawn your app with Frida.
  4. Click the Start button.
  5. Stop the script by pressing Ctrl+C.
1
2
#!/bin/bash
frida -U -f org.owasp.mastestapp.MASTestApp-iOS -l ./script.js -o 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
const UITextAutocorrectionType = {
  0: ".default",
  1: ".no",
  2: ".yes"
};

const UITextSpellCheckingType = {
  0: ".default",
  1: ".no",
  2: ".yes"
};

const classes = ["UITextField", "UITextView", "UISearchBar"];

classes.forEach(inputClass => hook(inputClass));

function hook(inputClass) {
  const cls = ObjC.classes[inputClass];
  if (!cls || !cls["- resignFirstResponder"]) {
    return;
  }
  Interceptor.attach(cls["- resignFirstResponder"].implementation, {
    onEnter(args) {
      const self = new ObjC.Object(args[0]);
      const correctionType = UITextAutocorrectionType[self.autocorrectionType()];
      const isSecure = self.isSecureTextEntry && self.isSecureTextEntry();

      let aid = null;
      if (self.$methods.indexOf("- accessibilityIdentifier") !== -1) {
        aid = self.accessibilityIdentifier();
      }

      let spellType = null;
      try {
        spellType = UITextSpellCheckingType[self.spellCheckingType()];
      } catch (e) {
        spellType = null;
      }

      let placeholder = null;
      if (self.$methods.indexOf("- placeholder") !== -1) {
        try {
          placeholder = self.placeholder();
        } catch (e) {
          placeholder = null;
        }
      }

      let cacheable = "Eligible for caching";

      if (isSecure ||
        (correctionType === ".no" && spellType === ".no")) {
        cacheable = "Excluded from caching";
      }

      console.log(
        `${cacheable} [autocorrectionType=${correctionType}, spellCheckingType=${spellType}, isSecureTextEntry=${isSecure}, class=${self.$className}, aid=${aid}, placeholder=${placeholder}]: "${self.text()}"`
      );
    }
  });
}

The script hooks into text input controls at runtime and monitors when they lose focus. For each interaction, it captures the entered text, the input field class, accessibility identifier when available, placeholder text when available, and the relevant input traits such as autocorrectionType, spellCheckingType, and isSecureTextEntry. Based on these values, it reports whether the input is eligible for keyboard caching.

Observation

output.txt
1
2
3
Eligible for caching [autocorrectionType=.default, spellCheckingType=.no, isSecureTextEntry=false, class=_UIAlertControllerTextField, aid=name_field, placeholder=Name]: "OWASP MAS"
Eligible for caching [autocorrectionType=.no, spellCheckingType=.default, isSecureTextEntry=false, class=_UIAlertControllerTextField, aid=email_field, placeholder=E-Mail]: "[email protected]"
Excluded from caching [autocorrectionType=.no, spellCheckingType=.no, isSecureTextEntry=true, class=_UIAlertControllerTextField, aid=password_field, placeholder=Password]: "my$ecr3tP4sS!"

The output contains all text that the user entered in every text input, along with its keyboard-cache eligibility.

Evaluation

The test fails because the output shows two text fields that are eligible for keyboard caching and contain sensitive information: the user's name and email address.

  • The name input has autocorrectionType set to default, making it eligible for keyboard caching.
  • The email input has autocorrectionType set to no but spellCheckingType set to default, making it eligible for keyboard caching.

The password input is protected against keyboard caching because isSecureTextEntry is set to true.