The Remote on the Local: Exacerbating Web Attacks Via Service Workers Caches

Marco Squarcina (TU Wien), Stefano Calzavara (Università Ca’ Foscari Venezia & OWASP), Matteo Maffei (TU Wien)
15th IEEE Workshop on Offensive Technologies (WOOT), co-located with IEEE S&P
May 27, 2021 (to appear)
Paper, Bibtex, Slides, Slides with transcript

Abstract

Service workers boost the user experience of modern web applications by taking advantage of the Cache API to improve responsiveness and support offline usage. In this paper, we present the first security analysis of the threats posed by this programming practice, identifying an attack with major security implications. In particular, we show how a traditional XSS attack can abuse the Cache API to escalate into a person-in-the-middle attack against cached content, thus compromising its confidentiality and integrity. Remarkably, this attack enables new threats which are beyond the scope of traditional XSS. After defining the attack, we study its prevalence in the wild, finding that the large majority of the sites which register service workers using the Cache API are vulnerable as long as a single webpage in the same origin of the service worker is affected by an XSS. Finally, we propose a browser-side countermeasure against this attack, and we analyze its effectiveness and practicality in terms of security benefits and backward compatibility with existing web applications.

Proof of Concept

SafeNotes is a static web application we developed to exemplify the attack . A demo is available here.

The application allows users to store personal notes and display them. Each note is encrypted with a user-provided password and stored into localStorage. Offline usage is enabled by a service worker that caches all static resources fetched by the browser.

We simulate the presence of a DOM-based XSS on the page other.html, e.g., /safenotes/other.html#alert(1) should trigger an alert popup. The aim of the attacker is to steal the passwords typed by the user on the main application. Using a traditional XSS, arbitrary code execution is restricted to the page other.html with no possibility for the attacker to interfere with the sensitive page index.html.

Our technique amplifies the attack surface of the application and enables the attacker to escalate their privileges to access encryption passwords. After the website is loaded and the index.html page is cached, an attacker can trick the victim into visiting this link to execute the XSS on other.html. The XSS payload, which is expanded below, uses the CacheStorage API from the page context to inject a script into the cached copy of index.html. This script registers a listener on the add note button to read the password value from the DOM and prompts an an alert message with the password when the button is clicked.

(async () => {
    let p = `<script>document.querySelector('#col-add button').addEventListener('click', (event) => {alert('Password stolen: ' + document.querySelector('#col-add input[type="password"]').value);});</script>`;
    let t = '/safenotes/';
    let c = await caches.open('static');
    let r = await c.match(t);
    let rt = await r.text();
    await c.put(t, 
      new Response(rt.replace('</body>', p + '</body>'), {
        status: 200,
        statusText: 'OK',
        headers: r.headers
      })
    );
})();

Step by step instructions to simulate the attack are as follows:

  1. Visit the safenotes application.
  2. In a new tab, execute the XSS on other.html.
  3. Go back to the safenotes application, refresh the page (ctrl-r is enough) and add a note.
  4. An alert message should display the password used to encrypt the note.

A video of the attack execution on Chrome can be downloaded at safenotes.mp4 or watched below.

This example has been tested on recent versions of Chrome, Firefox and Safari, up to Chrome 88, Firefox 86 and Safari 14.0.3. Notice that ServiceWorkers are disabled on Firefox when using private browsing mode, hence the demo requires a standard browsing session to work. On Safari, private browsing mode creates a new ephemeral session for each tab with dedicated caches isolated from each other. To account for this restriction in private browsing mode, the attack can be tested by executing all the steps (1-3) in the same tab.

Mitigation

Safenotes comes with a hardened version of the service worker that implements a simple mitigation to protect against malicious cache modifications from the page context. The hardened service worker can be activated by clicking on Enable protected SW. To compare the different behaviors, it is possible to re-enable the standard service worker by clicking on Enable standard SW.

The two service workers implement the same caching strategy. The hardened version only verifies that on matched cache objects, the url of the cached Response corresponds to the url of the event.request. If they are equivalent, then the cached response is returned, otherwise the service worker fetches the resource from the network.

--- sw.js       2021-03-09 02:11:06.046516842 +0100
+++ sw-mitigated.js     2021-03-09 02:10:45.474383769 +0100
@@ -11,7 +11,7 @@
 
 self.addEventListener('fetch', (event) => {
     event.respondWith(async function() { 
-        let cResponse = await caches.match(event.request);
+        let cResponse = await caches.match(event.request).then(r => r !== undefined && r.url === event.request.url ? r : null);
         if(cResponse) return cResponse;
         let response = await fetch(event.request);
         let responseClone = response.clone();

This simple solution provides an effective mitigation against the attack. The Response constructor does not allow to instantiate the url to an arbitrary value, which is set to the empty string in case of a synthetic Response objects added to the cache. Therefore, a mismatch is detected between the url of the request and the response and the cached entry is discarded.

Notice, however, that this mitigation comes at the cost of breaking legitimate caching patterns that make use of synthetic responses. The corresponding research paper proposes a redesign of the Cache API that would prevent by default unintended interactions between the page context and the Service Worker context, while maintaining backward compatibility with existing websites.

Attack Simulation via Chrome Extension

TamperCache is a Google Chrome extension that simulates the presence of an XSS on a website to mount the attack described in this work. The extension injects a script in the page that pollutes the Service Workers cache by appending an alert(); instruction at the end of each JavaScript file found in the cache. The attack simulation is successful if the website loads and executes the polluted files from the cache, persistently executing the alert(); instruction that we implanted inside benign scripts.

A video demonstrating the attack on the Google Developers portal and WhatsApp Web is available at googlewhatsapp.mp4 or can be watched below.

Follow these steps to reproduce the attack locally:

  1. Open Google Chrome, possibly with a fresh profile to not interfere with your active sessions:
    $ google-chrome --user-data-dir=/tmp/chrome-playground
    
  2. Download the TamperCache extension and unpack it, e.g.:
    $ cd ~/Downloads
    $ unzip tampercache.zip
    
  3. Install the TamperCache extension. Type in the address bar chrome://extensions, enable developer mode and click on Load unpacked: select the extension folder created in the previous step at ~/Downloads/tampercache and press OK.
  4. Visit any website that loads JavaScript files from the Service Workers cache, for instance Google Developers or WhatsApp Web. Notice that WhatsApp may require some interaction before filling the cache.
  5. Click on the TamperCache icon and launch the attack by clicking on Inject!.
  6. Refresh the page, close the tab or the browser. By visiting the same origin where the attack was performed, an alert message box displaying Persistent XSS should appear in case the attack was performed successfully.