Android uses the WebKit browser engine as the foundation for both its
Browser application and the WebView
embeddable browsing widget. The
Browser application, of course, is something Android users can
interact with directly; the WebView
widget is something you can
integrate into your own applications for places where an HTML
interface might be useful.
Earlier in this book, we saw a simple integration of a WebView
into
an Android activity, with the activity dictating what the browsing
widget displayed and how it responded to links.
Here, we will expand on this theme, and show how to more tightly integrate the Java environment of an Android application with the JavaScript environment of WebKit.
Understanding this chapter requires that you have read the core chapters,
particularly the one covering WebView
. Some of the samples
use LocationManager
for obtaining a GPS fix.
When you integrate a WebView
into your activity, you can control
what Web pages are displayed, whether they are from a local provider
or come from over the Internet, what should happen when a link is
clicked, and so forth. And between WebView
, WebViewClient
, and
WebSettings
, you can control a fair bit about how the embedded
browser behaves. Yet, by default, the browser itself is just a
browser, capable of showing Web pages and interacting with Web sites,
but otherwise gaining nothing from being hosted by an Android
application.
However, WebView
offers a few options for more tightly integrating
the Java and JavaScript realms, so Web content can call into your app
to get data to display, and your app can push data into the Web page for
JavaScript to render.
Unfortunately, the techniques for doing this have changed over the
years. Partially that is due to changes in WebView
, particularly
starting with Android 4.4. But, some of the changes are due to
security issues, particularly when you are loading arbitrary content,
such as Web-based ads from an ad network, into your WebView
.
The following sections will go over four separate sample apps. All do the same thing: provide data about the ambient light level, using the sensor on the phone or tablet, to the Web page for rendering. The differences are whether we are pushing data from Java into JavaScript (e.g., as the light level changes), or whether we are pulling data from Java using JavaScript (e.g., in response to a user tapping on something in the Web page). Also, we will see two approaches to push and two approaches to pull.
The addJavascriptInterface()
method on WebView
allows you to
inject a Java object into the WebView
, exposing its methods, so
they can be called by JavaScript loaded by the Web content in the
WebView
itself.
Now you have the power to provide access to a wide range of Android
features and capabilities to your WebView
-hosted content. If you
can access it from your activity, and if you can wrap it in something
convenient for use by JavaScript, your Web pages can access it as
well.
The
WebKit/SensorPull
sample project demonstrates using addJavascriptInterface()
to pull light
sensor data into a Web page to display to the user.
For all four of these sample apps, the UI is just a WebView
:
<?xml version="1.0" encoding="utf-8"?>
<WebView android:id="@+id/webkit"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</WebView>
In onCreate()
, we set things up:
@SuppressLint({"AddJavascriptInterface", "SetJavaScriptEnabled"})
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mgr=(SensorManager)getSystemService(Context.SENSOR_SERVICE);
light=mgr.getDefaultSensor(Sensor.TYPE_LIGHT);
wv=(WebView)findViewById(R.id.webkit);
wv.getSettings().setJavaScriptEnabled(true);
wv.addJavascriptInterface(jsInterface, "LIGHT_SENSOR");
wv.loadUrl("file:///android_asset/index.html");
}
Specifically, we:
SensorManager
and the appropriate Sensor
for measuring ambient light
levels, saving those in fieldsWebView
via findViewById()
getSettings()
and setJavaScriptEnabled(true)
addJavascriptInterface()
, andBecause we are enabling JavaScript, Lint will complain that this poses security
risks, so onCreate()
has a @SuppressLint
annotation for SetJavaScriptEnabled
to indicate that we are aware of the risks. Similarly, because we are calling
addJavascriptInterface()
, Lint will complain that this poses even more security
risks. So, @SuppressLint
suppresses both the SetJavaScriptEnabled
warning
and the AddJavascriptInterface
warning.
Also, you may notice that there is a significant debate within the Android SDK
as to whether the “s” in “JavaScript” gets capitalized or not. In general, it
does, but addJavascriptInterface()
shipped in API Level 1 with a lowercase “s”
in its name, and so that method, and variations of it (e.g., the AddJavascriptInterface
annotation) will use a lowercase “s”. Eventually, you just get used to this.
addJavascriptInterface()
takes two parameters: a Java object to inject into
the JavaScript of the Web page, and a String
that is the name by which JavaScript
can reference that object. So, we have a jsInterface
object that JavaScript
can reference via LIGHT_SENSOR
.
jsInterface
is an instance of JSInterface
, a static
nested class inside
MainActivity
:
private static class JSInterface {
float lux=0.0f;
private void updateLux(float lux) {
this.lux=lux;
}
@JavascriptInterface
public String getLux() {
return(String.format(Locale.US, "{\"lux\": %f}", lux));
}
}
It just has a getter and setter around the current light level, which is a
float
named lux
(referring to the unit of brightness used for the values
coming from the ambient light sensor). The getter, however, has two interesting
traits:
String
, representing a JSON object wrapped around the lux
value@JavascriptInterface
This annotation is required of apps with
android:targetSdkVersion
set to 17 or higher, though it is a good idea
to start using it anyway. With such an android:targetSdkVersion
, in an
app running on an Android 4.2 or higher device, only public
methods with the
@JavascriptInterface
annotation will be accessible by JavaScript code.
On earlier devices, or with an earlier android:targetSdkVersion
, all
public
methods on the JsInterface
object would be accessible by JavaScript,
including those inherited from superclasses like Object
. Note that your
build target (i.e., compileSdkVersion
in Android Studio)
will need to be Android 4.2 or higher in order to reference
the @JavascriptInterface
annotation.
The reason for returning a JSON object (in string form), rather than just
the float
, is for two reasons:
@JavascriptInterface
objects need to be something that
JavaScript can use, and a simple way to do that is to create data structures
in JSON.float
did not seem to work well as a return type, as it always seemed to
turn into 0.0
on the JavaScript side, for unknown reasonsWe register with the SensorManager
to find out when the light level changes,
via registerListener()
(in onStart()
) and unregisterListener()
(in onStop()
):
@Override
protected void onStart() {
super.onStart();
mgr.registerListener(this, light, SensorManager.SENSOR_DELAY_UI);
}
@Override
protected void onStop() {
mgr.unregisterListener(this);
super.onStop();
}
That, in turn, will trigger a call to onSensorChanged()
when the light level
changes. There, we pass the light level (the first float
out of the
values
array from the SensorEvent
) to the JsInterface
instance, ready to
be retrieved by the JavaScript code in our Web page:
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
jsInterface.updateLux(sensorEvent.values[0]);
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
// unused
}
In the Web page, we set it up to show the current light level, starting with
a value of 0.0
. When the user taps on the Light Level
caption, we
call a pull()
JavaScript function, which:
getLux()
method on the LIGHT_SENSOR
global objectupdate_lux()
function to update the lux
span with the
new light level<html>
<head>
<title>Android Light Sensor Demo</title>
<script language="javascript">
function update_lux(lux) {
document.getElementById("lux").innerHTML=lux;
}
function pull() {
var result=JSON.parse(LIGHT_SENSOR.getLux());
update_lux(result.lux);
}
</script>
</head>
<body>
<p><b><a onClick="pull()">Light Level</a>: </b><span id="lux">0.0</span> lux</p>
</body>
</html>
If you run the app, you get our trivial Web page in the WebView
:
Figure 517: SensorPull Demo, As Initially Launched
Tapping on the words “Light Level” will cause JavaScript to request the light level, updating the page to match:
Figure 518: SensorPull Demo, Showing Light Level
Note that this sample app will only work on devices with an ambient light sensor. It is rather likely that the app will crash spectacularly on devices lacking such a sensor.
Unfortunately, addJavascriptInterface()
opens up a number of security issues,
outlined later in this chapter. Where possible, avoid
the use of this API.
Now that we have seen how JavaScript can call into Java, it would be nice if Java could somehow call out to JavaScript. Well, as luck would have it, we can do that too. This is a good thing, otherwise, this would be a really weak section of the book.
What is unusual is how you call out to JavaScript. One might imagine
there would be an evaluateJavaScript()
counterpart to
addJavascriptInterface()
, where you could supply some JavaScript
source and have it executed within the context of the
currently-loaded Web page.
Actually, there is such a method on Android 4.4. However, earlier
versions of Android lacked that method.
Instead, on older versions of Android,
given your snippet of JavaScript source to execute, you call
loadUrl()
on your WebView
, as if you were going to load a Web
page, but you put javascript:
in front of your code and use that as
the “address” to load.
If you have ever created a “bookmarklet” for a desktop Web browser,
you will recognize this technique as being the Android analogue
– the javascript:
prefix tells the browser to treat the rest
of the address as JavaScript source, injected into the
currently-viewed Web page.
The
WebKit/SensorPush
sample project expands upon the SensorPull
app. This time, though, in addition
to pulling via addJavascriptInterface()
, we support pushing light levels as
sensor readings come in.
That comes courtesy of a revised onSensorChanged()
method:
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
float lux=sensorEvent.values[0];
jsInterface.updateLux(lux);
String js=String.format(Locale.US, "update_lux(%f)", lux);
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT) {
wv.evaluateJavascript(js, null);
}
else {
wv.loadUrl("javascript:"+js);
}
}
Before, we just updated the JsInterface
object with the new light level. Now,
we also format a JavaScript call to update_lux()
, supplying our light level.
Then, based on Android OS version (Build.VERSION.SDK_INT
), we either call
evaluateJavascript()
or loadUrl()
, the latter also employing the javascript:
scheme.
Because we had pulled out update_lux()
as a separate function before, our HTML
and JavaScript does not need to change at all:
<html>
<head>
<title>Android Light Sensor Demo</title>
<script language="javascript">
function update_lux(lux) {
document.getElementById("lux").innerHTML=lux;
}
function pull() {
var result=JSON.parse(LIGHT_SENSOR.getLux());
update_lux(result.lux);
}
</script>
</head>
<body>
<p><b><a onClick="pull()">Light Level</a>: </b><span id="lux">0.0</span> lux</p>
</body>
</html>
If you run this sample app, you will find that the Web page updates in real time as you wave your hand in front of the light sensor, shine a light on that sensor, etc.
Both of those techniques have worked since API Level 1. But, as mentioned,
addJavascriptInterface()
has security issues. Also, evaluateJavascript()
(or its loadUrl()
equivalent) requires the Java code to know what functions
are available in the Web page. That may tie the Java and JavaScript more tightly
than you might like.
Android 6.0 introduced another pair of options for communicating between
Java and JavaScript — WebMessage
and WebMessagePort
— that try to eliminate
these issues.
The simpler of the two approaches is WebMessage
. Instead of calling
evaluateJavascript()
or loadUrl()
, you create a WebMessage
object and
call postWebMessage()
to deliver it to the JavaScript in your Web page.
So, in the
WebKit/SensorMessage
sample project, we have an updated onSensorChanged()
method that does this:
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
float lux=sensorEvent.values[0];
jsInterface.updateLux(lux);
String js=String.format(Locale.US, "update_lux(%f)", lux);
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.M) {
wv.postWebMessage(new WebMessage(jsInterface.getLux()),
Uri.EMPTY);
}
else if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT) {
wv.evaluateJavascript(js, null);
}
else {
wv.loadUrl("javascript:"+js);
}
}
The WebMessage
constructor that we are using here takes a simple string that
is the content of the message. In this case, it is the JSON object wrapping
our light level in lux, using the same getLux()
method that JavaScript can
call on our JsInterface
instance that we registered via addJavascriptInterface()
.
postWebMessage()
takes two parameters. The first is the WebMessage
to deliver
to the page. The other is supposed to be the Uri
of the Web page. This is supposed
to be used to confirm that you are sending the message to the page that you think
you are sending it to.
Unfortunately, this is not behaving especially well.
It only works as advertised for http
/https
URLs, or for data that you
load using loadDataWithBaseURL()
and supply some http
or https
URL.
If you load from a file
URL, as we are doing here, you cannot use the
actual URL. Instead, you have to use Uri.EMPTY
, which is a “wildcard” that
skips over this test, which is what we use here. Apparently, this is all
working as intended.
To receive these messages, our JavaScript needs to define an onmessage()
global function:
<html>
<head>
<title>Android Light Sensor Demo</title>
<script language="javascript">
function update_lux(lux) {
document.getElementById("lux").innerHTML=lux;
}
function parse(json) {
var result=JSON.parse(json);
update_lux(result.lux);
}
function pull() {
parse(LIGHT_SENSOR.getLux());
}
onmessage = function (e) {
parse(e.data);
}
</script>
</head>
<body>
<p><b><a onClick="pull()">Light Level</a>: </b><span id="lux">0.0</span> lux</p>
</body>
</html>
This receives the HTML Web message equivalent of the WebMessage
that we posted.
The data
field on the supplied event object (e
in the sample) contains
our string. So, we turn around and parse()
it, just as we would parse()
the JSON we got from calling getLux()
on our LIGHT_SENSOR
.
If you run this sample app on an Android 6.0+ device, you should get the same
results as with SensorPush
, where the light level changes automatically. However,
in this case, we will be using code that relies upon WebMessage
and
postWebMessage()
, instead of evaluateJavascript()
or loadUrl()
. In particular,
our Java code does not need to know anything about the internal workings of the
JavaScript (e.g., function names) — it just passes over the message and relies
on the JavaScript to have registered itself appropriately to receive the message.
What would be nice is to use this WebMessage
system to be able to replace
addJavascriptInterface()
and allow JavaScript to call back into Java. This
is possible, but it is fairly complex.
For our Java code to receive messages sent to it from JavaScript, we need to do three things:
createWebMessageChannel()
on the WebView
. This creates a private
communications channel between us and our target Web page. It returns
a two-element WebMessagePort
array. Index 0
of that array is our end
of the channel; index 1
is the JavaScript end of the channel.setWebMessageCallback()
on our WebMessagePort
, supplying a
WebMessageCallback
that will be called with onMessage()
when a message
arrives on the port from JavaScript.WebMessagePort
to JavaScript using a WebMessage
.In the
WebKit/SensorPort
sample project, this is handled by an initPort()
method:
@TargetApi(Build.VERSION_CODES.M)
private void initPort() {
final WebMessagePort[] channel=wv.createWebMessageChannel();
port=channel[0];
port.setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
@Override
public void onMessage(WebMessagePort port, WebMessage message) {
postLux();
}
});
wv.postWebMessage(new WebMessage("", new WebMessagePort[]{channel[1]}),
Uri.EMPTY);
}
The WebMessage
constructor that we use this time takes two parameters: an
arbitrary string (here, just set to ""
, as we are not using it) and
a one-element WebMessagePort
array containing the JavaScript end of the
communications channel.
However, we cannot do any of this work until the Web page is ready to be used.
Otherwise, the JavaScript code will not receive our WebMessage
, since it is
not yet ready.
So, we have to postpone calling initPort()
until a WebViewClient
is called
with onPageFinished()
, as we do in onCreate()
:
@SuppressLint({"AddJavascriptInterface", "SetJavaScriptEnabled"})
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mgr=(SensorManager)getSystemService(Context.SENSOR_SERVICE);
light=mgr.getDefaultSensor(Sensor.TYPE_LIGHT);
wv=(WebView)findViewById(R.id.webkit);
wv.getSettings().setJavaScriptEnabled(true);
wv.addJavascriptInterface(jsInterface, "LIGHT_SENSOR");
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.M) {
wv.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
initPort();
}
});
}
wv.loadUrl(URL);
}
Our WebMessageCallback
, upon receipt of a WebMessage
from the JavaScript,
calls a postLux()
method. We are just using the existence of the message
as a “ping” from the JavaScript to Java, asking for us to send it the ambient light
level. So, in postLux()
, we create a WebMessage
and send it to JavaScript…
but not via the postWebMessage()
method on WebView
. Instead, we use our
end of the WebMessagePort
communications channel, calling postMessage()
on it,
in a postLux()
method:
@TargetApi(Build.VERSION_CODES.M)
private void postLux() {
port.postMessage(new WebMessage(jsInterface.getLux()));
}
So, when the JavaScript sends a WebMessage
to Java, Java sends a WebMessage
right back, supplying the light level JSON.
To prove that this is working, this sample comments out the automated push of
the light level in onSensorChanged()
— ordinarily, we would call postLux()
to push over the light level when we get it:
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
float lux=sensorEvent.values[0];
jsInterface.updateLux(lux);
String js=String.format(Locale.US, "update_lux(%f)", lux);
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.M) {
// postLux();
}
else if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT) {
wv.evaluateJavascript(js, null);
}
else {
wv.loadUrl("javascript:"+js);
}
}
In JavaScript, our onmessage
global function is now a bit more complex as
well. We get our end of the communications channel by retrieving our port
from ports
on the event delivered to onmessage()
. Then, we register
an onmessage
function on that port, which is how we receive the light
levels that Java delivers via postMessage()
on its WebMessagePort
.
When the user taps the label, we call a pull()
function in JavaScript,
that calls postMessage()
on the port
, supplying some string as a message
(here, hardcoded as "ping"
and ignored by our Java code):
<html>
<head>
<title>Android Light Sensor Demo</title>
<script language="javascript">
function update_lux(lux) {
document.getElementById("lux").innerHTML=lux;
}
function parse(json) {
var result=JSON.parse(json);
update_lux(result.lux);
}
var port;
function pull() {
port.postMessage("ping");
}
onmessage = function (e) {
port = e.ports[0];
port.onmessage = function (f) {
parse(f.data);
}
}
</script>
</head>
<body>
<p><b><a onClick="pull()">Light Level</a>: </b><span id="lux">0.0</span> lux</p>
</body>
</html>
If you run this sample on Android 6.0+, and you tap the “Light Level” label,
you will get the light level, delivered by means of our WebMessagePort
-based
communications channel.
(NOTE: the author would like to thank Diego Torres Milano for his assistance in finding out how this stuff works).
There is no navigation toolbar with the WebView widget. This allows you to use it in places where such a toolbar would be pointless and a waste of screen real estate. That being said, if you want to offer navigational capabilities, you can, but you have to supply the UI. WebView offers ways to perform garden-variety browser navigation, including:
reload()
to refresh the currently-viewed Web pagegoBack()
to go back one step in the browser history, and canGoBack()
to determine if there is any history to go back togoForward()
to go forward one step in the browser history, and
canGoForward()
to determine if there is any history to go forward togoBackOrForward()
to go backwards or forwards in the browser
history, where negative numbers represent a count of steps to go
backwards, and positive numbers represent how many steps to go
forwardscanGoBackOrForward()
to see if the browser can go backwards or
forwards the stated number of steps (following the same
positive/negative convention as goBackOrForward()
)clearCache()
to clear the browser resource cache and clearHistory()
to clear the browsing historyWith your favorite desktop Web browser, you have some sort of “settings” or “preferences” or “options” window. Between that and the toolbar controls, you can tweak and twiddle the behavior of your browser, from preferred fonts to the behavior of JavaScript.
Similarly, you can adjust the settings of your WebView widget as you see fit,
via the WebSettings
instance returned from calling the widget’s
getSettings()
method.
There are lots of options on WebSettings
to play with. Most appear fairly
esoteric (e.g., setFantasyFontFamily()
). However, here are some that you
may find more useful:
setDefaultFontSize()
(to use a point size)
or setTextSize()
(to use constants indicating relative sizes like
LARGER and SMALLEST)setUserAgent()
, so you can supply
your own user agent string to make the Web server think you are a
desktop browser, another mobile device (e.g., iPhone), or whatever.
The settings you change are not persistent, so you should store them
somewhere (such as via the Android preferences engine) if you are allowing
your users to determine the settings, versus hard-wiring the settings in your
application.More so than normal widgets, WebView
opens up potential security
issues, just as a Web browser could. If all you are doing is displaying
your own content, the risks are minimal. If, on the other hand, you are
displaying content from third parties, it is possible that their content
is malicious in a way that can compromise your app’s security, to your
users’ detriment.
If you call setJavaScriptEnabled(true)
on your WebSettings
, you are
allowing JavaScript code to be loaded and executed by WebView
. In many
cases, this is essential to get your content to render properly (e.g.,
the JavaScript is issuing AJAX calls). However, if you did not write the
scripts, you do not know what they might be doing. If there are flaws
in WebView
— such as those discussed in the next sections — then
your users may be at risk.
Even in the absence of such bugs, JavaScript can always:
javascript:
URLs or evaluateJavaScript()
Another way that rogue JavaScript can attack users is if you use
addJavascriptInterface()
to allow JavaScript code to call out to a
Java object that you supply.
As was noted earlier in this chapter, when
addJavascriptInterface()
was introduced, there is this
@JavascriptInterface
annotation that we should apply to the methods
we want JavaScript to be able to call on the object we supply via
addJavascriptInterface()
. This is because of a bug in the
addJavascriptInterface()
implementation, whereby on 4.1 and below
any method on the Java object could be called by JavaScript. This
includes methods like getClass()
… which in turn would allow
JavaScript to use Class.forName()
to get at arbitrary stuff. This
was used by various bits of malware.
Hence, using addJavascriptInterface()
on Android 4.1 and below
is rather risky, if you are loading arbitrary third-party JavaScript.
If you have the means of examining that JavaScript (e.g., you are
loading the scripts yourself), you might perform some simple scans
of it to see if they appear to be doing anything unfortunate with your
Java object that you injected into JavaScript via addJavascriptInterface()
.
Worse, Android sometimes also injects its own objects, without our requesting them.
In particular, this security bug
points out that, through Android 4.3, if users have enabled an accessibility
service, Android automatically injects objects into WebView
, using
addJavascriptInterface()
, named accessibility
and accessibilityTraversal
.
So, even if you do not inject any objects yourself via addJavascriptInterface()
,
your WebView
may be at risk. The security researchers who uncovered
this attack vector suggest using removeJavascriptInterface()
to specifically
get rid of those objects.
Due to a bizarre bug in the parsing of URLs, it is possible for JavaScript
code to violate the “same-origin policy” of a WebView
on Android 4.3 and
earlier.
Quoting Wikipedia from September 2014:
the same-origin policy is an important concept in the web application security model. The policy permits scripts running on pages originating from the same site — a combination of scheme, hostname, and port number — to access each other’s DOM with no specific restrictions, but prevents access to DOM on different sites… This mechanism bears a particular significance for modern web applications that extensively depend on HTTP cookies to maintain authenticated user sessions, as servers act based on the HTTP cookie information to reveal sensitive information or take state-changing actions. A strict separation between content provided by unrelated sites must be maintained on the client side to prevent the loss of data confidentiality or integrity.
All modern Web browsers implement the same-origin policy (SOP)… but there can
be bugs. A security researcher disclosed that the original AOSP Browser
application failed to implement the SOP properly,
when a javascript:
URL has a null byte before the j
in javascript
.
And while the report was focused on the AOSP Browser app, the problem
really lies with WebView
.
To see this in action, load https://commonsware.com/misc/sop-demo.html
in the AOSP Browser app on Android 4.3 or lower. This Web page consists
of:
<html>
<head>
<title>WebView SOP Test</title>
</head>
<body>
<h1>WebView SOP Test</h1>
<iframe name="test" src="http://developer.android.com"></iframe>
<input type=button value="test" onclick="window.open('\u0000javascript:alert(document.domain)','test')">
</body>
</html>
It is derived from a similar example found in the blog post outlining the security flaw.
In an SOP-compliant browser, clicking the button will have no effect.
In the AOSP Browser app, clicking the button shows the domain name of the
document in the iframe
. And, loading this HTML into a WebView
has
the same effect.
Part of the WebView
overhaul in Android 4.4 — replacing the original
implementation with a new one backed by Chromium — had the effect of fixing
this bug, whether intentionally or inadvertently.
There is no obvious mitigation approach for this bug, insofar as for the
attack shown above, none of the callbacks on WebViewClient
or
WebChromeClient
seem to allow us to intercept this URL before its
JavaScript is executed. If you are loading HTML yourself from a third party,
you might consider scanning that HTML for obvious signs of the attack
(e.g., regular expression check for \u0000javascript
), but that will
be limited at best. Beyond that, try to limit the content in a WebView
to be from only one origin, so that there is nothing for attackers to
obtain via this bug.
Also note that the security researcher who found this bug has also found another SOP violation, suggesting that mitigation strategies may be impractical.
WebView
, as Android developers use it, is really a facade API, as of Android 5.0.
Previously, the WebView
implementation was part of the Android framework.
Nowadays, the implementation is delegated to the Android System WebView, which
allows Google to update the WebView
implementation without relying upon
manufacturers to distribute firmware updates.
Hence, to some extent, WebView
continuously changes, as the Android System
WebView app gets updated a few times per year.
However, it is only in a new version of Android that Google changes the API or
the general approach taken by a WebView
implementation. In Android 8.0, two significant
changes arrived: multi-process mode and support for banning of cleartext
traffic.
Historically, WebView
code ran in your app’s regular process. Because we often
load JavaScript into a WebView
, having the WebView
in our process would raise
the risks of WebView
bugs. If JavaScript could cause arbitrary code to execute
in our own process, it would have whatever rights our own application code does,
including everything for which we have runtime permissions.
Android 7.0 added a developer option for moving the WebView
execution to a
separate process, and that is now standard on Android 8.0. As the documentation describes it,
“Web content is handled in a separate, isolated process from the containing app’s process for enhanced security.”
In principle, this should not require code changes. However, this is a significant
architectural change, and so it is worthwhile to fully test your WebView
usage
on Android 8.0+, to make sure nothing breaks compared with that same code
running on Android 7.1 or older devices.
If your app has a targetSdkVersion
over 25, and in your
network security configuration you banned cleartext traffic,
WebView
will honor that setting on Android 8.0+. On older devices, WebView
ignores the network
security configuration, and it appears that it still ignores the rest
of the configuration (e.g., certificate pinning).
However, this allows you to block the accidental transmission of data over
unencrypted channels. Most apps doing network I/O should ban cleartext traffic,
at least for debug
builds, to identify where they are accidentally using
a plain http
URL. Apps for which security is a priority might also ban cleartext
traffic for release
builds.
The
WebKit/BrowserSecure
sample project demonstrates this. It has a network security configuration
XML file that bands cleartext traffic:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
It applies that configuration in the manifest via the android:networkSecurityConfig
attribute on the <application>
element:
<application
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/net_security_config">
<activity android:name=".BrowserDemo1">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
The activity just loads up a plain HTTP-served Web page:
package com.commonsware.android.browser1;
import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
public class BrowserDemo1 extends Activity {
WebView browser;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.main);
browser=(WebView)findViewById(R.id.webkit);
browser.loadUrl("http://www.andglobe.com");
}
}
The project is set up to target 26
:
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion '26.0.2'
defaultConfig {
minSdkVersion 25
targetSdkVersion 26
applicationId 'com.commonsware.android.browser.secure'
}
}
If you change that to have it target 25, the app will run “normally” on Android 8.0,
showing the designated Web page. But, with a targetSdkVersion
set at 26
, the
WebView
will not show that page, as it would be loaded over a cleartext (HTTP)
connection:
Figure 519: BrowserSecure Sample, Showing Banned Cleartext Traffic
Chrome custom tabs
serve as middle ground between using a WebView
in your own
app and launching a URL into a separate Web browser.
With a WebView
, you have complete control over the overall user
experience within your app. However, your WebView
is decoupled
from any other browser the user may be using on the device.
Conversely, launching URLs into the user’s chosen browser gives the
user their normal browsing experience, but you have no control over
the user experience, as the user is now in the browser app, not your app.
With Chrome custom tabs, while Chrome is handling the URL, it will allow
you limited control over the action bar (color and custom actions). It also
simplifies some things that you might otherwise have had to handle yourself,
such as pre-fetching a Web page to be able to quickly switch to it.
Basic integration is also fairly easy, coming in the form of extras on
the same sort of ACTION_VIEW
Intent
that you might have used for
launching the URL in a standalone browser.
At the same time, there are some concerns:
ACTION_VIEW
Intent
, and so the user can choose
to view the URL in a different browser, you will not get the custom
integration in that case. This may be fine, but you will need to make
sure that from a marketing and documentation standpoint you handle both
the case where the user chooses Chrome (and you get the “custom tab”)
and the case where the user chooses something else.ACTION_VIEW
Intent
, you need to take
into account that any information in the custom extras, like the PendingIntent
to use for a custom action bar item, is stuff that you are willing
for arbitrary apps to get their hands on. Do not assume that your
communications will solely be with Chrome.