Skip to content

MASTG-DEMO-0134: Custom URL Scheme Handler Without Input Validation

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

Sample

The app registers a custom URL scheme (mastgtest://).

The URL handler reads a query parameter value from URLs targeting the transfer action and uses it directly as a string without converting it to a numeric type or checking its value against any bounds. Any app on the device can open a URL such as mastgtest://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
11
12
13
14
15
<?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>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>mastgtest</string>
            </array>
        </dict>
    </array>
</dict>
</plist>
 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
import SwiftUI

// SUMMARY: This sample demonstrates a custom URL scheme handler that accepts
// URL parameters without input validation. The handler reads the "amount"
// query parameter and uses it directly without bounds-checking or type conversion.

enum URLState {
    static var lastURL: URL?
}

struct MastgTest {
    @inline(never) @_optimize(none)
    public static func mastgTest(completion: @escaping (String) -> Void) {
        guard let url = URLState.lastURL else {
            completion("""
            Waiting for incoming URL scheme...

            Open the registered scheme from another app to see the result:
              mastgtest://transfer?amount=500
            """)
            return
        }
        handleURL(url, completion: completion)
    }

    // FAIL: [MASTG-TEST-0370] The handler uses the "amount" parameter directly
    // without bounds-checking or type validation.
    @inline(never) @_optimize(none)
    public static func handleURL(_ url: URL, completion: @escaping (String) -> Void) {
        let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
        let action = url.host ?? ""

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

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 Static Analysis on iOS to locate the URL 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
e asm.bytes=false
e scr.color=false
e asm.var=false

?e === URL handler method ===
f~handleURL

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

?e
?e === References to onOpenURL (SwiftUI URL handling) ===
f~onOpenURL

?e
?e === Disassembly of handleURL: URL parsing and action comparison ===
pd 30 @ 0x100004340

?e
?e === Disassembly of handleURL: value extraction through string interpolation ===
pd 45 @ 0x100004500
1
2
#!/bin/bash
r2 -q -i input_validation.r2 -A MASTestApp > output.txt

Observation

The output shows the handleURL method symbol, an empty result for Int conversion references, the onOpenURL registration, and focused disassembly of the handler covering URL parsing 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
=== URL handler method ===
0x100004280 1328 sym.MASTestApp.MastgTest.handleURL.completion.Foundation...n0F0V_ySSctFZ
0x1000047b0 132 sym.MASTestApp.MastgTest.handleURL.completion.Foundation...n0F0V_ySSctFZSbAF12URLQueryItemVXEfU_
0x10000499c 1284 sym.MASTestApp.MastgTest.handleURL.completion.Foundation...n0F0V_ySSctFZ04_s10a48App11ContentViewV4bodyQrvg7SwiftUI05TupleD0VyAE0r68PAEE7paddingyQrAE4EdgeO3SetV_12CoreGraphics7CGFloatVSgtFQOyAE6HStacks3AGyw23TextV_AE6SpacerVAiEEAJyv42AN_ARtFQOyAiEE12cornerRadius_11antialiasedv91AQ_SbtFQOyAiEE10background_20ignoresSafeAreaEdgesQrqd___ANtAE10ShapeStyleRd__lFQOyAE6Buttons10AiEE4fontyvw18FontVSgFQOyAiEEAJyv17AN_ARtFQOyAiEEAJyv71AN_ARtFQOyAV_Qo__Qo__Qo_G_AE14LinearGradientVQo__Qo__Qo_tGG_Qo__AiEEAJyv148AN_ARtFQOyA

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

=== References to onOpenURL (SwiftUI URL handling) ===
0x100006a6c 72 sym.SwiftUI.WindowGroup...VyAA4ViewPAAE9onOpenURL7performQry10Foundation0H0Vc_tFQOy10MASTestApp07ContentE0V_Qo_GACyxGAA5SceneAAWl
0x1000089fe 0 sym.Content._symbolic______y_____y______Qo_G_7SwiftUI11WindowGroupV_AA4ViewPAAE9onOpenURL7performQry10Foundation0H0Vc_tFQO_10MASTestApp07ContentE0V
0x100008a26 0 sym.Content._symbolic______y______Qo__7SwiftUI4ViewPAAE9onOpenURL7performQry10Foundation0F0Vc_tFQO_10MASTestApp07ContentC0V
0x100008a36 0 sym.SwiftUI.WindowGroup...VyAA4ViewPAAE9onOpenURL7performQry10Foundation0H0Vc_tFQOy10MASTestApp07ContentE0V_Qo_GAA5SceneHPyHC.1
0x1000101e8 0 sym.SwiftUI.WindowGroup...VyAA4ViewPAAE9onOpenURL7performQry10Foundation0H0Vc_tFQOy10MASTestApp07ContentE0V_Qo_GMD
0x100010200 0 sym.SwiftUI.WindowGroup...VyAA4ViewPAAE9onOpenURL7performQry10Foundation0H0Vc_tFQOy10MASTestApp07ContentE0V_Qo_GACyxGAA5SceneAAWL

=== Disassembly of handleURL: URL parsing and action comparison ===
           0x100004340      mov x0, x20
           0x100004344      mov w1, 0
           0x100004348      bl sym.imp.Foundation.URLComponents.url.resolvingAgainstBaseURL...A0G0Vh_SbtcfC
           0x10000434c      bl sym.imp.Foundation.URL.host_...Sgvg_   ; Foundation.URL.host(...Sgvg)
           0x100004350      stur x0, [x29, -0x70]
           0x100004354      stur x1, [x29, -0x68]
           0x100004358      ldur x8, [x29, -0x68]
       ┌─< 0x10000435c      cbz x8, 0x100004374
          0x100004360      ldur x8, [x29, -0x70]
          0x100004364      ldur x9, [x29, -0x68]
          0x100004368      stur x8, [x29, -0x60]
          0x10000436c      stur x9, [x29, -0x58]
      ┌──< 0x100004370      b 0x1000043a0
      ││   ; CODE XREF from func.100004280 @ 0x10000435c(x)
      │└─> 0x100004374      adrp x8, 0x100008000
          0x100004378      add x0, x8, 0xad5
          0x10000437c      mov w8, 1
          0x100004380      mov x1, 0
          0x100004384      and w2, w8, 1
          0x100004388      bl sym.imp._builtinStringLiteral.utf8CodeUnitCount.isASCII__String:_Builtin.Word__B_...cfC_ ; _builtinStringLiteral.utf8CodeUnitCount.isASCII__String: Builtin.Word, B(...cfC)
          0x10000438c      stur x0, [x29, -0x60]
          0x100004390      stur x1, [x29, -0x58]
          0x100004394      ldur x8, [x29, -0x68]
      │┌─< 0x100004398      cbz x8, 0x1000043ec
     ┌───< 0x10000439c      b 0x1000043f0
     │││   ; CODE XREFS from func.100004280 @ 0x100004370(x), 0x1000043ec(x), 0x100004400(x)
     │└──> 0x1000043a0      ldur x27, [x29, -0x60]
         0x1000043a4      ldur x24, [x29, -0x58]
         0x1000043a8      adrp x8, 0x100008000
         0x1000043ac      add x0, x8, 0xad6                         ; 0x100008ad6 ; "transfer"
         0x1000043b0      mov w8, 1
         0x1000043b4      mov x1, 8

=== Disassembly of handleURL: value extraction through string interpolation ===
           0x100004500      mov x0, x25                               ; void *arg1
           0x100004504      bl sym.SwiftUI.ModifiedContent...VyACyACyACyAA10ScrollViewVyACyACyAA4TextVAA16_FlexFrameLayoutVGAA08_PaddingJ0VGGAIGAA24_BackgroundStyleModifierVyAA5ColorVGGAA11_ClipEffectVyAA16RoundedRectangleVGGALGWOhTm ; func.100004908
       ┌─< 0x100004508      b 0x100004694
          ; CODE XREF from func.100004280 @ 0x1000044f4(x)
          0x10000450c      mov x20, x25
          0x100004510      bl sym.imp.Foundation.URLQueryItem.value_...Sgvg_ ; Foundation.URLQueryItem.value(...Sgvg)
          0x100004514      mov x20, x0
          0x100004518      mov x26, x1
          0x10000451c      ldr x8, [x27, 8]
          0x100004520      mov x0, x25
          0x100004524      mov x1, x21
          0x100004528      blr x8
          ; CODE XREF from func.100004280 @ 0x10000469c(x)
          0x10000452c      stur x20, [x29, -0xb0]
          0x100004530      stur x26, [x29, -0xa8]
          0x100004534      ldur x8, [x29, -0xa8]
      ┌──< 0x100004538      cbz x8, 0x100004550
      ││   0x10000453c      ldur x8, [x29, -0xb0]
      ││   0x100004540      ldur x9, [x29, -0xa8]
      ││   0x100004544      stur x8, [x29, -0xa0]
      ││   0x100004548      stur x9, [x29, -0x98]
     ┌───< 0x10000454c      b 0x10000457c
     │││   ; CODE XREF from func.100004280 @ 0x100004538(x)
     │└──> 0x100004550      adrp x8, 0x100008000
         0x100004554      add x0, x8, 0xaf1
         0x100004558      mov w8, 1
         0x10000455c      mov x1, 1
         0x100004560      and w2, w8, 1
         0x100004564      bl sym.imp._builtinStringLiteral.utf8CodeUnitCount.isASCII__String:_Builtin.Word__B_...cfC_ ; _builtinStringLiteral.utf8CodeUnitCount.isASCII__String: Builtin.Word, B(...cfC)
         0x100004568      stur x0, [x29, -0xa0]
         0x10000456c      stur x1, [x29, -0x98]
         0x100004570      ldur x8, [x29, -0xa8]
     │┌──< 0x100004574      cbz x8, 0x10000467c
    ┌────< 0x100004578      b 0x100004680
    ││││   ; CODE XREFS from func.100004280 @ 0x10000454c(x), 0x10000467c(x), 0x100004690(x)
    │└───> 0x10000457c      ldur x26, [x29, -0xa0]
     ││   0x100004580      ldur x21, [x29, -0x98]
     ││   0x100004584      mov x0, x19
     ││   0x100004588      bl sym.imp.swift_retain
     ││   0x10000458c      mov x0, 0x13
     ││   0x100004590      mov x1, 1
     ││   0x100004594      bl sym.imp.DefaultStringInterpolation.literalCapacity.interpolationCount_...itcfC_ ; DefaultStringInterpolation.literalCapacity.interpolationCount(...itcfC)
     ││   0x100004598      stur x0, [x29, -0xc0]
     ││   0x10000459c      stur x1, [x29, -0xb8]
     ││   0x1000045a0      adrp x8, 0x100008000
     ││   0x1000045a4      add x0, x8, 0xaf3                         ; 0x100008af3 ; "Transferring"
     ││   0x1000045a8      mov w8, 1
     ││   0x1000045ac      mov x1, 0xd
     ││   0x1000045b0      and w2, w8, 1

Evaluation

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

  • At 0x100004348, the handler calls URLComponents.init(url:resolvingAgainstBaseURL:) to parse the incoming URL.
  • At 0x10000434c, it calls URL.host to read the URL host, which represents the action, and compares it against the "transfer" string literal loaded at 0x1000043ac.
  • At 0x100004510, it calls URLQueryItem.value to extract the raw String? value from a query item.
  • At 0x100004538, a cbz instruction checks whether the value is nil. If nil, the fallback "0" string literal is loaded at 0x100004550. If non nil, the raw string value is used as is.
  • At 0x100004594, the value (either the raw string or the "0" fallback) is passed directly to DefaultStringInterpolation.init to build the "Transferring ... units" output string at 0x1000045a4.

Between URLQueryItem.value (0x100004510) and DefaultStringInterpolation (0x100004594) 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 URL query and uses it directly in the transfer related output, instead of validating that the value is numeric and within an expected range.

Exploitation

You can use xcrun to launch the app on a connected iOS device with an arbitrary custom URL scheme payload.

First, list the connected devices and copy the device identifier:

xcrun devicectl list devices

Then launch the app with a crafted mastgtest:// URL:

xcrun devicectl device process launch \
  --device <DEVICE_IDENTIFIER> \
  --payload-url "mastgtest://transfer?amount=9999999" \
  org.owasp.mastestapp.MASTestApp-iOS

After the app opens, tap Start in the demo app to process the stored URL. The result is observable in the app output:

Transferring 9999999 units

Repeat the command with a non-numeric value:

xcrun devicectl device process launch \
  --device <DEVICE_IDENTIFIER> \
  --payload-url "mastgtest://transfer?amount=not-a-number" \
  org.owasp.mastestapp.MASTestApp-iOS

After tapping Start, the app output shows:

Transferring not-a-number units

This confirms at runtime that the handler accepts attacker-controlled URL input and uses it directly in the transfer output without numeric conversion or bounds checking.