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
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Gain Privileges or Assume Identity - Attackers can access functionality inadvertently exposed through URL schemes, gaining privileges intended for authorized callers. |
| Access Control | Scope: Access Control Bypass Protection Mechanism - URL scheme handlers may bypass normal authentication or authorization flows. |
| Integrity | Scope: 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.
Related CWEs
- 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
- MITRE Corporation. "CWE-939: Improper Authorization in Handler for Custom URL Scheme." https://cwe.mitre.org/data/definitions/939.html
- Apple Developer Documentation. "Defining a Custom URL Scheme for Your App."
- Android Developers. "Create Deep Links to App Content."