Tue 18 May 2021
WebView
is an important component for mobile applications, it allows Android and iOS applications to render web content and execute Javascript code inside a mobile application.
The design of WebView implies changes of the landscape of the Web and requires special attention to avoid security holes in the applications.
Through 5 concise and useful tips, you'll understand the usage of WebViews
, and the common security issues you need to check.
Ostorlab already checks automatically for all of these issues using both static and dynamic analysis.
Tell me your OS version, I tell you your webview?
Webview was introduced in iOS 1 and Android 2 and knew important changes to optimize its performance and enhance its security.
Here are the biggest milestones in the Webview design:
-
iOS:
UIWebview
is available sinceiOS
1 and deprecated iniOS
8. It has many security issues:- You can NOT disable Javascript
- You can NOT disable Access to files
- You can NOT implement the same origin policy for file access
- Native application has access to all the requests/response, which is not ideal for sensitive data and external authentication
- The rendered content, and the native application shares the same process
WKWebView
is available startingiOS
8 and introduced multiple performance and security enhancements:- You can disable Javascript
- You can disable Access to files
- You can implement the same origin policy for file access
- Native application has access to all the requests/responses, which is not ideal for sensitive data and external authentication
- The rendered content, and the native application run in different processes
SFSafariViewController
is available startingiOS
9 and provides a browser-like experience; It is mainly used when the application needs only to display the web content.- You can disable Javascript
- You can disable Access to files
- You can implement the same origin policy for file access
- Native application can NOT access all the requests/responses, but you can use different implementations to share the Cookies and stored data
- The rendered content, and the native application run in different processes.
-
Android:
WebKit
from 2.x to 3:- You can disable Javascript
- You can Not disable Access to files
- You can NOT implement the same origin policy for file access
- The rendered content, and the native application shares the same process
WebKit
on 3:- You can disable Javascript
- You can disable Access to files
- You can NOT implement the same origin policy for file access
- The rendered content, and the native application shares the same process
Chromium
30 on 4.4:Webview
runs in the UI thread- You can disable Javascript
- You can disable Access to files
- You can implement the same origin policy for file access
- The rendered content, and the native application shares the same process
- Chromium M37 on 5.0:
PermissionRequest
class introduced to grant theWebView
permission to access protected resources like the camera and microphone.- Supports Chromium updates from Google Play Store
- From Android 7.0 to Android :
- the geolocation API will only be allowed on secure origins (over HTTPS.)
WebView
runs web content in a separate sandboxed process- Safe Browsing API added to notify when
WebView
attempts to navigate to a URL that Google has classified as a known threat.
Is it safe to enable debugging WebViews contents in production?
Short answer is: NO!
In this section we will see how a malicious application can access the data rendered in a WebView
if the WebContentsDebugging
is enabled.
To illustrate in an application how it is enabling the debugging WebViews
contents, I will use Ostorlab's analysis environment, and search for the function setWebContentsDebuggingEnabled
.
To check the call tree, and the parameters passed to the function, I go to the call tree tab:
We can see that initWebView
is calling setWebContentsDebuggingEnabled
:
Jumping to the source code, we can see that the parameter is retrieved from the method this.config.isWebContentsDebuggingEnabled()
private void initWebView()
{
android.webkit.WebSettings v0_1 = this.webView.getSettings();
v0_1.setJavaScriptEnabled(1);
v0_1.setDomStorageEnabled(1);
v0_1.setGeolocationEnabled(1);
v0_1.setDatabaseEnabled(1);
v0_1.setAppCacheEnabled(1);
v0_1.setMediaPlaybackRequiresUserGesture(0);
v0_1.setJavaScriptCanOpenWindowsAutomatically(1);
if (this.config.isMixedContentAllowed()) {
v0_1.setMixedContentMode(0);
}
String v1_3 = this.config.getAppendedUserAgentString();
if (v1_3 != null) {
String v2_0 = v0_1.getUserAgentString();
String v3_1 = new StringBuilder();
v3_1.append(v2_0);
v3_1.append( );
v3_1.append(v1_3);
v0_1.setUserAgentString(v3_1.toString());
}
String v2_2 = this.config.getOverriddenUserAgentString();
if (v2_2 != null) {
v0_1.setUserAgentString(v2_2);
}
String v3_4 = this.config.getBackgroundColor();
if (v3_4 == null) {
} else {
try {
this.webView.setBackgroundColor(com.getcapacitor.util.WebColor.parseColor(v3_4));
} catch (boolean v4) {
com.getcapacitor.Logger.debug(WebView background color not applied);
}
}
this.webView.requestFocusFromTouch();
android.webkit.WebView.setWebContentsDebuggingEnabled(this.config.isWebContentsDebuggingEnabled());
return;
}
Checking the definition of isWebContentsDebuggingEnabled
, we can see that it uses capacitor.config.json
and the parameter is set to true.
Next step, we install the application and run it on the phone.
WebView
debugging uses the Chrome Debug Protocol, and it is exposed using an abstract named unix socket. The socket is either
name webview_devtools_remote
or webview_devtools_remote_<pid>
.
Abstract sockets do not use file system permissions to enforce access and are therefore accessible to all applications on the device.
We can use netstat
to find the exposed socket:
shell# netstat -untapexW | grep webview_devtools_remote
unix 2 [ ACC ] STREAM LISTENING 2633690 26634/com.xxxxx.i@webview_devtools_remote_26634
Now to exploit and read the content of the socket all we have to do is to run the following command on the phone:
socat TCP-LISTEN:9999,fork ABSTRACT:webview_devtools_remote_2466
Please note that Java doesn't have an API to access abstract socket, attackers performing this type of attack will likely use native code.
To access the remote protocol, use the Chrome Debug Protocol client, like pychrome
:
import pychrome
# connect to webview on the exposed port.
browser = pychrome.Browser(url="http://127.0.0.1:9999")
t = browser.list_tab()[0]
t.start()
t.DOM.enable()
# Access document.
t.DOM.getDocument()
We can use the DOM object to inspect all the content of the page:
>>> t.DOM.getDocument()
{'root': {'nodeId': 1, 'backendNodeId': 2, 'nodeType': 9, 'nodeName': '#document', 'localName': '', 'nodeValue': '', 'childNodeCount': 2, 'children': [{'nodeId': 2, 'parentId': 1, 'backendNodeId': 42, 'nodeType': 10, 'nodeName': 'html', 'localName': '', 'nodeValue': '', 'publicId': '', 'systemId': ''}, {'nodeId': 3, 'parentId': 1, 'backendNodeId': 43, 'nodeType': 1, 'nodeName': 'HTML', 'localName': 'html', 'nodeValue': '', 'childNodeCount': 2, 'children': [{'nodeId': 4, 'parentId': 3, 'backendNodeId': 44, 'nodeType': 1, 'nodeName': 'HEAD', 'localName': 'head', 'nodeValue': '', 'childNodeCount': 80, 'attributes': []}, {'nodeId': 5, 'parentId': 3, 'backendNodeId': 45, 'nodeType': 1, 'nodeName': 'BODY', 'localName': 'body', 'nodeValue': '', 'childNodeCount': 5, 'attributes': []}], 'attributes': ['lang', 'en'], 'frameId': '8C9DD9891A40F2CEC8D73094D29D9152'}], 'documentURL': 'https://www.xxx.com/', 'baseURL': 'https://www.xxx.com/', 'xmlVersion': ''}}
A safe trip from Java code to Javascript:
Java objects can be injected into the WebView and exposed to JavaScript using the addJavascriptInterface
method. Below is a simple example illustrating how we can implement it:
webView = (WebView) findViewById(R.id.webView1);
webView.addJavascriptInterface(new JavaScriptBridge(), "safeBridge");
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebChromeClient(new WebChromeClient());
webView.loadUrl("file:///android_asset/main.html");
public class JavaScriptBridge {
@JavascriptInterface
public String helloSafeWorld()
{
return "Hello World!";
}
}
In this example the helloSafeWorld()
method can be invoked from JavaScript, using the following code:
var HelloWorld = window.safeBridge.helloSafeWorld();
Once an interface is registered to WebView
through addJavascriptInterface
, it becomes global. All pages loaded in
the WebView
can call this interface, and access the same data maintained by the interface. This makes it possible for
web pages from one origin to affect those from others
Starting from API version 17, only methods with the @JavascriptInterface
annotation are available to JavaScript code.
Prior to API version 17, reflection could be used to execute arbitrary code on the device (see CVE-2012-6636).
Let's have a look at a real example from the com.microsoft.skydrive
application. First we search for the function addJavascriptInterface
In the call stack tab, we can see that we have multiple interfaces exposed.
We will focus on the package com.microsoft.skydrive.reportabuse
.
public void onViewCreated(android.view.View p3, android.os.Bundle p4)
{
kotlin.jvm.internal.Intrinsics.checkNotNullParameter(p3, view);
android.webkit.WebView v3_12 = ((android.webkit.WebView) this._$_findCachedViewById(com.microsoft.skydrive.R$id.web_view));
kotlin.jvm.internal.Intrinsics.checkNotNullExpressionValue(v3_12, web_view);
android.webkit.WebView v3_13 = v3_12.getSettings();
kotlin.jvm.internal.Intrinsics.checkNotNullExpressionValue(v3_13, web_view.settings);
v3_13.setJavaScriptEnabled(1);
((android.webkit.WebView) this._$_findCachedViewById(com.microsoft.skydrive.R$id.web_view)).addJavascriptInterface(new com.microsoft.skydrive.reportabuse.ReportAbuseJavascriptInterface(this), external);
android.webkit.WebView v3_7 = ((android.webkit.WebView) this._$_findCachedViewById(com.microsoft.skydrive.R$id.web_view));
kotlin.jvm.internal.Intrinsics.checkNotNullExpressionValue(v3_7, web_view);
v3_7.setWebViewClient(new com.microsoft.skydrive.reportabuse.ReportAbuseDialogFragment$onViewCreated$1(this));
((android.webkit.WebView) this._$_findCachedViewById(com.microsoft.skydrive.R$id.web_view)).loadUrl(https://www.onedrive.com/reportabuse);
return;
}
The interface exposes the following methods:
package com.microsoft.skydrive.reportabuse;
public interface ReportAbuseInterface {
public abstract void dismissReportAbuse();
public abstract String getReportAbuseContextInformation();
public abstract void pageFinishedLoading();
public abstract void reportClicked();
public abstract void resize();
}
A key element when implementing those methods is to validate each input and avoid creating generic behaviors that an attacker might use to retrieve or modify sensitive data.
In the reportClicked
implementation below, calling the function with a null
value will trigger an error when calling valueOf
, which leads to unexpected behavior.
public void reportClicked(String p12, String p13)
{
android.content.Context v1_1 = this.getContext();
if (v1_1 != null) {
com.microsoft.authorization.instrumentation.AccountInstrumentationEvent v9_1 = new com.microsoft.authorization.instrumentation.AccountInstrumentationEvent(v1_1, com.microsoft.skydrive.instrumentation.EventMetaDataIDs.REPORT_ABUSE_CLICKED, this.a);
try {
com.microsoft.skydrive.reportabuse.ReportAbuseTask v2_0 = com.microsoft.skydrive.reportabuse.ReportAbuseDialogFragment$ReportAbuseType.valueOf(p12);
com.microsoft.authorization.OneDriveAccount v4 = this.a;
} catch (IllegalArgumentException) {
String v13_2 = new StringBuilder();
v13_2.append(Invalid report abuse type - );
v13_2.append(p12);
com.microsoft.odsp.io.Log.dPiiFree(ReportAbuseDialogFragment, v13_2.toString());
kotlin.jvm.internal.Intrinsics.checkNotNullExpressionValue(v1_1, context);
this.b(v1_1, 2131952415);
v9_1.addProperty(InvalidReportAbuseType, p12);
com.microsoft.instrumentation.util.ClientAnalyticsSession.getInstance().logEvent(v9_1);
}
...
Let's try iOS, probably it is safer!
Before iOS 7, implementing a native bridge on iOS is slightly more complex than it is for Android: There are no explicit API methods defined for this purpose.
The common way used to work by overloading the URL loading system so that arbitrary messages can be passed from JavaScript to a callback in the native UIWebView
.
Any time a URL is loaded within the WebView
it invokes the shouldStartLoadWithRequest
delegate method, which intercepts the full URL, including any parameters.
The format of the URL is typically used to pass messages from JavaScript to the native container.
For example, the following may be used to find a name in the list of employees :
window.location = mysafebridge://employees/search/contact?firstname=john
The native container then implements the shouldStartLoadWithRequest
delegate of the WebView
using code similar to the following:
- (BOOL)webView:(UIWebView*)webView
shouldStartLoadWithRequest:(NSURLRequest*)request
navigationType:(UIWebViewNavigationType)navigationType {
NSURL *URL = [request URL];
if ([[URL scheme] isEqualToString:@"mysafebridge"]) {
// parse URL, extract host and parameters to define actions
}
}
The shouldStartLoadWithRequest
method would typically read in the URL, then separate and interpret each of the URL components to determine what actions it should take.
The URL loading technique, however, provides only a one-way bridge from the web layer to the native container. It is
possible to create a bi-directional communication channel using a JavaScript callback and the stringByEvaluatingJavaScriptFromString
method of the UIWebview
class.
For example, to execute a JavaScript method from the native container you might find code similar to the following:
[webView stringByEvaluatingJavaScriptFromString: @"addEmployee('%@','%@')",firstname,job];
This simple example would cause the addEmployee()
JavaScript function to be executed, passing the NSString
objects
"firstname" and "job" to JavaScript. When used in conjunction with shouldStartLoadWithRequest
, this technique is
capable of providing a rudimentary bridge between the native and web layers.
You need to be careful when using a custom URI schemes, since an attacker can share a malicious link (via email, chat or SMS) and malicious JavaScript or HTML code will invoke native mobile functionality and exploit its data.
Note: UIWebView
JavaScript execution limits total allocations to 10MB and runtime to 10 seconds, at which point
execution will be immediately and unequivocally halted.
To overcome the limitations of UIWebView
, iOS 7 is shipped with the JavaScriptCore
framework which has full support
for bridging communications between native Objective-C and a JavaScript runtime. The bridge is created via the new
JSContext
global object, which provides access to a JavaScript virtual machine for evaluating code. The Objective-C
runtime can also obtain strong references to JavaScript values via JSValue
objects.
The JSExport
protocol allows applications to expose entire Objective-C classes and instances to JavaScript and operate on them as if they were JavaScript objects.
Defining variables and methods within a protocol that inherits JSExport
signals to JavaScriptCore1
that those elements can be accessed from JavaScript:
@objc public protocol CarJSExports : JSExport {
var model: String { get set }
var year: String { get set }
var price: NSNumber? { get set }
var fullDetail: String { get }
static func createWith(model: String, year: String) -> Car
}
In the example above, JSExport
protocol declaration allows Javascript to access the variables model
and year
and the function createWith
.
Now that JavaScriptCore knows about the CarJSExports
protocol, it can create an appropriate wrapper object when you add an instance of it to a JSContext
:
@objc public class Car : NSObject, CarJSExports {
public dynamic var model: String
public dynamic var year: String
public dynamic var price: NSNumber?
public required init(model: String, year: String) {
self.model = model
self.year = year
}
public class func createWith(model: String, year: String) -> Car {
return Car(model: model, year: year)
}
public var fullDetail: String {
return "\(model) \(year)"
}
}
let context = JSContext()!
context.setObject(Car.self, forKeyedSubscript: "Car" as NSString)
context.evaluateScript(#"""
function loadCar(json) {
return JSON.parse(json)
.map((attributes) => {
let car = Car.createWithModelYear(attributes.model, attributes.year);
car.price = attributes.price;
return car;
});
}
"""#)
let json = """
[
{ "model": "Tesla", "year": "2020", "price": 999 },
{ "model": "Toyota", "year": "2220", "price": 998 },
{ "model": "Mercedes", "year": "2222", "price": 909 }
]
"""
guard let loadCar = context.objectForKeyedSubscript("loadCar"),
let cars = loadCar.call(withArguments: [json])?.toArray()
else {
fatalError()
}
for car in cars {
let model = (car as! Car).model
NSLog(model);
}
With this implementation, it becomes easy to expose objects to JavaScriptCore
, this is why developers should make
sure to expose only necessary data and not mirror the JSExport
definition, and the initial data model.
For example, if the function fullDetail
might contain sensitive data, it should not be declared in CarJSExports
protocol and should be only declared in the Car
class definition.
Can I read one more file?
In most SDK and WebViews
components, it is by default allowed to load files from the filesystem. This poses a risk
when a malicious application is able to open local files within another application's WebView
. This opens the exposed WebView
to a myriad of exploitation techniques, from abusing accessibility settings to same origin bypass.
On Android, you can disable filesystem access from a WebView
as follows on Android:
webview.getSettings().setAllowFileAccess(false);
This will not stop the WebView
from being able to load file the application's resources or assets folder using
file:///android_res
and file:///android_asset
. To lock down the WebView
, you should not allow loaded
files from the filesystem to access other files. This will restrict the loaded page in exfiltrating
private files.
webview.getSettings().setAllowFileAccessFromFileURLs(false);
webview.getSettings().setAllowUniversalAccessFromFileURLs(false);
Furthermore, you can protect a WebView
from being able to access content providers on the device by using the following setting:
webview.getSettings().setAllowContentAccess(false);
Below is a vulnerable example where an attacker uses a malicious link to read sensitive data from the application's directory. The application writes cleartext password to shared preferences:
SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("password", "MyBadPassword");
editor.apply();
And, the application uses a Webview
to display a URL from an intent or, a user input.
String badUrl = getIntent().getStringExtra("URL");
WebView webview = findViewById(R.id.webview);
WebSettings webSettings = webview.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setAllowFileAccessFromFileURLs(true);
webview.setWebChromeClient(new WebChromeClient());
webview.loadUrl(badUrl);
In this case, both JavaScript and AllowFileAccessFromFileURLs
are enabled. The malicious file test.html
reads the
shared preferences file MainActivity.xml
and is able to exfiltrate it.
function readTextFile(file)
{
var rawFile = new XMLHttpRequest();
rawFile.open("GET", file, false);
rawFile.onreadystatechange = function ()
{
if(rawFile.readyState === 4)
{
if(rawFile.status === 200 || rawFile.status == 0)
{
var allText = rawFile.responseText;
// send allText to external link
}
}
}
rawFile.send(null);.0
};
By accessing the URL within the application, we can see that the malicous code is executed, and the content of the file is accessible:
Summary
Webview
is an important component of mobile apps. It provides a lot of flexibility to access and interact with
external resources, but with it comes many security challenges.
To sum up, ensure to use the latest SDK version and avoid deprecated components, be careful when enabling JavaScript code or implementing native to JavaScript bridges. Features should be restricted the least required.
I hope you found this useful and do not hesitate to test your application on Ostorlab.
We do newsletters, too
Get the latest news, updates, and product innovations from Ostorlab right in your inbox.