MASTG-TECH-0169: Opening Deep Links
To test how an iOS app handles deep links, you can open a URL through Apple tooling, through another app such as Safari or Notes, or through dynamic instrumentation. The best method depends on whether you are testing a simulator, a physical non jailbroken device, or a jailbroken device.
Using xcrun devicectl¶
Use xcrun devicectl to open a deep link on a physical device connected to your Mac. First list connected devices and copy the device identifier:
xcrun devicectl list devices
Then launch the target app with the URL payload:
xcrun devicectl device process launch \
--device 76557B40-8C76-5087-B9C5-F708C051911A \
--payload-url 'mastgtest://import?session=<payload>' \
org.owasp.mastestapp.MASTestApp-iOS
Replace the device identifier, URL, and bundle identifier with the values for your target.
Using uiopen¶
On a jailbroken device shell (see Accessing the Device Shell), you can use uiopen to ask iOS to open the URL directly:
uiopen 'mastgtest://import?session=<payload>'
This is useful when you are already working from the device shell and want a quick way to trigger the app's URL handler.
Using Safari¶
You can also open deep links from Safari. This is useful to test behavior that is closer to a user initiated flow.
For example, entering the following URL in Safari opens the system phone flow:
tel://123456789
iOS shows a confirmation prompt before dialing. If the user confirms, the Phone app starts the call.
For custom URL schemes, enter or navigate to a URL such as:
mastgtest://import?session=<payload>
If an installed app has registered the scheme, iOS can offer to open the URL with that app.
Using the Notes app¶
You can paste deep links into the Notes app and open them from there. Exit editing mode first, then tap or long press the link.
mastgtest://import?session=<payload>
This is a simple way to test user initiated deep links without writing a webpage. If the URL is not recognized as a link, check that the target app is installed, that the scheme is registered, and that the URL is properly encoded.
Triggering Universal Links¶
Universal links ( Universal Links) use https:// URLs and are routed to the app only after iOS has verified the domain against the app's Apple App Site Association file. This makes them harder to trigger than custom URL schemes:
- Safari: typing a universal link in the address bar does not open the app. You must follow an existing link on a web page so iOS treats it as a navigation.
- Notes app: paste the
https://link, leave editing mode, then long press it and choose the option to open it in the app (a single tap may open it in Safari instead, and the chosen option becomes the default for later taps). xcrun devicectl: pass anhttps://URL to--payload-urlto open it on a connected device, for example--payload-url 'https://www.example.com/transfer?amount=9999999'. The app opens only if its associated domain is verified on the device.
If the domain is not verified on your test device (for example, because the Apple App Site Association file is not reachable from your build), you can still exercise the handler with Frida (iOS) by constructing an NSUserActivity with an activityType of NSUserActivityTypeBrowsingWeb and a crafted webpageURL, then invoking the app's continuation entry point. This is useful for fuzzing the path and query parameters without depending on live domain verification.
function triggerUniversalLink(urlString) {
var NSUserActivity = ObjC.classes.NSUserActivity;
var NSURL = ObjC.classes.NSURL;
var activity = NSUserActivity.alloc().initWithActivityType_("NSUserActivityTypeBrowsingWeb");
activity.setWebpageURL_(NSURL.URLWithString_(urlString));
var scenes = ObjC.classes.UIApplication.sharedApplication()
.connectedScenes().allObjects();
for (var i = 0; i < scenes.count(); i++) {
var scene = scenes.objectAtIndex_(i);
var delegate = scene.delegate();
if (delegate !== null &&
delegate.respondsToSelector_(ObjC.selector("scene:continueUserActivity:"))) {
delegate.scene_continueUserActivity_(scene, activity);
return true;
}
}
return false;
}
triggerUniversalLink("https://example.com/transfer?amount=9999999");
This calls scene:continueUserActivity: directly on the UIWindowSceneDelegate, which is the UIKit entry point that SwiftUI's .onContinueUserActivity modifier hooks into. It bypasses OS-level domain verification because the OS only verifies the domain when routing a link — calling the delegate method directly skips that step entirely.
Using Frida (iOS)¶
If you are instrumenting the device with Frida, you can also trigger a URL programmatically. This is useful during dynamic analysis, especially when testing many payloads.
The following example runs inside the target app process and asks UIApplication to open the URL:
function openURL(url) {
const UIApplication = ObjC.classes.UIApplication.sharedApplication();
const NSURL = ObjC.classes.NSURL;
const toOpen = NSURL.URLWithString_(url);
return UIApplication.openURL_(toOpen);
}
openURL("mastgtest://import?session=<payload>");
openURL: is deprecated for app development, but it is still commonly seen in instrumentation snippets because it is short and easy to call from Frida. When writing app code, prefer openURL:options:completionHandler: or the Swift equivalent open(_:options:completionHandler:).
Another common dynamic analysis approach is to run the Frida script in SpringBoard and call the non public LSApplicationWorkspace API:
function openURL(url) {
const workspace = ObjC.classes.LSApplicationWorkspace.defaultWorkspace();
const toOpen = ObjC.classes.NSURL.URLWithString_(url);
return workspace.openSensitiveURL_withOptions_(toOpen, null);
}
openURL("mastgtest://import?session=<payload>");
This style is useful for black box URL scheme testing because SpringBoard is responsible for dispatching the URL to the registered app.
Note
LSApplicationWorkspace is a non-public API. Do not use it in App Store apps. For security testing and dynamic analysis on a test device, it can be useful to trigger URL handling paths that are otherwise difficult to exercise repeatedly.
Tests¶
MASTG-TEST-0370: Missing Input Validation in Custom URL Scheme Handlers
Demos¶
MASTG-DEMO-0149: References to Object Deserialization of a URL Scheme Payload with r2 MASTG-DEMO-0153: Universal Link Handler Without Input Validation