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/Method | Impact |
---|---|
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:
- Parent page is loaded into a WebView via the
WebView.load*()
family of methods. - This parent page embeds an
<iframe>
from a trusted sourcehttps://trusted.com
. https://trusted.com
embeds an<iframe>
from an untrusted sourcehttps://ads.com
.https://ads.com
may (un)intentionally serve up a malicious<iframe>
, depending on the ad content.
<iframe>
.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:
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.
JavaScriptEnabled
and JavaScriptCanOpenWindowsAutomatically
are both true
, then the frame can also create a popup with window.open()
or showModalDialog()
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 totrue
.
and/or
WebChromeClient
is attached to the WebView.onCreateWindow()
is overwritten, utilizingViewGroup.addView()
without an index (or withindex = -1
).
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
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:
- Frame creates a popup. Android creates and appends a new WebView containing the popup to the ViewGroup.
- 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. - Parent WebView is removed from ViewGroup. Popup WebView is all that remains.
- 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:
|
|
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.
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:
|
|
It is then converted to a base64 data URL:
|
|
Then the malicious ad embeds it as an <iframe>
:
|
|
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:
Message Forgery
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:
|
|
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:
|
|
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
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.
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 😊