iOS Custom URI Handlers / Deeplinks / Custom Schemes

Custom URL schemes allow apps to communicate via a custom protocol. An app must declare support for the schemes and handle incoming URLs that use those schemes.

URL schemes offer a potential attack vector into your app, so make sure to validate all URL parameters and discard any malformed URLs. In addition, limit the available actions to those that do not risk the user’s data.

For example, the URI: myapp://hostname?data=123876123 will invoke the application mydata (the one that has register the scheme mydata) to the action related to the hostname hostname sending the parameter data with value 123876123

One vulnerable example is the following bug in the Skype Mobile app, discovered in 2010: The Skype app registered the skype:// protocol handler, which allowed other apps to trigger calls to other Skype users and phone numbers. Unfortunately, Skype didn't ask users for permission before placing the calls, so any app could call arbitrary numbers without the user's knowledge. Attackers exploited this vulnerability by putting an invisible <iframe src="skype://xxx?call"></iframe> (where xxx was replaced by a premium number), so any Skype user who inadvertently visited a malicious website called the premium number.

You can find the schemes registered by an application in the app's Info.plist file searching for CFBundleURLTypes (example from iGoat-Swift):


However, note that malicious applications can re-register URIs already registered by applications. So, if you are sending sensitive information via URIs (myapp://hostname?password=123456) a malicious application can intercept the URI with the sensitive information.

Also, the input of these URIs should be checked and sanitised, as it can be coming from malicious origins trying to exploit SQLInjections, XSS, CSRF, Path Traversals, or other possible vulnerabilities.

Application Query Schemes Registration

Apps can call canOpenURL: to verify that the target app is available. However, as this method was being used by malicious app as a way to enumerate installed apps, from iOS 9.0 the URL schemes passed to it must be also declared by adding the LSApplicationQueriesSchemes key to the app's Info.plist file and an array of up to 50 URL schemes.


canOpenURL will always return NO for undeclared schemes, whether or not an appropriate app is installed. However, this restriction only applies to canOpenURL.

Testing URL Handling and Validation

In order to determine how a URL path is built and validated, if you have the original source code, you can search for the following methods:

  • application:didFinishLaunchingWithOptions: method or application:will-FinishLaunchingWithOptions:: verify how the decision is made and how the information about the URL is retrieved.

  • application:openURL:options:: verify how the resource is being opened, i.e. how the data is being parsed, verify the options, especially if access by the calling app (sourceApplication) should be allowed or denied. The app might also need user permission when using the custom URL scheme.

In Telegram you will find four different methods being used:

func application(_ application: UIApplication, open url: URL, sourceApplication: String?) -> Bool {
    self.openUrl(url: url)
    return true

func application(_ application: UIApplication, open url: URL, sourceApplication: String?,
annotation: Any) -> Bool {
    self.openUrl(url: url)
    return true

func application(_ app: UIApplication, open url: URL,
options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    self.openUrl(url: url)
    return true

func application(_ application: UIApplication, handleOpen url: URL) -> Bool {
    self.openUrl(url: url)
    return true

Testing URL Requests to Other Apps

The method openURL:options:completionHandler: and the deprecated openURL: method of UIApplication are responsible for opening URLs (i.e. to send requests / make queries to other apps) that may be local to the current app or it may be one that must be provided by a different app. If you have the original source code you can search directly for usages of those methods.

Additionally, if you are interested into knowing if the app is querying specific services or apps, and if the app is well-known, you can also search for common URL schemes online and include them in your greps (list of iOS app schemes).

egrep -nr "open.*options.*completionHandler" ./Telegram-iOS/
egrep -nr "openURL\(" ./Telegram-iOS/
egrep -nr "mt-encrypted-file://" ./Telegram-iOS/
egrep -nr "://" ./Telegram-iOS/

Testing for Deprecated Methods

Search for deprecated methods like:

For example, here we find those three:

$ rabin2 -zzq Telegram\\ X | grep -i "openurl"

0x1000d9e90 31 30 UIApplicationOpenURLOptionsKey
0x1000dee3f 50 49 application:openURL:sourceApplication:annotation:
0x1000dee71 29 28 application:openURL:options:
0x1000dee8e 27 26 application:handleOpenURL:
0x1000df2c9 9 8 openURL:
0x1000df766 12 11 canOpenURL:
0x1000df772 35 34 openURL:options:completionHandler:

Calling arbitrary URLs

  • Safari: To quickly test one URL scheme you can open the URLs on Safari and observe how the app behaves. For example, if you write tel://123456789 safari will try to start calling the number.

  • Notes App: Long press the links you've written in order to test custom URL schemes. Remember to exit the editing mode in order to be able to open them. Note that you can click or long press links including custom URL schemes only if the app is installed, if not they won't be highlighted as clickable links.

  • IDB:

    • Start IDB, connect to your device and select the target app. You can find details in the IDB documentation.

    • Go to the URL Handlers section. In URL schemes, click Refresh, and on the left you'll find a list of all custom schemes defined in the app being tested. You can load these schemes by clicking Open, on the right side. By simply opening a blank URI scheme (e.g., opening myURLscheme://), you can discover hidden functionality (e.g., a debug window) and bypass local authentication.

  • Frida:

    If you simply want to open the URL scheme you can do it using Frida:

    $ frida -U iGoat-Swift
    [iPhone::iGoat-Swift]-> function openURL(url) {
                                var UIApplication = ObjC.classes.UIApplication.sharedApplication();
                                var toOpen = ObjC.classes.NSURL.URLWithString_(url);
                                return UIApplication.openURL_(toOpen);
    [iPhone::iGoat-Swift]-> openURL("tel://234234234")

    In this example from Frida CodeShare the author uses the non-public API LSApplicationWorkspace.openSensitiveURL:withOptions: to open the URLs (from the SpringBoard app):

    function openURL(url) {
        var w = ObjC.classes.LSApplicationWorkspace.defaultWorkspace();
        var toOpen = ObjC.classes.NSURL.URLWithString_(url);
        return w.openSensitiveURL_withOptions_(toOpen, null);

    Note that the use of non-public APIs is not permitted on the App Store, that's why we don't even test these but we are allowed to use them for our dynamic analysis.

Fuzzing URL Schemes

If the app parses parts of the URL, you can also perform input fuzzing to detect memory corruption bugs.

What we have learned above can be now used to build your own fuzzer on the language of your choice, e.g. in Python and call the openURL using Frida's RPC. That fuzzer should do the following:

  • Generate payloads.

  • For each of them call openURL.

  • Check if the app generates a crash report (.ips) in /private/var/mobile/Library/Logs/CrashReporter.

The FuzzDB project offers fuzzing dictionaries that you can use as payloads.

Fuzzing Using Frida

Doing this with Frida is pretty easy, you can refer to this blog post to see an example that fuzzes the iGoat-Swift app (working on iOS 11.1.2).

Before running the fuzzer we need the URL schemes as inputs. From the static analysis we know that the iGoat-Swift app supports the following URL scheme and parameters: iGoat://?contactNumber={0}&message={0}.

$ frida -U SpringBoard -l ios-url-scheme-fuzzing.js
[iPhone::SpringBoard]-> fuzz("iGoat", "iGoat://?contactNumber={0}&message={0}")
Watching for crashes from iGoat...
No logs were moved.
Opened URL: iGoat://?contactNumber=0&message=0


Last updated