OrionX Blog

Mobile security issues on cross platform frameworks - Part 2: Apache Cordova

Written by Giuliano Fasto | 4/20/23 5:06 PM

This blogpost is part 2 of our “A look at common mobile security issues on cross platform frameworks” blog post series. To read part 1 that lays the ground the rest 2 build upon please visit part 1: introduction

Cordova (formerly PhoneGap) is one of the first frameworks that allows building cross-platforms applications using HTML and Javascript. Today it’s strictly related to the actual Adobe PhoneGap and Ionic frameworks.

Take 1 - Insecure version

Let’s go straight to building a simple application. Following the instructions of the HelloWorld app tutorial (https://cordova.apache.org/docs/en/latest/guide/cli/) we install the required software and create our first project.

The standard structure leave us with very few files to edit in order to start our test app:

-www/index.html : This is the first screen we are going to see when we start the application. We can use HTML for quick and simple design of the application.

-www/js/index.js: Here we will find the Javascript code to run our application logic. For a basic application we won’t need to create or modify other js files.

-config.xml: A bit of tweaking of the configuration file is required to achieve basic interactions like network connections.

HTTPS request in Javascript

The aim of our test app is to securely download secret code from our server so let’s get started and make our first HTTPS request. 

A HTML line in the index.html file will let us add a button for the user to click:

<body>

<div class="app">

<button type="button" id="HTTPSRequestButton">Click for a HTTPS request </button>

 </div>

<script type="text/javascript" src="cordova.js"></script>

<script type="text/javascript" src="js/index.js"></script>

</body>

Let’s now move to the index.js file. We assign a function to our button and then we want to make the function as simple as possible, just like someone with a Javascript background (but not much knowledge of mobile platforms) would do:

var request = new XMLHttpRequest();

request.open("GET", "https://www.foregenix.com"); 

request.send();

Now, before trying it we need to tune a few things in the config.xml file:

<access origin="https://www.foregenix.com" />

<allow-navigation href="https://www.foregenix.com" />

Note: we also need to update the Content-Security-Policy of our index.html file. You can check this in the app full source code (link).

We fire up our application and with great surprise it works - we are mobile developers now! 

The logic is very basic and, for simplicity purposes,  there is nothing in the UI telling us that it works, we need to check the server logs.

Before adding any more code, let’s submit the app for a security review to see what our pentesters have to say:

 

Despite making use of HTTPS, the request can be easily intercepted with a man-in-the-middle attack on both the iOS and Android platforms, with the pentesting team advising us to implement SSL pinning. To perform the man-in-the-middle attack, the pentesters had to install BurpSuite’s custom CA certificate on the phone’s certificate store. 

Note: Android had introduced a control since  OS version 7 (API level 24) which won’t let apps trust custom CAs installed on the device by the user (https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html). Interestingly enough, our app bypasses this control through the use of webviews.  

While we start to look into it, we move on with the development of our application. Now that we can contact the server, we need to download the secret code.

Let’s add a listener to our XMLHttpRequest object where we will add the logic to store the secret code:

var request = new XMLHttpRequest();
request.addEventListener("load", reqListener);
request.open("GET", "https://www.foregenix.com"); 
request.send();

Storing the secret code

Lastly we define the reqListener function. A quick look into the Cordova documentation gives us a few options for storing data. The simplest one is LocalStorage, which we are already familiar with from our web app development, let’s go for that:

function reqListener () {
      console.log('Secret code received: ' + this.status);
      var storage = window.localStorage;
     storage.setItem('secret_code', this.status)
}

We now have the secret code stored (which happens to be the server response code for illustration purposes, nothing secret at all but it’s an insecure app after all) and we can retrieve it very quickly. While we were at it, we added a log to help us with debugging the application. Let’s check our app again with the pentesting team, just in case they’ve got some more comments to add:

Android Logcat: 04-07 17:44:13.996 23745 23745 I chromium: [INFO:CONSOLE(52)] "Secret code received: 200", source: file:///android_asset/www/js/index.js (52)

Insecure storage of sensitive information (which is retrieved using the commands shown in the image above) and sensitive information disclosure through logs (the content of the Android logs, obtained with the logcat command, is also shown above) on both platforms….

So far, we’ve reviewed the introductory blog post and learned something about SSL Pinning and secure storage on mobile devices.

Take 2: Secure Version

With the pentesting report in hand, we now look into our options for Cordova. 

Dealing with HTTPS interception

The official website (https://cordova.apache.org/docs/en/latest/guide/appdev/security/index.html) states that the framework doesn’t support certificate pinning. However, there are plugins to achieve this with the assumption that the plugin will be used to make all of the requests within the application. Advanced HTTP 2 seems to be one of the most common plugins, supported by Cordova, Phonegap and Ionic: (https://www.npmjs.com/package/cordova-plugin-advanced-http-2).

Installing the plugin is easy enough through the command line:

cordova plugin add cordova-plugin-advanced-http-2

Next, we rewrite our index.js code:

cordova.plugin.http.get('https://www.foregenix.com', undefined, undefined, 
               function(response) {
                         //storing the code
                },function(response) {
                     //dealing with request failure                 
                } 
);

Finally we save a copy of our server’s public certificate in the /www/certificates folder and we are almost done!

We now run our app on both an Android and an iOS device. We set up the system proxy details to point to our local proxy, just like our pentester friends taught us and we try again to perform the request. Nothing seems to happen in the apps or in our local proxy - when we check the logs on the server, it looks like no request is received.

A quick look at the devices’ log consoles will tell us why:

Android Logcat: 04-07 18:37:52.986 25945 25945 I chromium: [INFO:CONSOLE(70)] "TLS connection could not be established: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.", source: file:///android_asset/www/js/index.js (70)

iOS Console: default 15:09:30.814444 +0200 CordovaVulnApp ERROR: An SSL error has occurred and a secure connection to the server cannot be made.

As we can see, the apps now compare the local certificate with the one obtained during the request, which happens to be a certificate generated by our local proxy in order to perform the man-in-the-middle, so the apps notice that something is wrong and avoid sending the request!

Moving on to dealing with the secure storage 

Time to move to the secure storage, we quickly notice again that there is no out-of-the-box option, and we need to use another plugin. We choose secure-storage and see what happens:

cordova plugin add cordova-plugin-secure-storage

We then add the missing code in our index.js file:

cordova.plugin.http.get('https://www.foregenix.com', undefined, undefined, 
               function(response) {
                         secure_storage.set(
                            function(key) { 
                                 //success    
                            }, 
                           function(error) { 
                               //something went wrong
                           } ,"secret_code",response.status.toString()); }
                },function(response) {
                     //dealing with request failure                 
                } 
);

As you can see, all the log settings are gone too, just as you would do before promoting an application to a production release.

Time to test the app again!

The android version created a secure key/value pair in the shared preferences folder of the application private folder. According to the description on the website (link) this is generated like this:

 The SecureStorage API is implemented as follows:

  • A random 256-bit AES key is generated.
  • The AES key encrypts the value.
  • The AES key is encrypted with a device-generated RSA (RSA/ECB/PKCS1Padding) from the Android KeyStore.
  • The combination of the encrypted AES key and value are stored in SharedPreferences.

Still from the author website, we find the description for the plugin behaviour on the iOS platform:

On iOS secrets are stored directly in the KeyChain through the SAMKeychain library.

Let’s confirm this by exporting the keychain on a jailbroken device:

{"...
"Account" : "secret_code",
"Service" : "my_secure_storage",
"Access Group" : "F9P2F3WQSF.lab.foregenix.vulncordova",
...}

As we can see our secret code is now stored in the system keychain rather than in the filesystem.

BONUS TIP: Obfuscation

Just like we said in the introductory post, in a Cordova application we need to obfuscate the Javascript code contained in the js files. In order to do so we can use any of the products available on the market (there are many free options too).

The following is an example showing how our application would look when techniques to obfuscate and “uglify” the Javascript code are applied. Keep in mind that the code is still valid and will continue to work as usual, it will just make it slower for an attacker to understand what’s being done under the hood:

var _0x1d9c=['\x6f\x70\x65','\x45\x76\x65','\x74\x6f\x20','\x65\x6e\x65','\x6e\x69\x78','\x73\x65\x6e','\x47\x45\x54','\x6c\x6f\x67','\x66\x61\x75','\x74\x44\x65','\x66\x6f\x72','\x75\x65\x73','\x65\x67\x65','\x76\x65\x6e','\x6c\x6f\x61','\x61\x64\x64','\x77\x77\x77','\x68\x74\x74','\x65\x6e\x69','\x78\x2e\x63','\x2e\x66\x6f','\x72\x65\x74','\x68\x65\x20','\x74\x69\x6e','\x77\x77\x2e','\x70\x72\x65','\x73\x65\x63','\x52\x65\x71','\x69\x73\x74','\x67\x20\x74','\x70\x73\x3a'];(function(_0x374f28,_0x1d9c40){var _0x5f6fd2=function(_0x3472de){while(--_0x3472de){_0x374f28['push'](_0x374f28['shift']());}};_0x5f6fd2(++_0x1d9c40);}(_0x1d9c,0x14c));var _0x5f6f=function(_0x374f28,_0x1d9c40){_0x374f28=_0x374f28-0x0;var _0x5f6fd2=_0x1d9c[_0x374f28];return _0x5f6fd2;};function _x(_0x192e32){_0x192e32[_0x5f6f('\x30\x78\x33')+_0x5f6f('\x30\x78\x31\x36')+_0x5f6f('\x30\x78\x31\x32')+_0x5f6f('\x30\x78\x31\x31')+'\x6c\x74']();console[_0x5f6f('\x30\x78\x31\x30')](_0x5f6f('\x30\x78\x35')+_0x5f6f('\x30\x78\x31\x34')+_0x5f6f('\x30\x78\x31')+_0x5f6f('\x30\x78\x37')+_0x5f6f('\x30\x78\x30')+_0x5f6f('\x30\x78\x34')+_0x5f6f('\x30\x78\x31\x65')+'\x20\x63\x6f'+'\x64\x65\x20'+_0x5f6f('\x30\x78\x62')+_0x5f6f('\x30\x78\x31\x39')+_0x5f6f('\x30\x78\x31\x64')+'\x72\x65\x67'+_0x5f6f('\x30\x78\x31\x62')+_0x5f6f('\x30\x78\x31\x63')+'\x6f\x6d');var _0x35a612=new XMLHttpRequest();_0x35a612[_0x5f6f('\x30\x78\x31\x38')+_0x5f6f('\x30\x78\x61')+'\x6e\x74\x4c'+_0x5f6f('\x30\x78\x36')+_0x5f6f('\x30\x78\x63')+'\x72'](_0x5f6f('\x30\x78\x31\x37')+'\x64',reqListener);_0x35a612[_0x5f6f('\x30\x78\x39')+'\x6e'](_0x5f6f('\x30\x78\x66'),_0x5f6f('\x30\x78\x31\x61')+_0x5f6f('\x30\x78\x38')+'\x2f\x2f\x77'+_0x5f6f('\x30\x78\x32')+_0x5f6f('\x30\x78\x31\x33')+_0x5f6f('\x30\x78\x31\x35')+_0x5f6f('\x30\x78\x64')+'\x2e\x63\x6f'+'\x6d');_0x35a612[_0x5f6f('\x30\x78\x65')+'\x64']();}

var _0x3e0d=['\x73\x65\x63','\x65\x64\x3a','\x74\x75\x73','\x73\x65\x74','\x53\x65\x63','\x6c\x6f\x63','\x72\x65\x74','\x61\x6c\x53','\x72\x65\x63','\x5f\x63\x6f','\x73\x74\x61','\x20\x63\x6f','\x6c\x6f\x67','\x65\x69\x76','\x61\x67\x65'];(function(_0x37387d,_0x3e0d32){var _0x58e1fe=function(_0x4dfa72){while(--_0x4dfa72){_0x37387d['push'](_0x37387d['shift']());}};_0x58e1fe(++_0x3e0d32);}(_0x3e0d,0x14c));var _0x58e1=function(_0x37387d,_0x3e0d32){_0x37387d=_0x37387d-0x0;var _0x58e1fe=_0x3e0d[_0x37387d];return _0x58e1fe;};function reqListener(){console[_0x58e1('\x30\x78\x61')](_0x58e1('\x30\x78\x32')+_0x58e1('\x30\x78\x34')+_0x58e1('\x30\x78\x39')+'\x64\x65\x20'+_0x58e1('\x30\x78\x36')+_0x58e1('\x30\x78\x62')+_0x58e1('\x30\x78\x65')+'\x20'+this[_0x58e1('\x30\x78\x38')+_0x58e1('\x30\x78\x30')]);var _0x5826be=window[_0x58e1('\x30\x78\x33')+_0x58e1('\x30\x78\x35')+'\x74\x6f\x72'+_0x58e1('\x30\x78\x63')];_0x5826be[_0x58e1('\x30\x78\x31')+'\x49\x74\x65'+'\x6d'](_0x58e1('\x30\x78\x64')+_0x58e1('\x30\x78\x34')+_0x58e1('\x30\x78\x37')+'\x64\x65',this[_0x58e1('\x30\x78\x38')+_0x58e1('\x30\x78\x30')]);}

var _0x285d=['\x73\x65\x74','\x77\x77\x2e','\x2e\x63\x6f','\x70\x73\x3a','\x5f\x63\x6f','\x74\x6f\x53','\x6e\x69\x78','\x66\x61\x75','\x68\x74\x74','\x70\x6c\x75','\x70\x72\x65','\x65\x67\x65','\x74\x75\x73','\x73\x65\x63','\x72\x65\x74','\x65\x72\x72','\x67\x65\x74','\x67\x69\x6e','\x2f\x2f\x77','\x76\x65\x6e'];(function(_0x1b810b,_0x285dcd){var _0x83db2e=function(_0x349422){while(--_0x349422){_0x1b810b['push'](_0x1b810b['shift']());}};_0x83db2e(++_0x285dcd);}(_0x285d,0xd7));var _0x83db=function(_0x1b810b,_0x285dcd){_0x1b810b=_0x1b810b-0x0;var _0x83db2e=_0x285d[_0x1b810b];return _0x83db2e;};function __x(_0x587708){_0x587708[_0x83db('\x30\x78\x66')+_0x83db('\x30\x78\x34')+'\x74\x44\x65'+_0x83db('\x30\x78\x63')+'\x6c\x74']();cordova[_0x83db('\x30\x78\x65')+_0x83db('\x30\x78\x32')][_0x83db('\x30\x78\x64')+'\x70'][_0x83db('\x30\x78\x31')]('\x68\x74\x74'+_0x83db('\x30\x78\x38')+_0x83db('\x30\x78\x33')+_0x83db('\x30\x78\x36')+'\x66\x6f\x72'+_0x83db('\x30\x78\x31\x30')+_0x83db('\x30\x78\x62')+_0x83db('\x30\x78\x37')+'\x6d',undefined,undefined,function(_0x215d6e){secure_storage[_0x83db('\x30\x78\x35')](function(_0x516137){},function(_0x1f60b6){},_0x83db('\x30\x78\x31\x32')+_0x83db('\x30\x78\x31\x33')+_0x83db('\x30\x78\x39')+'\x64\x65',_0x215d6e['\x73\x74\x61'+_0x83db('\x30\x78\x31\x31')][_0x83db('\x30\x78\x61')+'\x74\x72\x69'+'\x6e\x67']());},function(_0x236ac8){console[_0x83db('\x30\x78\x30')+'\x6f\x72'](_0x236ac8['\x65\x72\x72'+'\x6f\x72']);});}

 

Conclusion

Overall we notice how quick it is to go from zero to above zero on mobile applications using Cordova. At the same time, we saw how easy it is to overlook security when we implement our application. Cordova doesn’t offer out-of-the-box security but provides an easy way to create and integrate plugins, which allows the community to contribute the security controls our software needs.

In this blog post we tried to use a few to highlight potential solutions to shortcomings. Please keep in mind there are many more available. A few things are certain though:

  • Local Storage should never be used to store sensitive data (we’ve actually seen this happening!) as it doesn’t provide any security whatsoever. Sensitive data should ideally be protected by cryptography AND stored in a secure location on the device.
  • A secure TLS configuration on the server side paired with SSL Pinning on the client side will provide the security your data in transit needs.
  • Don’t log information locally on a production application. Despite local logs becoming more and more difficult to obtain, it’s preferable to not generate them at all. .

This is the end of the second part of this blog post series. In this post we discussed the most common mistakes made when using Apache Cordova, a popular cross platform framework. Part 3 will discuss the same topics but this time when using Xamarin.