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:...")
andevaluateJavascript()
, 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 theWebView
, to get aWebMessagePort
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 anhttp
orhttps
URL to make this work. You cannot, for example, use this withloadUrl("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.