This article covers a collection of techniques that allow me to build HTML5 applications with native capabilities, and iterate on my code using the edit-save-refresh cycle that is characteristic of Web development.
I love building Web applications, and I’m addicted to the edit-save-refresh cycle.
At the same time, I miss many features that mobile app developers take for
granted, but will take years to get through the Web standards gauntlet, such
as WakeLock and
Push notifications. Security decisions that
make perfect sense in the context of Web pages become really limiting for
applications. For example, it is impossible to build an alarm clock, because
play() method on HTML5 media elements (
user interaction, at least on
HTML5 application wrappers rose to respond to this challenge. A wrapper
essentially bundles a browser component (usually known as a
WebView, and the HTML, CSS and JS code that you supply. The first wrapper that
I am aware of is Titanium.
Today’s favorite wrapper, Apache Cordova, has made a lot of progress on the tooling front, but still uses the same bundling technique, which means that I have to build a native application, deploy it on my mobile device, and restart, whenever I want to make a change.
Outline and Code
The following sections will describe the tricks I used to restore the edit-save-refresh cycle inside a Cordova application container.
Some tricks in this article can be repurposed for other wrappers easily, while some tricks are specific to Cordova’s implementation. The latter may still suggest approaches to solving similar problems in different wrappers.
I used the methods described here in the html5-app-container project, which is open-sourced on GitHub. The code samples in this article lack some of the details in the GitHub project, in the interest of brevity and readability. I consider the GitHub project code to be the production-quality version that you should use for your applications.
Chapter 1: The Naive Solution
In order to be able to use edit-save-refresh, I have to get the Cordova container to load my application code from a server. The naive solution would be to simply navigate to the server’s URL from the application’s main page.
While the application gets the edit-save-refresh cycle, it loses all the native
Cordova functionality. This is because the native functionality uses the
communicate with the native Cordova code. The Cordova bridge is initialized by
loading a platform-specific
cordova.js file that is placed along the HTML5
application’s files by the Cordova build chain.
At this stage, it is tempting to attempt to load
cordova.js from the Web
site. This could work on Android, where the HTML5 application is accessible
file:///android_asset/www/index.html, but is doomed to fail on iOS,
because the application path has a GUID in it, e.g.
Even if the complex path on iOS can be tamed, Chrome’s remote inspector
reveals that accessing a
file:///android_asset resource from a
causes a security error, as shown below.
Before moving on to the next attempt, it’s worth noting that even the naive
solution provides advantages over running the application from a Web browser.
Cordova’s WebView uses a security model that is better suited for applications,
play() method of HTML5 media elements can be called without user
Furtermore, the Crosswalk project offers a Cordova fork that uses a WebView built on Chromium, instead of the platform’s native WebView. This approach removes a lot of variability coming from the wide range of WebView versions shipped by device firmware. I can’t recommend it strongly enough! In fact, I wasted a few months trying to build pretty much the same thing, before this existed.
Last, both Cordova and Crosswalk support debug and release builds. The debug builds set up the WebViews so that they can be debugged remotely. On iOS, the Cordova WebView can be debugged by Safari’s Remote Inspector. On Android, the Cordova WebView and the Crosswalk WebView can be debugged using Chrome’s Remote Debugger.
In both remote debuggers, hitting the Refresh keyboard shortcut (Command+R on Mac OS X, Control+R on all other systems) reloads the WebView. This is an important piece of the edit-save-refresh cycle, as having to reach out for the phone and close/reopen the application would be much slower than hitting Refresh on my development machine.
Chapter 2: The Iframe
<iframe> element has introduced many security issues in the Web platform,
so it seems fitting that we’d try to use it to work around Cordova’s
A reasonable design would be to have the actual application running inside a
<iframe> takes up the host page’s entire display
space. The host page is the default Cordova starting page,
www/index.html, so it can load
cordova.js and talk to the Cordova
The host page uses
to communicate with the application running inside the
To keep the initialization process simple, the host page first waits until the
Cordova bridge is initialized, and then loads the Web application in the
postMessage receiver only accepts messages whose origin matches the Web
application. This prevents hostile content, such as embedded ads, from
obtaining native capabilities.
The receiver shown above is simple by design. It essentially
received message and responds with a message that contains the evaluation
eval means that I can iterate on all the interesting code by
changing the Web application code.
The second drawback is that debugging my application with Chrome or Safari’s
remote inspector is a bit more tedious. In order to see my page’s elements, I
have to peek into the host page’s
objects, I have to access the
handler code posted above assigins the
<iframe> element to the
property on the host page’s window, to make debugging slightly easier.
Last, I was worried that as I use data-intensive features, such as Cordova’s File API, shuttling large strings around might become a performance bottleneck.
Fortunately, we can do better!
Chapter 3: The Iframe Grows Stronger
This section describes a trick that removes the need to
to the host page in most cases, by giving the Web application inside the
<iframe> direct access to Cordova’s native objects. This makes the
approach more bearable.
In a nutshell, after the Web application loads inside the
<iframe>, it posts
a message to the host page, asking it to copy the references to Cordova’s
objects to the
<iframe>’s window object. For generality, the host page
creates an empty object, copies all the enumerable properties of its window
into the newly created object, then assigs the object to a property on the
Note that the host page still waits for the Cordova bridge to be fully
initialized before navigating the
<iframe> to the Web application. This
guarantees that the host page’s window contains Cordova’s native objects by
the time the
postMessage receiver is invoked.
Unfortunately, this trick does not work in Crosswalk 10+, so I had to look for a better solution. However, it is worth knowing, as it still works in Cordova, and might come in handy when dealing with other HTML5 application wrappers.
Chapter 4: The Demise of the Iframe
A different approach to obtaining native capabilities is to attempt to initialize the Cordova bridge in the Web application.
In this case, we don’t embed any code in the Cordova application. Instead, we
add a config.xml
<access> points to our Web application.
According to Cordova Bug 5988,
the Cordova bridge can be used from a
file:// origin, or from the top level
application, we can execute it and gain access to Cordova’s native features.
platform-specific assets directory for each platform. For example, the Android
The first file we need is
cordova.js, the build product of the
cordova-js project. As far as we’re
concerned, it implements Cordova’s module system (
cordova.require), and contains the platform-specific bootstrap code.
The second file we must extract is
cordova_plugins.js, in the same directory.
It defines the
cordova/plugin_list module, which exports a list of all the
plugins installed in the application.
The other files needed to set up the bridge are spread throughout the
plugins/ directory. A reasonable method for obtaining them is recusively
.js files inside the
plugins/ directory. In order to avoid
including any test code, we can filter out the files that don’t start with
We can collect and concatenate all the files for each platform into one big
bundle file, e.g.
android.js for Android, and
ios.js for iOS. In the Web
application, we need to detect whether we’re running under Cordova and, if so,
load the appropriate bundle.
run under Cordova. For example, the Android bundle currently uses
window.prompt() to talk to the native Cordova code. If this code runs outside
Cordova, the user will be presented with cryptic dialog boxes. In order to
avoid that, we must make sure that we run under Cordova before executing any
The code above takes advantage of Cordova implementation details, and was
derived from Cordova 3.6. On Android, Cordova defines a
object. On iOS, Cordova appends a numeric nonce wrapped in parantheses to the
If the Web application detects that it runs under Cordova, a
pointing to the appropriate script is dynamically generated and added to the
While implementing this approach, I ran over a
Crosswalk crashing bug
that brings down the entire Android application when a non-
attempts to initialize the Cordova bridge. Fortunately, I was able to find and
submit a fix.
This method addresses the main issues raised by the
<iframe> approach. The
Web application does not need to
page, and the Chrome and Safari remote inspectors display the Web application’s
From a performance standpoint, this approach has the downside of having to load
biggger. Furthermore, the file is loaded via a dynamically-added
tag, which eludes
The Rails asset pipeline sweetens the performance blow quite a bit, as the file is 26kb after minifying and gzipping, and is served with a long-lived cache expiration date. Still, it’s nice to know that the following refinement removes the performance issue completely, while being easier to maintain at the same time.
Chapter 5: The Return of the Iframe
This is mildly tricky, because the Cordova build scripts wipe the
directory inside the platform-specific
platforms/ subdirectory at the
beginning of a build. Fortunately, the script copies the platform-specific
cordova.js from the
platform_www directory inside the platform’s
fact that it cannot read
file:// resources. We bypass this limitation by
<iframe> technique discussed earlier.
The application wrapper includes a host page that loads a bootstrap page from
our Web application in a hidden
The bootstrap page receives the JS bundle from the host page via
and stores it in its
sessionStorage. Note that the bootstrap page must be
hosted by the Web application, because the
sessionStorage store is
per-origin. After the bundle is stored, the bootstrap page sends a message to
the host message telling it to navigate to the Web application’s main page. The
main page URL is contained in the message so that the application wrapper only
needs to hard-code one URL, namely the bootstrap page’s URL.
small straightforward parts. The
getBundle function uses
onMessage function implements the
used to communicate with the Web application’s bootstrap page, and is similar
to the code for the
<iframe>-based approach presented above.
code navigates the top-level window to the Web application’s main URL. So,
after the bundle transfer, the Web application takes over the entire Cordova
WebView, as opposed to being contained inside an
<iframe>, so the drawbacks
<iframe>-based solution do not apply.
This method also does not require the Web application to rely on Cordova
implementation details to detect whether it runs inside a Cordova container or
not. Instead, application code can simply probe
sessionStorage for the
Unfortunately, the code here has an XSS (cross-site scripting) vulnerability.
If an attacker convinces a user to download a malicious HTML page and open the
downloaded version, such that it gets the
file:// origin, the malicious page
can open our Web application’s bootstrap page in an
<iframe> and use the
postMessage protocol to convince the bootstrap page to store some evil
sessionStorage. If the user then visits our Web application
from the same computer, the application will run the attacker’s evil
Chapter 6: The Revenge of the Iframe
In order to fix the XSS vulnerability, we apply a well-known method for preventing against CSRF (cross-site request forgery) vulnerabilities.
The Web application’s bootstrap page generates a random token and places it
<div> attribute in its DOM tree. The application wrapper uses its
privileges to extract the token from the
<div> element, and sends the token
sessionStore if the token in the message doesn’t match the value that it
The main source of complexity in the updated bootstrap page code (shown below)
is the token generation code. We first attempt to generate a cryptographically
secure token, using the WebCrypto API.
If that is not supported, we fall back to using
Note that when the code above receives an incorrect token, it follows through
postMessage protocol, but doesn’t update
sessionStorage. This reduces
the amount of information that a potential attacker might obtain, at the cost
The host page’s code, embedded in the application wrapper, is identical to the
previous version, up to the
getIframeToken function. The main source of
complexity here is working around browser support for the
onMessage function was modified to call out to
getIframeToken and embed the return value in the message that it posts to
the bootstrap page.
The security in this method can be proven by reasoning that an attacker who can
extract the token from the bootstrap page can also extract a CSRF token
embedded in a
<meta> tag. Such an attacker can already
execute requests on behalf of its victim.
Unfortunately, just like the
contentWindow trick described in a previous
section, the code above does not work in Crosswalk 10, because of
If you want to use Crosswalk versions impacted by the bug, the host page
CSRF token, such as
0000. The bootstrap page can accept the special token if
impacted Crosswalk version.
The host page changes above make the token extraction code more robust, and
have no negative impact, from a security perspective. Therefore, they are
included in the
The Web application page gets to decide whether it accepts the special CSRF token. Therefore the modification below can simply be skipped by an application that never releases a wrapper based on a Crossswalk version impacted by the bug above. Although the changes
The code presented above has a couple of performance issues, compared to a wrapper that loads the Web application directly into a WebView. Time-permitting, they may be the subject of future work.
First, loading the bootstrap page adds an HTTP round-trip to the application’s startup time. This can be significant for applications that are meant to be used on the go. The round-trip can be eliminated from most starts by serving the bootstrap page with a far-in-the-future cache expiration time.
sessionStorage is a
source of performance issues. The
sessionStorage API consists of blocking
calls, so the WebView’s JS thread is unresponsive while the mobile device reads
sessionStorage backing store. Also,
some WebView implementations store the
sessionStorage contents in RAM, so the
minification, but is likely stored as a
UTF-16 or UCS-2 string.
So, we’re possibly taking up 200kb of RAM with a string that’s only used once
during page load.
storageSession is used in these samples because of its simplicity and
wide support. A performance-conscious Web application could use
fall back to the deprecated
Filesystem API and
WebSQL database API.
Unfortunately, all fallbacks are necessary to achieve reasonable platform
coverage. On a brighter note, the choice of bundle backing store is completely
decoupled from the code inside the Cordova wrapper, so a Web application can
deploy this performance improvement without having to change the code in
A more serious attacker could load an old (but still signed) version of a bundle with known security vulnerabilities, and exploit the vulnerabilities against the site.
General Security Considerations
Unfortunately, all the designs above have the drawback that any XSS (cross-site scripting) vulnerability can be used to obtain access to native features.
At the same time, it is worth mentioning that native applications also suffer from security vulnerabilities, such as push notifications spam and hijacking and Android intent hijacking. Furthermore, a security patch in a Web application requires a server push to be deployed, which can be done in minutes or at most hours. A mobile application patch requires an app store / market submission, which often includes a human review, and can take between hours and days.
This article described the tricks I used in the html5-app-container project, which allows me to build Web applications with the native capabilities afforded by Cordova, and iterate on my code using the edit-save-reload cycle that is typical of Web applications.