Replacing addJavascriptInterface() with HTML Message Channels

One item in Google’s app security checklist caught my eye late last week:

WebViews do not use addJavaScriptInterface() with untrusted content.

On Android M and above, HTML message channels can be used instead.

I had glossed over HTML message channels back in my Android 6.0 “random musings” post, and I was somewhat at a loss as to how to use them to replace addJavascriptInterface().

Fortunately, Diego Torres Milano came to my rescue, pointing out a CTS test case for these APIs. From there, I was able to figure out how this stuff works, more or less.

The classic ways we have used to bridge Java and JavaScript, in the context of a WebView, are:

  • loadUrl("javascript:...") and evaluateJavascript(), to have Java invoke JavaScript code

  • addJavascriptInterface() to expose a Java object as a notional JavaScript global, so that JavaScript can call methods exposed on that object

The latter has been the source of many security bugs over the years, which presumably is why Google is counseling against it.

HTML message channels are a viable alternative, for cases where you control the Web content being shown in the WebView.

postWebMessage() is a method on WebView that allows you to send over an arbitrary string to the JavaScript code in the currently loaded Web page. To receive it, though, the JavaScript must have registered a global onmessage function. So, postWebMessage(new WebMessage("Hi"), ...) will cause the following onmessage function to be called, with e.data being "Hi":

onmessage = function (e) {
  parse(e.data);
}

This can serve as a replacement for loadUrl() and evaluateJavascript(), for Java telling the JavaScript in the current Web page to do something… if and only if the JavaScript is set up to listen to those messages.

Replacing addJavascriptInterface() is substantially more involved, including:

  • Calling createWebMessageChannel() on the WebView, to get a WebMessagePort pair representing a communications channel

  • Passing one of those ports to JavaScript via postWebMessage()

  • Each side, as needed, registering handlers to listen for messages on those ports

  • Each side, as needed, sending messages to the other side via those ports

My Stack Overflow answer has a bit more detail. This sample app shows it in use, and I’ll be covering this in greater detail in the next update to The Busy Coder’s Guide to Android Development.

There are some limitations, though:

  • You have to modify the JavaScript code to work with these message ports, which should not be a huge issue, given that you had to modify the JavaScript to talk to your injected notional JavaScript global from addJavascriptInterface()

  • A bug or undocumented limitation requires you to use loadDataWithBaseURL() and an http or https URL to make this work. You cannot, for example, use this with loadUrl("file:///android_asset/..."), which confused the heck out of me when I was trying to go that route in the sample app.

  • This is only available on Android 6.0+, which means until your minSdkVersion is 23 or higher, you have to use the other APIs as well, at least for the older devices that you are supporting.

Is this more secure than addJavascriptInterface()? Probably, insofar as the scope of the API that you expose from Java to the JavaScript code is more intentional. It is more difficult to accidentally expose something when it is more difficult for you to expose anything. However, neither HTML message channels nor addJavascriptInterface() are great solutions for when you are working with arbitrary HTML/JS that you did not write.