Deep dive into the security of Progressive Web Apps

In order to expand existing web applications to mobile and desktop environments,  more and more web developers are creating Progressive Web App (PWA) versions of their web applications. PWAs, originally proposed by Google in 2015, leverage the latest web standards to offer a native-like experience for both mobile and desktop applications.

PWAs combine the best parts of the web and native applications to create applications which:

  • Have improved performance;
  • Do not need to be installed or updated;
  • Are independent of any app store;
  • Security is handled by the browsers;
  • Are platform independent;
  • Are easy to develop;
  • Can be used when offline;
  • Allow for push notifications;

Given all these benefits, it’s not a surprise that big technology firms are moving into the space of PWAs. Some examples are Outlook Web Access, Twitter Lite and Pinterest. You can easily check these out by visiting them in your browser and clicking the ‘Add to Home screen’ button or + icon in the URL bar (depending on the browser and operating system). The browser will prompt you to install the application. If you agree, the app will be installed as if it is a native application on your mobile phone or desktop. In case of a mobile phone, a new application icon will be added to the home screen, while the desktop shows a new entry in the list of installed applications:

Even though PWAs have many benefits, there are some downsides to using this approach:

  • PWAs are unable to use hardware specific features on both mobile devices (fingerprint scanner, NFC, Bluetooth, …) and desktops (Bluetooth, Touch Bar,…). PWAs are restricted to the capabilities of the browsers while native applications or cross-platform desktop apps written in Electron or React Native are able to leverage native APIs;
  • Native applications allow for more advanced security measures such as certificate pinning and app store security verification and multifactor authentication.

New technology often implies new attack vectors. Therefore it might be of interest to take a look into the security of this type of web applications. Since PWAs are in fact enriched web applications, all of the known web attacks are still applicable. Nonetheless, PWAs take advantage of new technologies and features of which come with their own security responsibilities.

Structure of PWAs

As explained in the previous section, progressive web apps are enriched web applications leveraging different kind of new web technologies to make more powerful applications. Instead of perceiving PWAs as a new type of application, one might think of a PWA as a normal web application with the addition of the following features specified in HTML5:

  • A manifest;
  • One or multiple service worker(s).

The manifest is a JSON file which provides the necessary information to download the app and present it to the user as if it was a native app. It includes information such as the name, description, icon and display options of the PWA.

The service worker on the other hand adds more functionality to a PWA. This is a JavaScript file that runs in the background of the web page and allows developers to add more “app-like” functionalities to their PWA. Some of the service worker functionalities include push notifications, offline browsing (made possible by caching) and background syncing.

We will take a closer look at these two concepts in the following sections.

PWAs vs Cross-Platform Desktop Apps

Similar to PWAs, cross-platform desktops apps written in Electron are gaining ground. Some popular examples are Slack, Github Desktop and Visual Studio Code. But what’s the difference between these hybrid desktop apps and PWAs?

For one thing, PWAs are pure web applications. Their client-side code consists solely out of JavaScript and HTML and they inherit all usability, performance and security capabilities from the browsers running them.
Electron apps are written in JavaScript, HTML and additionally, native code. They leverage Chromium’s rendering engine, Node.js and native APIs for native operations.
Because of this, desktops apps are able to use all functionalities of the operating system while PWAs live in a browser environment and are restricted by the capabilities of the browsers.

While PWAs seem to be more limited and less flexible, their simplicity has several benefits compared to cross-platform desktops apps:

  • Installing a PWA is seamless and updates are applied automatically;
  • Security is handled by the (well protected) browsers;
  • The size of PWAs is usually much smaller;
  • PWAs can be used for building mobile applications.

Browser Support

PWAs do not depend on a single API, but use various technologies to deliver a seamless web experience. The main driving force behind PWAs are the service workers. Currently, service workers are supported on all major browsers on mobile and desktop.

Browsers supporting Service Workers (image taken from Caniuse)

The web app manifest on the other hand, which adds the ‘add to home screen’ functionality and a custom app icon has a wide support on mobile browsers. However on desktop only Chrome is currently supporting this feature.

Manifest

Each PWA includes a web app manifest which defines the appearance of the PWA when the app is “installed” on the user’s mobile device or desktop. These properties include aspects such as name, background color, app icon, display options, etc. When visiting a website which has imported a manifest, a new option will be visible in the browser which allows the user to add the PWA to the home screen as an app icon.

A shortened version Twitter’s web app manifest:

{  
   "background_color":"#ffffff",
   "description":"It's what's happening. From breaking news and entertainment, sports and politics, to big events and everyday interests.",
   "display":"standalone",
   "icons":[  
      {  
         "src":"https://abs.twimg.com/responsive-web/web/icon-default.3c3b2244.png",
         "sizes":"192x192",
         "type":"image/png"
      },
      ...
   ],
   "name":"Twitter",
   "short_name":"Twitter",
   "start_url":"https://twitter.com/?utm_source=homescreen&utm_medium=shortcut",
   "theme_color":"#ffffff",
   "scope":"/"
}

PWAs have different display modes, which can be declared in the manifest. The default mode is to display the PWA as a standalone app. Here the PWA looks and feels like a native app. Another possibility is to use the ‘browser’ mode where the standard browser experience will be retained, including the URL bar.

The manifest declares a “start_url” parameter, which is the URL that will be opened when launching the app. This could either be an absolute URL, or a relative URL. In case of a relative URL, the domain is the same as the domain from which the PWA was installed.

Another interesting entry in this manifest is the scope. The scope defines the boundaries of the PWA. Any URL that falls within this scope will be handled by the PWA. This is very similar to iOS’s Apple-App-Site-Association and Android’s App Links that allow a mobile application to handle specific URLs when visiting them on the web. Do note, if a user navigates out of this scope, for example by clicking a link inside the application, the requested page will render within the existing PWA window unless “target=”_blank”” is added to the link tag.

A web app manifest is deployed by creating a link tag in the head section of the HTML document. For example:

<link rel="manifest" href="./manifest.json">

Here, the manifest is hosted on the same origin since it specifies a relative path. However, it is also possible to host the manifest on an external origin such as a content delivery network (CDN) by specifying an absolute path. In this case, CORS should be properly implemented such that the PWA is able to fetch the manifest from the external resource. Either the PWA includes credentials when fetching the manifest by adding the crossorigin=”use-credentials” attribute in the link tag, or the external resource allows cross-origin requests by including “Access-Control-Allow-Origin: *” in their server response.

If multiple links to a manifest are declared in the HTML document, browsers will use the first occurrence. Therefore an attacker who’s able to exploit a cross-site scripting (XSS) vulnerability in the target web application, will not be able to override the manifest. On the other hand, if the application has no manifest configured, the attacker could link a malicious manifest. While the potential impact is limited, the attacker could control the name, description and icon when the application is ‘installed’ as a PWA on either a desktop or mobile phone.

Some browsers support a new “manifest-src” CSP directive which could restrict the domains from which a web app manifest can be fetched. The list of supported browsers can be found here. For example, the following CSP configuration will only load manifests from the site’s own origin:

Content-Security-Policy: manifest-src ‘self’ 

Service workers

The main driving force behind PWAs are service workers. In its most basic form, a service worker is a programmable proxy between the frontend and the rest of the web application. It allows developers to hook certain functionalities on outgoing requests by intercepting each request initiated from within the web application. While it resides in the browser, it’s executed on a separate thread and does not have direct access to the DOM or UI. Its main purpose is to perform non-UI tasks such as caching, handling notifications and improving performance.

A detailed guide on the internal workings of service workers is beyond the scope of this blog post. Instead, we will cover the most important aspects together with their security implications. More information can be found on Google Web Fundamentals.

In order to install a service worker, it should be registered using the navigator.serviceWorker API and passing the service worker JavaScript file as argument.

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function(registration) {
    //service worker successfully installed
  }).catch(function(err) { 
    //service worker install failed
  });        
}

The register method only accepts relative URLs since service workers must reside on the same domain as the web application. This implies that a XSS vulnerability is not sufficient to install a malicious service worker, unless of course, an attacker is able to host a malicious service worker on the same origin.

Once the service worker has been registered, the ‘install’ and ‘activate’ event of the service worker will be triggered, where the developer can implement some initialization functionalities such as pre-caching. Pre-caching allows the application to already cache certain content to make the application more responsive. For example, OWA could already cache all the emails from the inbox, as those will most likely be opened right after starting the application.

After the installment, the service worker can be in any of the following 3 states:

  • Fetch/message: The service worker is actively running in a different thread and fires ‘fetch’/’message’ events upon each network request performed by the web application. Here, the developer could granularly implement caching for each request and return cached responses to support offline availability.
  • Idle: The service worker is waiting for incoming events;
  • Terminated: After the service worker has been idle for 30 seconds, it becomes terminated. However it can become idle as soon as it receives new events. Chrome also detects long-running service workers and terminates them.
Image taken from Google Developers

Once a service worker is installed, it is capable of intercepting both same-origin as well as cross-origin requests. Upon each request initiated from within the application, a ‘fetch’ event is fired inside the installed service worker. A typical implementation would be to configure a service worker which listens for this event and caches specific (regularly used) resources. For example, the PWA version of a music player app might intercept and cache your 10 most played songs such that they become available when you’re offline.

The following code snippet shows an example of a service worker logging all requested URLs in the console and subsequently leveraging the browser’s Cache Storage to cache all responses which have not been previously cached.

self.addEventListener('fetch', (event) => {
  var request = event.request;
  //Logging each request to the console
  console.log(request.url)

  //Tell the browser to wait for newtwork request and respond with below
  event.respondWith(
    //If request is already in cache, return it
    caches.match(request).then((response) => {
      if (response) {
        return response;
      }
      //if request is not cached, add it to cache
      return fetch(request).then((response) => {
        var responseToCache = response.clone();
        caches.open(cacheName).then((cache) => {
            cache.put(request, responseToCache).catch((err) => {
              console.warn(request.url + ': ' + err.message);
            });
          });
        return response;
      });
    })
  );
});
Service Worker logging all requested URLs in Google Chrome’s console.

While PWAs often heavily rely on the Cache Storage in order to improve performance and offline availability, developers should refrain from storing confidential information inside the cache. If they do store any confidential information, it’s up to the application to delete any confidential entries in a timely manner since the browser will not delete them (unless the maximum amount of cache storage has been reached). For example, if the user chooses to log out of the application, the local cache should be cleared. Both the Cache Storage and IndexedDB can be accessed by a service worker to store data.

Requests and responses residing in the Cache Storage in Google Chrome

Do note that a service worker adheres to the same-origin policy (SOP) where it runs in the same context as its caller. Hence, when cross origin requests are made, the origin on which the resources reside should permit cross-origin requests (i.e. by adding “Access-Control-Allow-Origin: *” in their server response).

Service worker are installed by the web application without any client permission or without showing a notification. Combined with the fact that service workers live indefinitely and are not removed once you close the browser, users are often unaware if a certain service worker is installed for a web application.
In Chrome, a user could open the developer tools and click on ‘Application’ > ‘Service Workers’ to consult all installed service workers. For each service worker, the domain, source file and status is shown. Here the user could unregister any unwanted service workers.
If you want to disable service workers in general, this can be done by disabling storage in chrome://settings.

Service Workers tab in Google Chrome Developer Tools

Malicious server workers

As explained earlier, a service worker acts as a network proxy between the frontend and backend of the web application. This is really powerful and an interesting objective for an attacker since it could allow him to hijack connections or serve modified responses. Therefore, it is of utmost importance that this service worker is trusted and can not be modified by an attacker. If at some point the attacker is able to take control over the service worker, it would result in a persistent man-in-the-middle (MiTM) attack. To minimize the exposure of the service workers, they can only be registered on pages served over HTTPS ensuring the received service workers had not been tampered with.

Nonetheless, the following two attack vectors could allow attackers to install a malicious service worker for a specific domain in the browsers of their victims:

XSS and File-upload

For this attack the web application should be vulnerable to both an XSS attack and allow users to upload their own files and directly access them through a link on the same domain. This approach can be used when a websites implements a very secure content security policy that does not allow inline scripts and only allows scripts from a number of whitelisted resources. Since service workers also can’t be loaded from an external domain, a file upload is also needed. If the application satisfies both conditions, Shadow Workers could be leveraged to exploit the application. The Shadow Workers project is a combination of a malicious service worker, and a C&C server. If an attacker is able to install the service worker in the browser of its victim, the attacker could proxy through the service worker and browse on the compromised application as the victim. Additionally, the service worker includes post exploitation modules, which for example allows the attacker to execute a localhost port scan.

Dormant service worker

In the dormant service worker attack, a user unknowingly installs a malicious service worker on localhost on a specific port. This could be achieved by social engineering the victim into running malicious code or hiding a malicious script on a project serving a different purpose. From then on, the service worker remains in effect on localhost and will idle until it is explicitly unregistered. Afterwards, when the user runs a different application on localhost and the same port, the dormant service becomes active and allows the attacker to perform a MiTM attack.

Do note service workers are enforced to be installed over HTTPS, however localhost is an exception from the strict security policy to permit easy development.

The dormant service worker attack is documented in a vulnerability report on HackerOne where a malicious service worker is installed on http://localhost:8080 to attack an Augur client.

Impact

In order to limit the impact of malicious server workers, the following security features are built in:

  • Service workers do not have access to the DOM (and thus the cookies); Instead, a service worker can communicate with the pages it controls by responding to messages sent via the postMessage interface, upon which those pages can manipulate the DOM if needed. However, the application should support this in order to work.
  • Service workers only have access to the Cache Storage and indexedDB for caching purposes but not the local or session storage;
  • Service workers are unable to read and set a set of forbidden headers (they might include session identifiers or other valuable information).

An attacker able to install a malicious service worker could pursue one of the following paths:

  • Actively man-in-the-middle all the traffic between the frontend of the victim and the backend of the vulnerable application. For example, instead of returning the actual response, an attacker could return a redirect to a phishing website.
  • Discovered and researched by P. Papadopoulos et al. malicious service workers could form a persistent web based botnet, a so called MarioNet, which could be leveraged for various malicious purposes like crypto-mining, DDoS attacks, distributed password cracking and much more

Conclusion

PWAs take advantage of the latest technologies to combine the best of web and native apps. Thanks to their ease of development and platform independence, where PWAs can both be installed on mobile devices and desktops, they are quickly gaining ground. Popular services like Outlook, Pinterest, Spotify and Twitter already built PWA versions of their apps.

At their core, PWAs are enriched web applications taking advantage of new HTML5 features like the web app manifest and service workers. Therefore all our existing web security knowledge is still applicable and penetration testers can tackle a PWA as if it was a normal web application by following standard web security methodologies. Since PWAs often heavily rely on caching to increase performance and support offline availability, the caches stored in the browser might benefit from some additional examination.

About the author

-tRU0XaV_400x400

Vincent De Schutter is part of the NVISO Cyber Resilience team where he is mainly researching web and mobile applications. During his off-hours he tries to further expand his knowledge by participating in various bug bounty programs. You can find Vincent in the gym on Twitter or LinkedIn.

Leave a Reply