Why XSS Attacks Are More Dangerous for Capacitor/Cordova Apps

10 min read

Originally published February 15, 2021

I want to preface this article by saying that the vulnerability we will be discussing does not mean that a "hybrid" application built with Capacitor/Cordova is insecure. This vulnerability is also not limited to Capacitor/Cordova, it would apply to any native application that uses a web view that implements a Javascript interface, or "bridge", from the web view to Native APIs.

Ideally, it should not be possible to execute an XSS (Cross-Site Scripting) attack in your application because such attacks would have been appropriately protected against. However, if there is an XSS vulnerability in your application, the impact to the user could potentially be much worse when that vulnerability is exploited in a hybrid mobile application (rather than just a standard website).

In this article, we are going to demonstrate how a successfully executed XSS attack in a Capacitor application could allow the attacker to track the users location using the native Geolocation API. Although we will demonstrate this vulnerability with the Geolocation API, the same attack could be used to access any Native API that the application has access to.

NOTE: It is worth mentioning that this article is using version 2.x of Capacitor. This means that all of the core plugins would be available to attack by default because Capacitor 2.x includes all of the core plugins by default (e.g. just by installing Capacitor an attacker would have access to these). Capacitor 3.x will require core plugins to be installed individually, which will help limit the impact of an attack like this - the attacker would only be able to attack plugins that have actually been installed. I have not tested Capacitor 3.x myself yet to confirm that this is the case, but I would assume this additional installation step would prevent access to an attacker.

Outline

Before we Get Started

This article is going to continue on from my previous article on XSS vulnerability: Protecting Against XSS (Cross Site Scripting) Exploits in Ionic. If you do not understand what an XSS attack is or how it works, it would be a good idea to read that article first.

When we get to the demonstration of the attack we will be taking the same XSS attack example we demonstrated in the Angular application in the previous article, and modifying that to instead grab the user's location periodically through the Geolocation API.

How Capacitor/Cordova Break out of the Webview Sandbox

The Webview is designed in such a way that it is sandboxed from the rest of the application and operating system. For example, if you go use a web view to visit https://google.com, the Javascript on https://google.com won't have the permissions required to interact with the underlying operating system. It wouldn't be able to access files, contacts, or any other native functionality. This is also the case with a normal browser like Google Chrome that you would run on your desktop computer.

However, the unique thing about "hybrid" applications is that whilst the majority of the application runs inside of a web view, the application is still able to access native functionality outside of this "sandbox". This is achieved through the core mechanism that allows Capacitor/Cordova to work: a Javascript interface, or "bridge", that allows Javascript code from within the web view to make calls to native code and receive data back.

The ability to create this bridge is what allows a hybrid application to break out of its sandbox. You don't need to add this bridge yourself, this is done by default when you install Capacitor/Cordova. However, just to get a sense of what is going on here let's take a brief look at how the bridge is implemented.

On Android, this is achieved through calling the addJavascriptInterface method on the web view:

webView.addJavascriptInterface(this, 'androidBridge');

On iOS, this is achieved through calling the evaluateJavaScript method on the web view:

  self.getWebView()?.evaluateJavaScript(wrappedJs, completionHandler: { (_, error) in
      if error != nil {
          CAPLog.print("⚡️  JS Eval error", error!.localizedDescription)
      }
  })

This "bridge" is like having backdoor access into a secure location which can be used for both good and nefarious purposes. To continue this analogy, you might imagine that we have a guard stationed at this door protecting it from bad actors. However, if this guard fails to do their job (e.g. an XSS vulnerability is able to be exploited in the application) then the attacker can make use of the same door back door you are using.

This creates two unique problems for hybrid applications like those built with Ionic and Capacitor/Cordova, or with any native application that makes use of a web view with a Javascript interface:

  1. The consequences for a successful XSS attack can be more severe for the user, potentially giving the attacker access to Contacts, Camera, SMS, GPS location and more
  2. The avenues for executing an XSS attack are greater, as the malicious code could be injected from the native code into the webview

The main focus of this article is on the first point, but consider that since we sometimes make use of data coming from a Native API to display in our application, that creates additional avenues for code to be injected into our application. For example, a standard stored XSS attack might rely on storing a malicious Javascript string in an SQL database for the application, which would then later be pulled from the database into the application to be displayed, at which point the code would be executed. If we also have data that is being pulled in from Native APIs, then there is potential for code to be injected in other creative ways.

These are not methods I have personally verified, but you could imagine storing a malicious Javascript on a Contact that is being pulled into the application, or a QR code that is being scanned.

How the Attack Works

As the developer of the application, we would make use of native functionality with whatever nice packages we have to work with that will make our code clean and organised. If we want to use the Geolocation API, then we would do something like this:

import { Plugins } from '@capacitor/core';

const { Geolocation } = Plugins;

class GeolocationExample {
  async getCurrentPosition() {
    const coordinates = await Geolocation.getCurrentPosition();
    console.log('Current', coordinates);
  }
}

However, there is no need for an attacker to deal with that level of complexity. All plugins will be available through the global Capacitor object. This means that, if you want to, you can just access native functionality directly with a call like this:

Capacitor.Plugins.Geolocation.getCurrentPosition();

You could even just run this code directly in the DevTools console. It then becomes easy to modify the XSS attack we demonstrated in the previous article to make use of this API:

home.page.ts

import {
  Component,
  AfterViewInit,
  ViewChild,
  ElementRef,
  Renderer2,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements AfterViewInit {
  public maliciousString: string;

  @ViewChild('userContent', { static: false }) userContent: ElementRef;

  constructor(private renderer: Renderer2, private sanitizer: DomSanitizer) {
    this.maliciousString = `<img src=nonexistentimage onerror="setInterval(function(){Capacitor.Plugins.Geolocation.getCurrentPosition().then(function(position){console.log('Hacked position:', position)})}, 10000);" />`;
  }

  ngAfterViewInit() {
    // DANGER - Vulnerable to XSS
    this.renderer.setProperty(
      this.userContent.nativeElement,
      'innerHTML',
      this.maliciousString
    );
  }
}

home.page.html

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title> XSS Example </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Blank</ion-title>
    </ion-toolbar>
  </ion-header>

  <div id="container">
    <div #userContent></div>
  </div>
</ion-content>

This example uses an unsafe method of setting the innerHTML property, which would allow the XSS attack to successfully execute. Let's take a closer look at the malicious code that is being executed in the onerror error handler of the fake image being injected:

setInterval(function () {
  Capacitor.Plugins.Geolocation.getCurrentPosition().then(function (position) {
    console.log('Hacked position:', position);
  });
}, 10000);

We are using a setInterval to automatically run this code every 10 seconds, which will make a call to getCurrentPosition and log out the result to the console. This means every 10 seconds, the coordinates of any user who viewed this malicious image through the application would have be displayed in the console. If you read the previous article, you will see this can easily be extended to send that coordinate data off to the attackers server rather than just logging it out to the console.

Mitigating Against the Attack

Again, this vulnerability does not mean that hybrid applications are inherently insecure. It just means that the potential damage for a successfully executed XSS attack is greater. We can protect against these attacks in the same way that we protect against any other XSS attack, but there also some other steps unique to this situation that we can take:

  1. General XSS prevention - Use the same techniques and best practices as a normal website for protecting against XSS attacks
  2. Follow the principle of least privilege - Only install plugins for functionality you application actually needs to use, and request the smallest set of permissions that your application needs to do its job
  3. Review and remove any plugins that your application no longer needs to make use of
  4. Consider the potential impact for harm of any plugin you install - if an attacker gained access to this plugin, what could they do? Is there a safer plugin you could use or develop to achieve what you need?

The potential for harm to your users if this attack is executed successfully is quite large, so we should all do what we can to help fight for the users.