Web-to-App Communication: App Protocols

Note: This post is part of a series about Web-to-App Communication techniques.
Last updated: March 26, 2024

Just over eight years ago, I wrote my last blog post about App Protocols, a class of URL schemes that typically1 open another program on your computer instead of returning data to the web browser. A valid scheme name is an ASCII letter followed by one or more ASCII Letters, Digits, Hyphens, Dots, or Plus characters. (RFC7595 Guidelines).

App Protocols2 are both simple and powerful, allowing client app developers to easily enable the invocation of their apps from a website. For instance, ms-screenclip is a simple app protocol built into Windows 10 that kicks off the process of taking a screenshot:

    ms-screenclip:?delayInSeconds=2

When the user invokes this url, the handler waits two seconds, then launches its UI to collect a screenshot. Notably, App Protocols are fire-and-forgetmeaning that the handler has no direct way to return data back to the browser that invoked the protocol. App Protocols can be invoked from almost every browser, and many other surfaces (e.g. email clients, Start > Run, etc.)

The power and simplicity of App Protocols comes at a cost. They are the easiest route out of browser sandboxes and are thus terrifying, especially because this exploit vector is stable and available in every browser from legacy IE to the very latest versions of Chrome/Firefox/Edge/Safari.

What’s the Security Risk?

A number of issues make App Protocols especially risky from a security point-of-view.

Careless App Implementation

The primary security problem is that most App Protocols were designed to address a particular scenario (e.g. a “Meet Now” page on a videoconferencing vendor’s website should launch the videoconferencing client) and they were not designed with the expectation that the app could be exposed to potentially dangerous data from the web at large.

We’ve seen apps where the app will silently reconfigure itself (e.g. sending your outbound mail to a different server) based on parameters in the URL it receives. We’ve seen apps where the app will immediately create or delete files without first confirming the irreversible operation with the user. We’ve seen apps that assumed they’d never get more than 255 characters in their URLs and had buffer-overflows leading to Remote Code Execution when that limit was exceeded. We’ve seen apps that reboot the computer without prompt. The list goes on and on.

Poor API Contract

In most cases3, App Protocols are implemented as a simple mapping between the protocol scheme (e.g. alert) and a shell command in the registry, e.g. 

AlertProtocol

Formal details can be found on MSDN. Beyond the typical shell\open\command\ option for launch,

You can quickly find the handlers from the command prompt:

    reg query HKCR /f "URL Protocol" /s

…or using the URLProtocolView tool.

When the protocol is invoked by the browser, it simply bundles up the URL and passes it on the command line to the target application. The app doesn’t get any information about the caller (e.g. “What browser or app invoked this?“, “What origin invoked this?“, etc) and thus it must make any decisions solely on the basis of the received URL.

Until recently, there was an even bigger problem here, related to the encoding of the URL. Browsers, including Chrome, Edge, and IE, were willing to include bare spaces and quotation marks in the URL argument, meaning that an app could launch with a command line like:

alert.exe "alert:This is an Evil URL" --DoSomethingDangerous --Ignore=This"

The app’s code interpreted the –DoSomethingDangerous text as an argument, failed to recognize it as a part of the URL, and invoked dangerous functionality. This attack led to remote code execution bugs in App Protocol handlers many times over the years. 

Chrome began %-escaping spaces and quotation marks8 back in Chrome 64, and Edge 18 followed suit in Windows 10 RS5.

Chromium limits URLs to 2048 characters, but still shows the confirmation prompt for longer URLs.

You can see how your browser behaves using the links on this test page.

Future Opportunity: A richer API contract that allows an App Protocol handler to determine how specifically it was invoked would allow it to better protect itself from unexpected callers.

Moving the App Protocol URL data from the command line to somewhere else could help reduce the possibility of parsing errors. For example, Windows 10’s UWP architecture has an explicit URI Activation contract whereby an OnActivated function is called with the IActivatedEventArgsKind property set to ActivationKind.Protocol and the URI passed in that args’ Uri.AbsoluteUri property.

Sandbox Escape

The application that handles the protocol typically runs outside of the browser’s sandbox. This means that a security vulnerability in the app can be exploited to steal or corrupt any data the user can access, install malware, etc. If the browser is running Elevated (at Administrator), any App Protocol handlers it invokes are launched Elevated; this is part of UAC’s design.

Because most apps are written in native code, the result is that most protocol handlers end up in the DOOM! portion of this diagram:

RuleOfTwo

Prompting

In most cases, the only4 thing standing between the user and disaster is a permission prompt.

In Internet Explorer (or a Web Browser Control host application with FEATURE_SHOW_APP_PROTOCOL_WARN_DIALOG enabled), the prompt looked like this:

IEPermission


As you can see, the dialog provides a bunch of context about what’s happening, including the raw URL, the name of the handler, and a remark that allowing the launch may harm the computer.

Such information is lacking in more modern browsers, including Firefox:

FirefoxPermission

…and Edge/Chrome:

ChromePermission

Browser UI designers (reasonably) argue that the vast majority of users are poorly equipped to make trust decisions, even with the information in the IE prompt, and so modern UI has been greatly simplified5

Unfortunately, lost to evolution is the crucial indication to the user that they’re even making a trust decision at all [Issue].

Update: I wrote a whole post about prompting for AppProtocol launches.

In contrast to browsers, Microsoft Office offers a prompt which makes it clear that a security decision is being made, although it’s still unclear whether a user is equipped to make the correct decision.

Eliminating Prompts

Making matters more dangerous, everyone hates security prompts that interrupt non-malicious scenarios. A common user request can be summarized as “Prompt me only when something bad is going to happen. In fact, in those cases, don’t even prompt me, just protect me.

Unfortunately, the browser cannot know whether a given App Protocol invocation is good or evil, so it delegates control of prompting in two ways:

In Internet Explorer and Edge Legacy (version <= 18), the browser respects a per-protocol WarnOnOpen boolean in the registry, such that the App itself may tell the browser: “No worries, let anyone launch me, no prompt needed.

In Firefox, Chrome, and Edge (version >= 76), the browser instead allows the user to suppress further prompts with an “Always open links of this type in the associated app” checkbox.

If the user selects this option, the protocol will silently launch in the future without the browser first asking permission.

However, Edge/Chrome version 77.0.3864 removed the “Always open these types of links in the associated app” checkbox.

NoCheckbox


The stated reason for the removal is found in Chrome issue #982341:

No obvious way to undo “Always open these types of links” decision for External Protocols.

We realized in a conversation around issue 951540 that we don’t have settings UI
that allows users to reconsider decisions they’ve made around external protocol
support. Until we work that out, and make longer-term decisions about the
permission model around the feature generally, we should stop making the problem
worse by removing that checkbox from the UI.

A user who had ticked the “Always open” box has no way to later review that decision6, and no obvious way to reverse it. Almost no one figured out that using the “Clear Browsing Data > Cookies and other site data” dialog box option directs Chrome to delete all previous “Always open” decisions for the user’s profile. 

Particularly confusing is that the “Always open” decision wasn’t made on a per-site basis– it applies to every site visited by the user in that browser profile.

Update 1 of 2An Enterprise policy for v79+ allows administrators to restore the checkbox. End users can import this registry script.

Future Opportunity: Much of the risk inherent in open-without-prompting behavior comes from the site that any random site (http://evil.example.com) can abuse ambient permission to launch the protocol handler. If browsers changed the option to “Always allow this site to open this protocol”, the risk would be significantly reduced, and a user could reasonably safely allow, e.g. https://teams.microsoft.com to open the msteams protocol without a prompt.

Update 2 of 2: Microsoft Edge 82 introduced a ‘Allow Site/Scheme’ checkbox, and Edge 85 added a policy to allow IT Admins the same level of control.

Alternatively, perhaps the Registry-based provisioning of a protocol handler should explicitly list the sites allowed to launch the protocol, akin to the SiteLock protection for legacy ActiveX controls.

For some schemes7 , Chrome will not even show a prompt because the protocol is included on a built-in allow or deny list.

Some security folks have argued that browsers should not provide any mechanism for skipping the permission prompt. Unfortunately, there’s evidence to suggest that such a firm stance might result in vendors avoiding the prompt by choosing even riskier architectures for Web-to-App communication. More on this in a future post.

Flood Prevention – User Gesture Requirement

Most browsers contain one additional protection against abuse– the requirement that the user interact with a page before an App Protocol may be invoked. If no user-gesture is present (and neither of two browser policies have disabled the requirement), the invocation is silently blocked with only a DevTools console message: Not allowed to launch because a user gesture is required revealing what happened:

Currently, flood prevention in Chromium does not work very well, suffering both false positives and false negatives. The problem is that the “allow” state is implemented as a simple boolean per browser (not per-markup).

Some flows you might reasonably expect to be allowed (e.g. launching https://webdbg.com/test/protocol/ twice from your OS shell) are only allowed once per browser instance until the user interacts with the browser.

The AutoLaunchProtocolsFromOrigins policy and the user-visible “Always allow example.com to open links of this type” checkbox bypass only the pre-launch prompt and do not bypass the preceding flood prevention check. To bypass the gesture requirement, an Edge Policy is available, but you must carefully consider the security implications before using that policy: An allow-listed site could open an unlimited number of instances of the handler app on the client without user recourse.

Best Practices for Web Developers

A common complaint from Web Developers is that their Application Protocols are not launching as expected from browsers. Often, the problem is intermittent, owing to different configurations of user’s browsers (e.g. with extensions or without) or systems (user forgot to install the native app that installed the URL protocol).

To that end, it is important to follow best-practices when building a web page to launch application protocols (for example, the Microsoft Teams Join Meeting page).

Specifically, any page that attempts to launch an external handler should:

  1. Do so as a result of an explicit user action (e.g. a click on a link or button in the page)
  2. Offer the user an explanation of the expected behavior (“We’re trying to launch the app now.“)
  3. Offer an option to retry (e.g. “Your application should launch now. If it does not, click here.”)
  4. Perform the navigation from the top-level page (and definitely NOT from a sandboxed iframe)

By following these rules, you can build an understandable launch experience that will work properly for your users.

Zero-Day Defense

Even when a zero day vulnerability in an App Protocol handler is getting exploited in the wild (e.g. this one), browsers have few defenses available to protect users. 

Unlike, say, file downloads, where the browser has multiple mechanisms to protect users against newly-discovered threats (e.g. file type policies and SmartScreen/SafeBrowsing), browsers do not presently have rapid update mechanisms that can block access to known vulnerable App Protocol handlers.

Future Opportunity: Use SafeBrowsing/SmartScreen or a file-type-policies style Component Update to supply the client with a list of known-vulnerable protocol handlers. If a page attempts to invoke such a protocol, either block it entirely or strongly caution the user.

Update: Guess what landed for Edge 96? ^ That.

To improve the experience even further, the blocklist could contain version information such that blocking/additional warnings would only be shown if the version of the handler app is earlier than the version number of the app containing the fix. 

Antivirus programs typically do monitor all calls to CreateProcess and could conceivably protect against malicious invocation of app protocol handlers, but I am not aware of any having ever done so.

IT Administrators can block users from launching protocols by listing them as rules in URLBlocklist policy:

REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge\URLBlockList" /v "1" /t REG_SZ /d "exampleBlocked:*" /f

Today, Edge does not offer a trivial mechanism to prevent the launch of all Application Protocols; if you don’t want to block them individually, you could set a URLBlocklist rule of * and then use the URLAllowlist to allow http:* https:* blob:* about:* and possibly edge:* extension:* mailto:* depending on your needs.

Privacy Concerns (Try To) Prevent Protocol Detection

One of the most common challenges for developers who want to use App Protocols for Web-to-App communication is that the web platform does not expose the list of available protocol handlers to JavaScript. This is primarily a privacy consideration: exposing the list of protocol handlers to the web would expose a significant amount of fingerprintable entropy and might even reveal things about the user’s interests and beliefs (e.g. a ConservativeNews App or a LGBTQ App might expose a protocol handler for app-to-app communication). In May 2021, the FingerprintJS folks put together a practical exploit. It’s also something of a security measure– knowing what software is installed on a client provides information about what an attacker might target on that system.

Internet Explorer and Edge <= 18 supply a non-standard JavaScript function msLaunchUri that allows a web page to detect that a user didn’t have a to-be-invoked protocol handler installed, but this function is not available in other browsers; sometimes Web Developers try unreliable hacks.

Back in the old old days (pre-2010), a common workaround was to have installers that installed a protocol handler also update the Internet Explorer User-Agent string or Version Vector information; a page could check for the flag before attempting to launch the protocol. These hacks are not available in modern browsers (although you could mimic them with a modern browser extension if you really wanted. But if you’re willing to do that, you could also forgo the protocol handler approach in favor of Native Messaging).

UX When a Protocol Isn’t Installed

Browser behavior varies if the user attempts to invoke a link with a scheme for which no protocol handler is registered.

Firefox shows an error page:

FirefoxNotInstalled

On Windows 8 and later, IE and Edge Legacy show a prompt that offers to take the user to the Microsoft Store to search for a protocol handler for the target scheme:

Win10NotInstalled

Unfortunately, this search is rarely fruitful because most apps are not available in the Microsoft Store.

Interestingly, Chrome and Edge76+ show nothing to the user at all when attempting to invoke a link for which no protocol handler is installed, in part to avoid leaking the non-existence of the protocol (as noted, a privacy concern).

Debugging Launches

In Chrome 85, I added logging to the Chromium Developer Tools to record when a Application Protocol launches or is blocked:

ConsoleLogging

Upcoming change – Require HTTPS to Invoke

Chrome is considering requiring that a page be served over HTTPS (“a Secure Context”) in order for it to invoke an application protocol.

In future posts, I’ll explore some other alternatives for Web-to-App communication.

-Eric


Notes

1 In some browsers, it’s possible to register web-based handlers for “AppProtocols” (e.g. maps: and mailto: might go to Google Maps and GMail respectively). This mechanism is currently little-used.

2 Within Chromium, App Protocols are called “External Protocols” or “External Handlers.” Igalia’s engineer did an awesome writeup on the new-for-2022 architecture.

3 There are other ways to launch protocol schemes, including COM and the Windows 10 App Model’s URI Activation mechanism, but these are uncommon.

4 As an anti-abuse mechanism, the browser may require a user-gesture (e.g. a mouse click) before attempting to launch an App Protocol, and may throttle invocations to avoid spamming the user with an infinite stream of prompts. This is discussed in the Flood Prevention section of this post.

5 Chrome’s prompt used to look much like IE’s.

6 Short of opening the Preferences for the profile in Notepad or another text editor. E.g. after choosing “Always open” for Microsoft Teams and Skype for Business, the JSON file %localappdata%\Microsoft\Edge SxS\user Data\default\preferences contains:

"protocol_handler":{"excluded_schemes":{"msteams":false, "ms-sfb": false}}

To see the list in IE/Edge<=18, you can run a registry query to find protocols with WarnOnOpen set to 0:

reg query "HKCU\SOFTWARE\Microsoft\Internet Explorer\ProtocolExecute" /s
reg query "HKLM\SOFTWARE\Microsoft\Internet Explorer\ProtocolExecute" /s

7 Hardcoded schemes:

kDeniedSchemes[] = {“afp”,”data”,”disk”,”disks”,”file”,”hcp”,”ie.http”,”javascript”,”ms-help”,”nntp”,”res”,”shell”,”vbscript”,”view-source”,”vnd.ms.radio”}
kAllowedSchemes[] = {“mailto”, “news”, “snews”};

Firefox maintains a different block list:

hcp,vbscript,javascript,data,ie.http,iehistory,ierss,mk,ms-cxh,ms-cxh-full,ms-help,ms-msdt,res,search,search-ms,shell,

vnd.ms.radio,help,disk,disks,afp,moz-icon,firefox,firefox-private,ttp,htp,ttps,tps,ps,htps,ile,le

The EscapeExternalHandlerValue function:

// Escapes characters in text suitable for use as an external protocol handler command.
// We %XX everything except alphanumerics and -_.!~*'() and the restricted
// characters (;/?:@&=+$,#[]) and a valid percent escape sequence (%XX). EscapeExternalHandlerValue()

 

Published by ericlaw

Impatient optimist. Dad. Author/speaker. Created Fiddler & SlickRun. PM @ Microsoft 2001-2012, and 2018-, working on Office, IE, and Edge. Now a GPM for Microsoft Defender. My words are my own, I do not speak for any other entity.

4 thoughts on “Web-to-App Communication: App Protocols

  1. Great post thank you!

    The discussion of what and how to ask people reminds me of work we did on NEAT (Necessary, Explained, Actionable, Tested) as guidance for usable security, and I think a lot of the tension here is that these are habitually “click ok to get your job done.”

    It seems that extending the protocol handler protocol to protect apps could also be useful – “register a protocol handler” could by default take a text argument of [a-z0-9] of up to 32 bytes; ‘register a protocol handler that accepts a URL” could get a RFC-compliant* URL handed to it, and “register a super-insecure protocol handler” could take any input the web wants to throw.

    * Yes, I remember that the RFCs are crazy permissive in what’s acceptable. The protocol handler code could trim that way down.

  2. Thank you, Eric. I went crazy searching through Edge Chromium settings to allow callTo: protocols to always allow Zoom as the handler without prompting. I had a previous version of Chrome installed where I was allowed to check the “Always open these types of links,” but after installing Edge Chromium, I couldn’t figure out why it works in Chrome but not Edge.

  3. Eric, thanks for the #prompting section of this article. I was really frustrated that I couldn’t see what my browser was “really” doing to open a corporate application. Luckily, we haven’t quite gotten IE uninstalled from one of my computers yet, so I was able to dig in! Don’t know what I’m going to do when it’s gone.

Leave a comment