mime

Background

Typically, if you want your website to send a document to a client application, you simply send the file as a download. Your server indicates that a file should be treated as a download in one of a few simple ways:

  • Specifying a nonwebby type in the Content-Type response header.
  • Sending a Content-Disposition: attachment; filename=whatever.ext response header.
  • Setting a download attribute on the hyperlink pointing to the file.

These approaches are well-supported across browsers (via headers for decades, via the download attribute anywhere but IE since 2016).

The Trouble with Plain Downloads

However, there’s a downside to traditional downloads — unless the file itself contains the URL from which the download originated, the client application will not typically know where the file originated, which can be a problem for:

  • Security – “Do I trust the source of this file?
  • Functionality – “If the user makes a change to this file, to where should I save changes back?“, and
  • Performance – “If the user already had a copy of this 60mb slide deck, maybe skip downloading it again over our expensive trans-Pacific link?

Maybe AppProtocols?

Rather than sending a file download, a solution developer might instead just invoke a target application using an App Protocol. For instance, the Microsoft Office clients might support a syntax like:

ms-word:ofe|u|https://example.com/docx.docx

…which directs Microsoft Word to download the document from example.com.

However, the AppProtocol approach has a shortcoming– if the user doesn’t happen to have Microsoft Word installed, the protocol handler will fail to launch and either nothing will happen or the user may get a potentially confusing error message. That brokenness will occur even if they happen to have another client (e.g. WordPad) that could handle the document.

DirectInvoke

To address these shortcomings, we need a way to instruct the browser: “Download this file, unless the client’s handler application would prefer to just get its URL.”

While a poorly-documented precursor technology existed as early as the 1990s, Windows 8 reintroduced this feature as DirectInvoke. When a client application registers itself indicating that it supports receiving URLs rather than local filenames, and when the server indicates that it would like to DirectInvoke the application using the X-MS-InvokeApp response header:

DirectInvoke

…then the download stream is aborted and the user is instead presented with a confirmation prompt:

UIPrompt

If the user accepts the prompt, the handler application is launched, passing the URL to the web content.

Now, for certain types, the server doesn’t even need to ask for DirectInvoke behavior via the X-MS-InvokeApp header. The FTA_AlwaysUseDirectInvoke bit can be set in the type’s EditFlags registry value. The bit is documented on MSDN as: 

FTA_AlwaysUseDirectInvoke 0x00400000
Introduced in Windows 8. Ensures that the verbs for the file type are invoked with a URL instead of a downloaded version of the file. Use this flag only if you’ve registered the file type’s verb to support DirectInvoke through the SupportedProtocols or UseUrl registration.

Microsoft’s ClickOnce deployment technology makes use of the FTA_AlwaysUseDirectInvoke flag.

A sample registry script for a type that should always be DirectInvoke’d might look like this:

To test it, first set up the registry, install a handler to C:\Windows, and then click this example link

TraditionalVsDI.png

Caveats

In order for this architecture to work reliably, you need to ensure a few things.

App Should Handle Traditional Files

First, your application needs to have some reasonable experience if the content is handled as a traditional download, as it would be using Chrome or Firefox, or on a non-Windows operating system.

By way of example, it’s usually possible to construct a ClickOnce manifest that works correctly after download. Similarly, Office applications work fine with regular files, although the user must take care to reupload the files after making any edits.

App Should Avoid Depending On Browser State

If your download flow requires a cookie, the client application will not have access to that cookie and the download will fail. The client application probably will not be able to prompt the user to login to otherwise retrieve the file.

If your download flow requires HTTP Authentication or HTTPS Client Certificate Authentication, the client application might work (if it supports NTLM/Negotiate) or it might not (e.g. if the server requires Digest Auth and the client cannot show a credential prompt.

App Should Ensure URL Support

Many client applications have limits in the sorts of URLs that they can support. For instance, the latest version of Microsoft Excel cannot handle a URL longer than 260 characters. If a .xlsx download from SharePoint site attempts to DirectInvoke, Excel will launch and complain that it cannot retrieve the file.

App Should Ensure Network Protocol Support

Similarly, if the client app registers for DirectInvoke of HTTPS URLs, you should ensure that it supports the same protocols as the browser. If a server requires a protocol version (e.g. TLS/1.2) that the client hasn’t yet enabled (say it only enables TLS/1.0), then the download will fail.

Server Must Not Send |Content-Disposition: attachment|

As noted in the documentation, a Content-Disposition: attachment response header takes precedence over DirectInvoke behavior. If a server specifies attachment, DirectInvoke will not be used.

Note: If you wish to use a Content-Disposition header to name the file, you can do so using Content-Disposition: inline; filename=”fuzzle.fuzzle”

Conclusion

As you can see, there’s quite a long list of caveats around using the DirectInvoke WebToApp communication scheme, but it’s still a useful option for some scenarios.

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

-Eric

Note: The current version of Edge 79 does not yet support FTA_AlwaysUseDirectInvoke. We expect to fix this in the future.

Note: I have a few test cases.

 

As we rebuild Microsoft Edge atop the Chromium open-source platform, we are working through various scenarios that behave differently in the new browser. In most cases, such scenarios also worked differently between 2018’s Edge (aka “Spartan”) and Chrome, but users either weren’t aware of the difference (because they used Trident-derived browsers inside their enterprise) or were aware and simply switched to a Microsoft-browser for certain tasks.

One example of a behavioral gap is related to running ClickOnce apps. Update: ClickOnce support is now available in Edge 77+. See the end of this post.

ClickOnce is a Microsoft application deployment framework that aims to allow installation of native-code applications from the web in (around) one click.

Chrome and Firefox can successfully install and launch ClickOnce’s .application files if the .application file specifies a deploymentProvider element with a codebase attribute (example):

InstallPrompt

Installation prompt when opening an .application file.

However, it’s also possible to author and deploy an .application that doesn’t specify a deploymentProvider element (example). Such files launch correctly from Internet Explorer and pre-Chromium Edge, but fail in Firefox and Chrome with an error message:

ApplicationCannotBeStarted

ClickOnce fails for a downloaded .application file.

So, what gives? Why does this scenario magically work in Edge Spartan but not Firefox or Chrome?

The secret can be found in the EditFlags for the Application.Manifest ProgId (to which the .application filename extension and application/x-ms-application MIME type are mapped):

ApplicationManifestRegistry

Registry settings for the Application.Manifest ProgId.

The EditFlags contain the FTA_AlwaysUseDirectInvoke flag, which is documented on MSDN as 

FTA_AlwaysUseDirectInvoke 0x00400000
Introduced in Windows 8. Ensures that the verbs for the file type are invoked with a URL instead of a downloaded version of the file. Use this flag only if you’ve registered the file type’s verb to support DirectInvoke through the SupportedProtocols or UseUrl registration.

If you peek in the Application.Manifest’s Shell\Open\Command value, you’ll find that it calls for running the ShOpenVerbApplication function inside dfshim.dll, passing along the .application file’s path or URL in a parameter (%1):

“C:\Windows\System32\rundll32.exe” “C:\Windows\System32\dfshim.dll”,ShOpenVerbApplication %1

And therein lies the source of the behavioral difference.

When you download and open an Application.Manifest file from Edge Spartan, it passes the source URL for the .application to the handler. When you download the file in Firefox or Chrome, it passes the local file path of the downloaded .application file. With only the local file path, the ShOpenVerbApplication function doesn’t know how to resolve the relative references in the Application Manifest’s XML and the function bails out with the Cannot Start Application error message.

Setting FTA_AlwaysUseDirectInvoke also has the side-effect of removing the “Save” button from Edge’s download manager:

NoSave

…helping prevent the user from accidentally downloading an .application file that won’t work if opened outside of the browser from the Downloads folder (since the file’s original URL isn’t readily available to Windows Explorer).

Advice to Publishers

If you’re planning to distribute your ClickOnce application from a website, specify the URL in Visual Studio’s ClickOnce Publish Wizard:

Manifest

Specify “From a Web site” in the ClickOnce Publish Wizard.

This will ensure that even if DirectInvoke isn’t used, ShOpenVerbApplication can still find the files needed to install your application.

Workarounds

A company called Meta4 offers a Chrome browser extension that aims to add fuller support for ClickOnce to Chrome. The extension comes in two pieces– a traditional JavaScript extension and a trivial “native” executable (written in C#) that simply invokes the ShOpenVerbApplication call with the URL. The JavaScript extension launches and communicates with the native executable running outside of the Chrome sandbox using Native Messaging.

Unfortunately, the extension is a bit hacky– it installs a blocking onBeforeRequest handler which watches all requests (not just downloads), and if the target URL’s path component ends in .application, it invokes the native executable. Alas, it’s not really safe to make any assumptions about extensions in URLs (the web is based on MIME types, rather than filenames).

Edge 77+ Implementation

ClickOnce support is available in the new Edge 77+. It’s off-by-default, but can be enabled via edge://flags/#edge-click-once.

Note that the ClickOnce implementation in Edge 77+ will always prompt the user before the handler is invoked. In Edge 18/IE, sites in your Intranet/Trusted Zone could spawn the .application handler without any prompt. That’s because these older browsers respect the FTA_OpenIsSafe bit in the EditFlags for the application.manifest progid. The new Edge doesn’t really use Windows Security Zones as a primitive, and it thus does not support the FTA_OpenIsSafe bit.

 

Do you use ClickOnce to deploy your applications? If so, are you specifying the deployment URL in the manifest file?

-Eric

PS: Notably, Internet Explorer doesn’t rely upon the DirectInvoke mechanism; removing the EditFlags value entirely causes IE to show an additional prompt but the install still succeeds. That’s because IE activates the file using a MIME handler (see the CLSID subkey of Application.Manifest) much like it does for .ZIP files. The DirectInvoke mechanism was invented, in part, to replace the legacy MIME handler mechanism.

Over the 14 years that I’ve been working on browsers and the web platform, I’ve seen a lot of bugs where the client’s configuration causes a problem with a website.

By default, Windows maintains File Extension to Content Type and Content Type to File Extension mappings mappings in the registry. You can find the former mappings in subkeys named for each file extension, e.g. HKEY_CLASSES_ROOT\.ext, and the latter as subkeys under the HKEY_CLASSES_ROOT\MIME\Database\Content Type key:

PDFMapping

These mappings are how Internet Explorer, Edge, and other browsers know that a file delivered as Content-Type: application/pdf should be saved with a .pdf extension, and that a local file named example.html ought to be treated as Content-Type: text/html.

Unfortunately, these mappings are subject to manipulation by locally-installed software, which means you might find that installing Microsoft Excel causes your .CSV file upload to have a Content-Type of application/vnd.ms-excel instead of the text/csv your website was expecting.

Similarly, you might be surprised to discover that some popular file extensions do not have a MIME type registered by default on Windows. Perhaps the most popular of these is files in JavaScript Object Notation format; these generally should have the file extension .json and a MIME type of application/json but Windows treats these as an unknown type by default.

Today, I looked at a site which allows the user to upload a JSON file containing data exported from some other service. The upload process fails in Edge with an error saying that the file must be JSON. Looking at the script in the site, it contains the following:

validateFile = function(file) {
  if (file.type !== "application/json") // BUG BUG BUG
    { alert('That is not a valid json file.'); return; }

This function fails in Edge– the file.type attribute is the empty string because Windows has no mapping between .json and application/json.

This site usually works in Chrome because Chrome has a MIME-type determination system which first checks a fixed list of mappings, then, if no fixed mapping was found, consults the system registry, and finally, if the registry does not specify a MIME type for a given extension, Chrome consults a “fallback” list of mappings (kSecondaryMappings), and .JSON is in that final fallback list. However, even Chrome users would be broken if the file had the wrong extension (e.g. data.jso) or if the user’s registry contained a different mapping (e.g. .json=>”text/json”).

As a consequence, client JavaScript and server-side upload processing logic should be very skeptical of the MIME type contained in the file.type attribute or Content-Type header, as the MIME value reported could easily be incorrect by accident (or malice!).

-Eric Lawrence
PS: End users can workaround the problem with sites that expect particular MIME types for JSON by importing the following Registry Script (save the text as FixJSON.reg and double-click the file):

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\.json]
"Content Type"="application/json"

[HKEY_CLASSES_ROOT\MIME\Database\Content Type\application/json]
"Extension"=".json"

 

Every non-empty response from a web server should contain a Content-Type response header that declares the type of content contained in the response. This declaration helps the browser understand how to process the response and can help prevent a number of serious security vulnerabilities.

Setting this header properly is more important than ever.

The Old Days

Many years ago, an easy way to exploit a stored-XSS vulnerability on a web server that accepted file uploads was to simply upload a file containing a short HTML document with embedded JavaScript. You could then send potential victims a link to http://vulnerable.example.com/uploads/123/NotReallyA.jpeg and when the victim’s browser rendered the document, it would find the JavaScript and run it in the security context of vulnerable.example.com, allowing it to steal the contents of cookies and storage, reconfigure your account, rewrite pages, etc.

Sites caught on and started rejecting uploads that lacked the “magic bytes” indicating a JPEG/GIF/PNG at the start of the file. Unfortunately, browsers were so eager to render HTML that they would “sniff” the bytes of the file to see if they could find some HTML to render. Bad guys realized they could shove HTML+Script into metadata fields of the image binary, and the attack would still work. Ugh.

In later years, browsers got smarter and stopped sniffing HTML from files served with an image/ MIME type, and introduced a new response header:

  X-Content-Type-Options: nosniff

…that declared that a browser should not attempt to sniff HTML from a document at all.

Use of the nosniff directive was soon expanded to help prevent responses from being interpreted as CSS or JavaScript, because clever attackers figured out that the complicated nature of Same Origin Policy meant that an attacking page could execute a cross-origin response and use side-effects (e.g. the exception thrown when trying to parse a HTML document as JavaScript) to read secrets out of that cross-origin response.

Browser makers have long dreamed of demanding that a response declare Content-Type: application/javascript in order for the response to be treated as JavaScript, but unfortunately telemetry tells us that this would break a non-trivial number of pages. So for now, it’s important to continue sending X-Content-Type-Options: nosniff on responses to mitigate this threat.

The Modern World

Chrome’s security sandbox helps ensure that a compromised (e.g. due to a bug in V8 or Blink) Renderer Process cannot steal or overwrite data on your device. However, until recently, a renderer compromise was inherently a UXSS vector, allowing data theft from every website your browser can reach.

Nearly a decade ago, Microsoft Research proposed a browser with stronger isolation between web origins, but as the Security lead for Internet Explorer, I thought it hopelessly impractical given the nature of the web. Fast forward to 2017, and Chrome’s Site Isolation project has shipped after a large number of engineer-years of effort.

Site Isolation allows the browser to isolate sites from one another in different processes, allowing the higher-privilege Browser Process to deny resources and permissions to low-privilege Renderers that should not have access. Sites that have been isolated are less vulnerable to renderer compromises, because the compromised renderer cannot load protected resources into its own process.

Isolation remains tricky because of complex nature of Same Origin Policy, which allows a cross-origin response to Execute without being directly Read. To execute a response (e.g. render an image, run a JavaScript, load a frame), the renderer process must itself be able to read that response, but it’s forced to rely upon its own code to prevent JavaScript from reading the bytes of that response. To address this, Chrome’s Site Isolation project hosts cross-origin frames inside different processes, and (crucially) rejects the loading of cross-origin documents into inappropriate contexts. For instance, the Browser process should not allow a JSON file (lacking CORS headers) to be loaded by an IMG tag in a cross-origin frame, because this scenario isn’t one that a legitimate site could ever use. By keeping cross-site data out of the (potentially compromised) renderer process, the impact of an arbitrary-memory-read vulnerability is blunted.

Of course, for this to work, sites must correctly mark their resources with the correct Content-Type response header and a X-Content-Type-Options: nosniff directive. (See the latest guidance on Chromium.org)

When Site Isolation blocks a response, a notice is shown in the Developer Tools console:

IsolationMessage

Console Message: Blocked current origin from receiving cross-site document

The Very Modern World

You may have heard about the recent “speculative execution” attacks against modern processors, in which clever attackers are able to read memory to which they shouldn’t normally have access. A sufficiently clever attacker might be able to execute such an attack from JavaScript in the renderer and steal the memory from that process. Such an attack on the CPU’s behavior results in the same security impact as a renderer compromise, without the necessity of finding a bug in the Chrome code.

In a world where a malicious JavaScript can read any byte in the process memory, the renderer alone has no hope of enforcing “No Read” semantics. So we must rely upon the browser process to enforce isolation, and for that, browsers need the help of web developers.

You can read more about Chrome’s efforts to combat speculative execution attacks here.

Guidance: Serve Content Securely

If your site serves JSON or similar content that contains non-public data, it is absolutely crucial that you set a proper MIME type and declare that the content should not be sniffed. For example:

 Content-Type: application/json; charset=utf-8
 X-Content-Type-Options: nosniff

Of course, you’ll also want to ensure that any Access-Control-Allow-Origin response headers are set appropriately (lest an attacker just steal your document through the front door!).

 

Thanks for your help in securing the web!

-Eric