Skip to content

MASTG-DEMO-0153: Universal Link Handler Without Input Validation

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

Sample

The app declares the Associated Domains entitlement for applinks:demo.mas.owasp.org, so iOS routes verified universal links for that domain to the app and delivers them through onContinueUserActivity(NSUserActivityTypeBrowsingWeb).

The handler reads the amount query parameter from the universal link's webpageURL for the /transfer path and uses it directly as a string, without converting it to a numeric type or checking its value against any bounds. The domain is verified by the OS, but the path and query parameters are caller-controlled: anyone can send the user a link such as https://demo.mas.owasp.org/transfer?amount=9999999, and the handler will accept the supplied value and use it in the transfer flow without validating that it is a valid, bounded amount.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:demo.mas.owasp.org</string>
    </array>
</dict>
</plist>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import SwiftUI

@main
struct MASTestAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
                    // Store the incoming universal link so mastgTest() can process it
                    UniversalLinkState.lastActivity = activity
                }
        }
    }
}
 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
import SwiftUI

// SUMMARY: This sample demonstrates a universal link handler that accepts the
// URL path and query parameters without input validation. The handler reads the
// "amount" query parameter from the verified domain's webpageURL and uses it
// directly, without converting it to a numeric type or checking it against any bounds.

enum UniversalLinkState {
    static var lastActivity: NSUserActivity?
}

struct MastgTest {
    @inline(never) @_optimize(none)
    public static func mastgTest(completion: @escaping (String) -> Void) {
        // In a real app, the system delivers the universal link through
        // onContinueUserActivity(NSUserActivityTypeBrowsingWeb) (see MASTestAppApp.swift).
        // For a self-contained demo, simulate an incoming universal link to the
        // app's verified domain when no real activity has been received.
        let activity = UniversalLinkState.lastActivity ?? {
            let a = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
            a.webpageURL = URL(string: "https://demo.mas.owasp.org/transfer?amount=9999999")
            return a
        }()
        handleUniversalLink(activity, completion: completion)
    }

    // FAIL: [MASTG-TEST-0395] The handler uses the "amount" parameter from the
    // universal link directly without bounds-checking or type validation.
    @inline(never) @_optimize(none)
    public static func handleUniversalLink(_ userActivity: NSUserActivity, completion: @escaping (String) -> Void) {
        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
              let url = userActivity.webpageURL,
              let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
            completion("Invalid universal link")
            return
        }

        if url.path == "/transfer" {
            let amount = components.queryItems?.first(where: { $0.name == "amount" })?.value ?? "0"
            completion("Transferring \(amount) units")
            return
        }
        completion("Unknown path: \(url.path)")
    }
}

Because the OS only routes the link after verifying the domain, triggering a real universal link requires the Apple App Site Association file to be reachable for demo.mas.owasp.org, which isn't the case for this demo. But you can still exercise the same handler by constructing an NSUserActivity with a crafted webpageURL and invoking the continuation entry point with Frida (iOS), as described in Opening Deep Links. Either way, the handler returns the attacker-controlled value unchanged:

Transferring 9999999 units

Repeating with a non-numeric value (amount=not-a-number) returns Transferring not-a-number units, confirming at runtime that the handler accepts arbitrary universal link input without numeric conversion or bounds checking.

Steps

  1. Use Exploring the App Package to extract the relevant binaries from the app package, which in this case is ./Payload/MASTestApp.app/MASTestApp.
  2. Use Extracting Entitlements from MachO Binaries with rabin2 (rabin2 -OC) to extract the entitlements embedded in the signed binary and confirm the app declares universal link support via com.apple.developer.associated-domains.
  3. Use Static Analysis on iOS to locate the universal link handler and check for input validation. Run the r2 script with the -i option.
 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
e asm.bytes=false
e scr.color=false
e asm.var=false

?e === Universal link handler method ===
f~handleUniversalLink

?e
?e === References to Int conversion (input validation) ===
f~Int_init

?e
?e === References to onContinueUserActivity (SwiftUI universal link handling) ===
f~onContinueUserActivity

?e
?e === References to webpageURL (universal link URL) ===
f~webpageURL

?e
?e === Disassembly of handleUniversalLink: URL parsing and path (action) comparison ===
pd 50 @ 0x1000069a0

?e
?e === Disassembly of handleUniversalLink: query value extraction through string interpolation ===
pd 80 @ 0x100006bc0
1
2
3
4
5
6
#!/bin/bash
# Extract the entitlements embedded in the signed binary (see @MASTG-TECH-0111).
rabin2 -OC MASTestApp | sed -n '1,/<\/plist>/p' > entitlements_reversed.plist

# Locate the universal link handler and check for input validation.
r2 -q -i input_validation.r2 -A MASTestApp > output.txt

Observation

The extracted entitlements confirm the app is associated with the demo.mas.owasp.org domain, so iOS routes its verified universal links to the app:

entitlements_reversed.plist
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:demo.mas.owasp.org</string>
    </array>
</dict>
</plist>

The r2 output shows the handleUniversalLink method symbol, an empty result for Int conversion references, the onContinueUserActivity registration, the webpageURL reference, and focused disassembly of the handler covering URL parsing, path comparison, and query value extraction:

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
 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
=== Universal link handler method ===
0x1000064cc 3068 sym.MASTestApp.MastgTest.handleUniversalLink.completion.NSUserActivity_...ySSctFZ_
0x100007290 152 sym.MASTestApp.MastgTest.handleUniversalLink.completion.NSUserActivity.Foundation.URLQueryItem...VXEfU_

=== References to Int conversion (input validation) ===

=== References to onContinueUserActivity (SwiftUI universal link handling) ===
0x100007b2c 116 sym.SwiftUI.WindowGroup...VyAA4ViewPAAE22onContinueUserActivity_7performQrSS_ySo06NSUserI0CctFQOy10MASTestApp07ContentE0V_Qo_GACyxGAA5SceneAAWl
0x10000a886 0 sym.Content._symbolic______y_____y______Qo_G_7SwiftUI11WindowGroupV_AA4ViewPAAE22onContinueUserActivity_7performQrSS_ySo06NSUserI0CctFQO_10MASTestApp07ContentE0V
0x10000a89e 0 sym.Content._symbolic______y______Qo__7SwiftUI4ViewPAAE22onContinueUserActivity_7performQrSS_ySo06NSUserG0CctFQO_10MASTestApp07ContentC0V
0x10000a8ce 2 sym.SwiftUI.WindowGroup...VyAA4ViewPAAE22onContinueUserActivity_7performQrSS_ySo06NSUserI0CctFQOy10MASTestApp07ContentE0V_Qo_GAA5SceneHPyHC.1
0x10000aa10 0 sym.SwiftUI.WindowGroup...VyAA4ViewPAAE22onContinueUserActivity_7performQrSS_ySo06NSUserI0CctFQOy10MASTestApp07ContentE0V_Qo_GMR
0x1000102a0 0 sym.SwiftUI.WindowGroup...VyAA4ViewPAAE22onContinueUserActivity_7performQrSS_ySo06NSUserI0CctFQOy10MASTestApp07ContentE0V_Qo_GMd
0x1000102b8 0 sym.SwiftUI.WindowGroup...VyAA4ViewPAAE22onContinueUserActivity_7performQrSS_ySo06NSUserI0CctFQOy10MASTestApp07ContentE0V_Qo_GACyxGAA5SceneAAWL

=== References to webpageURL (universal link URL) ===
0x10000a280 24 sym._objc_msgSend_webpageURL
0x10000acf2 11 str.webpageURL
0x100010018 8 reloc.fixup.webpageURL

=== Disassembly of handleUniversalLink: URL parsing and path (action) comparison ===
           0x1000069a0      sub x10, x29, 0xe4
           0x1000069a4      stur w9, [x10, -0x100]
           0x1000069a8      and w1, w9, 1
           0x1000069ac      bl sym.imp.Foundation.URLComponents.url.resolvingAgainstBaseURL...A0G0Vh_SbtcfC
           0x1000069b0      sub x8, x29, 0x98
           0x1000069b4      ldur x8, [x8, -0x100]
           0x1000069b8      sub x9, x29, 0xa8
           0x1000069bc      ldur x0, [x9, -0x100]
           0x1000069c0      sub x9, x29, 0xe4
           0x1000069c4      ldur w1, [x9, -0x100]
           0x1000069c8      sub x9, x29, 0xa0
           0x1000069cc      ldur x2, [x9, -0x100]
           0x1000069d0      ldr x8, [x8, 0x30]
           0x1000069d4      blr x8
           0x1000069d8      subs w8, w0, 1
       ┌─< 0x1000069dc      b.ne 0x100006a14
      ┌──< 0x1000069e0      b 0x1000069e4
      ││   ; CODE XREF from func.1000064cc @ 0x1000069e0(x)
      └──> 0x1000069e4      sub x8, x29, 0xa8
          0x1000069e8      ldur x0, [x8, -0x100]                     ; int64_t arg1
          0x1000069ec      bl sym.Foundation.URLComponents:_GenericAccessorW.bool____GenericAccessor ; func.1000071b0
          0x1000069f0      sub x8, x29, 0x40
          0x1000069f4      ldur x8, [x8, -0x100]
          0x1000069f8      sub x9, x29, 0x48
          0x1000069fc      ldur x1, [x9, -0x100]
          0x100006a00      sub x9, x29, 0x18
          0x100006a04      ldur x0, [x9, -0x100]
          0x100006a08      ldr x8, [x8, 8]
          0x100006a0c      blr x8
      ┌──< 0x100006a10      b 0x100007054
      ││   ; CODE XREF from func.1000064cc @ 0x1000069dc(x)
      │└─> 0x100006a14      sub x8, x29, 0x18
          0x100006a18      ldur x20, [x8, -0x100]
          0x100006a1c      sub x8, x29, 0xa0
          0x100006a20      ldur x2, [x8, -0x100]
          0x100006a24      sub x8, x29, 0xa8
          0x100006a28      ldur x1, [x8, -0x100]
          0x100006a2c      sub x8, x29, 0x88
          0x100006a30      ldur x0, [x8, -0x100]
          0x100006a34      sub x8, x29, 0x98
          0x100006a38      ldur x8, [x8, -0x100]
          0x100006a3c      ldr x8, [x8, 0x20]
          0x100006a40      blr x8
          0x100006a44      bl sym.imp.Foundation.URL.path_...vg_     ; Foundation.URL.path(...vg)
          0x100006a48      sub x8, x29, 0x100
          0x100006a4c      stur x0, [x8, -0x100]
          0x100006a50      sub x8, x29, 0xf0
          0x100006a54      stur x1, [x8, -0x100]
          0x100006a58      adrp x0, 0x10000a000
          0x100006a5c      add x0, x0, 0x6b7                         ; 0x10000a6b7 ; "/transfer"
          0x100006a60      mov w8, 9
          0x100006a64      mov x1, x8

=== Disassembly of handleUniversalLink: query value extraction through string interpolation ===
           0x100006bc0      ldur x20, [x8, -0x100]
           0x100006bc4      bl sym.imp.Foundation.URLQueryItem.value_...Sgvg_ ; Foundation.URLQueryItem.value(...Sgvg)
           0x100006bc8      sub x8, x29, 0x120
           0x100006bcc      ldur x8, [x8, -0x100]
           0x100006bd0      mov x2, x0
           0x100006bd4      sub x9, x29, 0xb8
           0x100006bd8      ldur x0, [x9, -0x100]
           0x100006bdc      sub x9, x29, 0x148
           0x100006be0      stur x2, [x9, -0x100]
           0x100006be4      mov x2, x1
           0x100006be8      sub x9, x29, 0x128
           0x100006bec      ldur x1, [x9, -0x100]
           0x100006bf0      sub x9, x29, 0x140
           0x100006bf4      stur x2, [x9, -0x100]
           0x100006bf8      ldr x8, [x8, 8]
           0x100006bfc      blr x8
           0x100006c00      sub x8, x29, 0x148
           0x100006c04      ldur x9, [x8, -0x100]
           0x100006c08      sub x8, x29, 0x140
           0x100006c0c      ldur x8, [x8, -0x100]
           0x100006c10      sub x10, x29, 0x138
           0x100006c14      stur x9, [x10, -0x100]
           0x100006c18      sub x9, x29, 0x130
           0x100006c1c      stur x8, [x9, -0x100]
       ┌─< 0x100006c20      b 0x100006c24
          ; CODE XREFS from func.1000064cc @ 0x100006c20(x), 0x100006e8c(x)
       └─> 0x100006c24      sub x8, x29, 0x138
           0x100006c28      ldur x9, [x8, -0x100]
           0x100006c2c      sub x8, x29, 0x130
           0x100006c30      ldur x8, [x8, -0x100]
           0x100006c34      stur x9, [x29, -0x88]
           0x100006c38      stur x8, [x29, -0x80]
           0x100006c3c      ldur q0, [x29, -0x88]
           0x100006c40      stur q0, [x29, -0xa0]
           0x100006c44      ldur x8, [x29, -0x98]
       ┌─< 0x100006c48      cbz x8, 0x100006c64
      ┌──< 0x100006c4c      b 0x100006c50
      ││   ; CODE XREF from func.1000064cc @ 0x100006c4c(x)
      └──> 0x100006c50      ldur x9, [x29, -0xa0]
          0x100006c54      ldur x8, [x29, -0x98]
          0x100006c58      stur x9, [x29, -0x78]
          0x100006c5c      stur x8, [x29, -0x70]
      ┌──< 0x100006c60      b 0x100006c94
      ││   ; CODE XREF from func.1000064cc @ 0x100006c48(x)
      │└─> 0x100006c64      adrp x0, 0x10000a000
          0x100006c68      add x0, x0, 0x6d1
          0x100006c6c      mov w8, 1
          0x100006c70      mov x1, x8
          0x100006c74      mov w8, 1
          0x100006c78      and w2, w8, 1
          0x100006c7c      bl sym.imp._builtinStringLiteral.utf8CodeUnitCount.isASCII__String:_Builtin.Word__B_...cfC_ ; _builtinStringLiteral.utf8CodeUnitCount.isASCII__String: Builtin.Word, B(...cfC)
          0x100006c80      stur x0, [x29, -0x78]
          0x100006c84      stur x1, [x29, -0x70]
          0x100006c88      ldur x8, [x29, -0x98]
      │┌─< 0x100006c8c      cbz x8, 0x100006e64
     ┌───< 0x100006c90      b 0x100006e68
     │││   ; CODE XREFS from func.1000064cc @ 0x100006c60(x), 0x100006e64(x), 0x100006e70(x)
     │└──> 0x100006c94      sub x8, x29, 0x60
         0x100006c98      ldur x0, [x8, -0x100]
         0x100006c9c      ldur x9, [x29, -0x78]
         0x100006ca0      sub x8, x29, 0x188
         0x100006ca4      stur x9, [x8, -0x100]
         0x100006ca8      ldur x8, [x29, -0x70]
         0x100006cac      sub x10, x29, 0x150
         0x100006cb0      stur x8, [x10, -0x100]
         0x100006cb4      stur x9, [x29, -0xb0]
         0x100006cb8      stur x8, [x29, -0xa8]
         0x100006cbc      bl sym.imp.swift_retain
         0x100006cc0      mov w8, 0x13
         0x100006cc4      mov x0, x8
         0x100006cc8      mov w8, 1
         0x100006ccc      mov x1, x8
         0x100006cd0      bl sym.imp.DefaultStringInterpolation.literalCapacity.interpolationCount_...itcfC_ ; DefaultStringInterpolation.literalCapacity.interpolationCount(...itcfC)
         0x100006cd4      sub x20, x29, 0xc0
         0x100006cd8      sub x8, x29, 0x170
         0x100006cdc      stur x20, [x8, -0x100]
         0x100006ce0      stur x0, [x29, -0xc0]
         0x100006ce4      stur x1, [x29, -0xb8]
         0x100006ce8      adrp x0, 0x10000a000
         0x100006cec      add x0, x0, 0x6d3                         ; 0x10000a6d3 ; "Transferring"
         0x100006cf0      mov w8, 0xd
         0x100006cf4      mov x1, x8
         0x100006cf8      mov w8, 1
         0x100006cfc      sub x9, x29, 0x17c

Evaluation

The test case fails because the handler uses the universal link's query value directly without any type conversion or bounds checking. The disassembly reveals the following flow:

  • At 0x1000069ac, the handler calls URLComponents.init(url:resolvingAgainstBaseURL:) to parse the verified universal link URL read from webpageURL.
  • At 0x100006a44, it calls URL.path to read the URL path, which represents the action, and compares it against the "/transfer" string literal loaded at 0x100006a5c.
  • At 0x100006bc4, it calls URLQueryItem.value to extract the raw String? value of the amount query item.
  • At 0x100006cd0, the value flows into DefaultStringInterpolation to build the "Transferring ... units" output string, with the "Transferring" literal loaded at 0x100006cec.

Between URLQueryItem.value (0x100006bc4) and DefaultStringInterpolation (0x100006cd0) there is no call to Int.init or any other visible type conversion or validation function. This is further confirmed by the empty === References to Int conversion (input validation) === section. The handler therefore accepts an arbitrary string value from the universal link query and uses it directly in the transfer-related output, instead of validating that the value is numeric and within an expected range.