Improper Authorization in Handler for Custom URL Scheme

Description

Improper Authorization in Handler for Custom URL Scheme occurs when applications that handle custom URL schemes (such as myapp://, customscheme://) fail to properly restrict which actors can invoke them. Mobile platforms like iOS and Android use custom URL schemes to enable inter-application communication and deep linking. When handlers for these schemes don't verify the source of the request or lack proper authorization checks, any application or website can trigger potentially dangerous functionality by crafting URLs with the custom scheme.

Risk

This vulnerability can have severe consequences depending on the functionality exposed through the custom URL scheme. Attackers can invoke file modification or deletion operations, trigger sensitive actions without user consent, access internal APIs meant for specific trusted applications, cause data leakage by invoking export or sharing functions, trigger purchases or financial transactions, and modify application settings or user preferences. The attack is particularly dangerous because it can be initiated from any application on the device or from web pages the user visits.

Solution

Implement authorization checks in URL scheme handlers to verify the caller's identity and permissions. Use secure platform mechanisms for inter-app communication where possible. Require user confirmation for sensitive operations triggered via URL schemes. Validate and sanitize all parameters passed through URL schemes. Consider using universal links (iOS) or app links (Android) which provide more secure alternatives. Limit the functionality exposed through URL schemes to non-sensitive operations. Log URL scheme invocations for security monitoring.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Gain Privileges or Assume Identity - Attackers can access functionality inadvertently exposed through URL schemes, gaining privileges intended for authorized callers.
Access ControlScope: Access Control

Bypass Protection Mechanism - URL scheme handlers may bypass normal authentication or authorization flows.
IntegrityScope: Integrity

Modify Application Data - Handlers that modify data without authorization checks allow attackers to alter application state.

Example Code

Vulnerable Code

// Vulnerable: iOS URL scheme handler without authorization
@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
        openURL:(NSURL *)url
        options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {

    NSString *scheme = [url scheme];
    NSString *action = [url host];
    NSDictionary *params = [self parseQueryParams:[url query]];

    // Vulnerable: No verification of source, no authorization check
    if ([action isEqualToString:@"replaceFileText"]) {
        NSString *fileName = params[@"filename"];
        NSString *newText = params[@"newtext"];

        // Any app can modify files!
        [self replaceTextInFile:fileName withText:newText];
        return YES;
    }

    if ([action isEqualToString:@"deleteFile"]) {
        NSString *fileName = params[@"filename"];
        // Any app can delete files!
        [self deleteFile:fileName];
        return YES;
    }

    return NO;
}

@end

// Attack: Open URL "myapp://replaceFileText?filename=config.txt&newtext=malicious"
// Vulnerable: Android URL scheme handler without checks
public class VulnerableDeepLinkActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Intent intent = getIntent();
        Uri data = intent.getData();

        if (data != null && "myapp".equals(data.getScheme())) {
            String action = data.getHost();

            // Vulnerable: No authorization check
            if ("transferFunds".equals(action)) {
                String amount = data.getQueryParameter("amount");
                String recipient = data.getQueryParameter("to");
                // Any app can initiate transfers!
                transferFunds(amount, recipient);
            }

            if ("exportData".equals(action)) {
                String destination = data.getQueryParameter("dest");
                // Any app can export user data!
                exportAllDataTo(destination);
            }
        }
    }
}
// Vulnerable: WebView with unprotected JavaScript bridge
class VulnerableWebViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!

    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

        if let url = navigationAction.request.url,
           url.scheme == "myapp" {

            // Vulnerable: No source verification
            // Any website can invoke native functions
            handleCustomURL(url)
            decisionHandler(.cancel)
            return
        }

        decisionHandler(.allow)
    }

    func handleCustomURL(_ url: URL) {
        let action = url.host ?? ""

        // Vulnerable: Exposing native APIs to web content
        if action == "getContacts" {
            // Malicious website can steal contacts
            let contacts = fetchAllContacts()
            injectToWebView(contacts)
        }

        if action == "sendSMS" {
            // Malicious website can send SMS
            let number = url.queryParam("number")
            let message = url.queryParam("message")
            sendSMS(to: number, message: message)
        }
    }
}

Fixed Code

// Fixed: iOS URL scheme handler with authorization
@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
        openURL:(NSURL *)url
        options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {

    NSString *scheme = [url scheme];
    NSString *action = [url host];
    NSDictionary *params = [self parseQueryParams:[url query]];

    // Fixed: Get source application
    NSString *sourceApp = options[UIApplicationOpenURLOptionsSourceApplicationKey];

    // Fixed: Check if source is authorized
    if (![self isAuthorizedSource:sourceApp forAction:action]) {
        NSLog(@"Rejected URL from unauthorized source: %@", sourceApp);
        return NO;
    }

    if ([action isEqualToString:@"replaceFileText"]) {
        NSString *fileName = params[@"filename"];
        NSString *newText = params[@"newtext"];

        // Fixed: User confirmation required for sensitive operations
        [self confirmAction:@"Replace File Text"
                   message:[NSString stringWithFormat:@"Allow %@ to modify %@?", sourceApp, fileName]
                completion:^(BOOL confirmed) {
            if (confirmed) {
                [self replaceTextInFile:fileName withText:newText];
            }
        }];
        return YES;
    }

    return NO;
}

- (BOOL)isAuthorizedSource:(NSString *)sourceApp forAction:(NSString *)action {
    // Define authorized callers per action
    NSDictionary *authorizedApps = @{
        @"viewDocument": @[@"com.trusted.app1", @"com.trusted.app2"],
        @"replaceFileText": @[@"com.trusted.app1"],  // Only specific app
    };

    NSArray *allowedApps = authorizedApps[action];
    if (!allowedApps) {
        return NO;  // Unknown action
    }

    return [allowedApps containsObject:sourceApp];
}

- (void)confirmAction:(NSString *)title
              message:(NSString *)message
           completion:(void (^)(BOOL confirmed))completion {

    UIAlertController *alert = [UIAlertController
        alertControllerWithTitle:title
        message:message
        preferredStyle:UIAlertControllerStyleAlert];

    [alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
        style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
            completion(NO);
        }]];

    [alert addAction:[UIAlertAction actionWithTitle:@"Allow"
        style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
            completion(YES);
        }]];

    [self.window.rootViewController presentViewController:alert animated:YES completion:nil];
}

@end
// Fixed: Android URL scheme handler with authorization
public class FixedDeepLinkActivity extends Activity {

    private static final Set<String> TRUSTED_PACKAGES = new HashSet<>(Arrays.asList(
        "com.trusted.partner",
        "com.mycompany.otherapp"
    ));

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Intent intent = getIntent();
        Uri data = intent.getData();

        if (data == null || !"myapp".equals(data.getScheme())) {
            finish();
            return;
        }

        // Fixed: Get referring package
        String referrer = getReferrer() != null ? getReferrer().getHost() : null;

        String action = data.getHost();

        // Fixed: Check authorization
        if (!isActionAllowed(action, referrer)) {
            Log.w(TAG, "Unauthorized access attempt from: " + referrer);
            Toast.makeText(this, "Unauthorized access", Toast.LENGTH_SHORT).show();
            finish();
            return;
        }

        if ("transferFunds".equals(action)) {
            // Fixed: Show confirmation dialog
            String amount = data.getQueryParameter("amount");
            String recipient = data.getQueryParameter("to");
            confirmAndTransfer(amount, recipient, referrer);
        }

        if ("viewDocument".equals(action)) {
            // Read-only operations may not need confirmation
            String docId = data.getQueryParameter("id");
            viewDocument(docId);
        }
    }

    private boolean isActionAllowed(String action, String referrer) {
        // Define action permissions
        Map<String, Boolean> publicActions = new HashMap<>();
        publicActions.put("viewDocument", true);  // Public
        publicActions.put("transferFunds", false);  // Requires trusted source

        Boolean isPublic = publicActions.get(action);
        if (isPublic == null) {
            return false;  // Unknown action
        }

        if (isPublic) {
            return true;
        }

        // Private action - verify source
        return TRUSTED_PACKAGES.contains(referrer);
    }

    private void confirmAndTransfer(String amount, String recipient, String source) {
        new AlertDialog.Builder(this)
            .setTitle("Confirm Transfer")
            .setMessage("Transfer " + amount + " to " + recipient + "?\nRequested by: " + source)
            .setPositiveButton("Confirm", (dialog, which) -> {
                // Proceed with transfer
                transferFunds(amount, recipient);
            })
            .setNegativeButton("Cancel", null)
            .show();
    }
}
// Fixed: WebView with protected JavaScript bridge
class FixedWebViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!

    // Fixed: Whitelist of trusted domains
    private let trustedDomains = Set(["www.trustedsite.com", "api.trustedsite.com"])

    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

        if let url = navigationAction.request.url,
           url.scheme == "myapp" {

            // Fixed: Verify the source is trusted
            if let sourceURL = webView.url,
               let sourceHost = sourceURL.host,
               trustedDomains.contains(sourceHost) {

                handleCustomURL(url)
            } else {
                print("Rejected custom URL from untrusted source: \(webView.url?.host ?? "unknown")")
            }

            decisionHandler(.cancel)
            return
        }

        decisionHandler(.allow)
    }

    func handleCustomURL(_ url: URL) {
        let action = url.host ?? ""

        // Fixed: Only expose safe, limited functionality
        switch action {
        case "shareContent":
            // Read-only, user-visible action
            if let content = url.queryParam("content") {
                showShareSheet(for: content)
            }

        case "openSettings":
            // Opens app settings - safe
            openAppSettings()

        default:
            print("Unknown action: \(action)")
        }

        // Fixed: Sensitive operations removed from URL scheme handler
        // Use authenticated API calls instead
    }
}

CVE Examples

  • CVE-2013-5725: URL scheme handler allowed remote attackers to perform actions without user prompts.
  • CVE-2013-5726: URL scheme handler could force users into undesired behaviors.

  • CWE-862: Missing Authorization (parent)
  • CWE-940: Improper Verification of Source of a Communication Channel (parent)
  • CWE-925: Improper Verification of Intent by Broadcast Receiver (related)

References

  1. MITRE Corporation. "CWE-939: Improper Authorization in Handler for Custom URL Scheme." https://cwe.mitre.org/data/definitions/939.html
  2. Apple Developer Documentation. "Defining a Custom URL Scheme for Your App."
  3. Android Developers. "Create Deep Links to App Content."