Description
This is a security issue. We tried to reach out to security@vuejs.org two weeks ago but didn't get any reply. Please check the original email for attachments.
Vulnerability Description
In devtools-background.js
, there is a code injection in the toast
function. It can be triggered by postMessage from any tab, which results in universal XSS upon opening the browser's developer tools(F12). An attacker can host a specially crafted web page to exploit this vulnerability, then convince a user to view the web page and open developer tools(F12) in other Chrome tabs.
Technical Details
In the manifest.json
of a Chrome extension, there are three types of Javascript files to run: background
, content_scripts
and devtools_page
. The first type is not involved in this vulnerability so we will ignore it.
content_scripts
is injected into the web page that meets the URL requirements. One thing to note is that vue-devtools does not explicitly set "all-frames" to true
, so content_scripts
will only be injected into the topmost frame in the tab.
devtools_page
will be loaded when the user opens the browser's developer tools (by hotkey F12
or Ctrl+Shift+I
or just right click -> "Inspect").
One of the extension's content_scripts detector.js
added an event listener for message event. It checks if the message is sent from current frame and has vueDetected
set to true, then it forwards our message to chrome.runtime API.
window.addEventListener('message', e => {
if (e.source === window && e.data.vueDetected) {
chrome.runtime.sendMessage(e.data)
}
})
When user opens the browser's developer tools, the devtool_page devtools-background.html
will be loaded. It simply embeds devtools-background.js
in a script tag. This script handles messages sent from chrome.runtime.sendMessage
.
chrome.runtime.onMessage.addListener(request => {
if (request === 'vue-panel-load') {
onPanelLoad()
} else if (request.vueToast) {
toast(request.vueToast.message, request.vueToast.type) // vulnerable
} else if (request.vueContextMenu) {
onContextMenu(request.vueContextMenu)
}
})
In the toast
function, we can see the params message
and type
are introduced into src
using template string, then src
gets executed in the "inspectedWindow".
function toast (message, type = 'normal') {
const src = `(function() {
__VUE_DEVTOOLS_TOAST__(\`${message}\`, '${type}');
})()`
chrome.devtools.inspectedWindow.eval(src, function (res, err) {
if (err) {
console.log(err)
}
})
}
Since message
and type
are not sanitized before they are added to src, we can craft a special message to escape from __VUE_DEVTOOLS_TOAST__
function, and inject arbitrary javascript code to execute.
Please note that message passing using chrome.runtime.onMessage
and chrome.runtime.sendMessage
is available for any part of one extension. That means it is possible to send specially crafted messages from one tab and run the injected javascript code on the other tabs.
This image describes how the messages are processed in this vulnerability:
Additional notes
-
Evaluating Javascript by
chrome.devtools.inspectedWindow.eval
is equivalent to evaluating in developer tools' console. It is not restricted by content security policy (CSP). You can test it on this page, where I set the CSP todefault-src 'none'
. -
The beta version of vue-devtools has not implemented
__VUE_DEVTOOLS_TOAST__
, buttoast
function remains untouched. That means it is necessary to add the function declaration to make the PoC work for both versions since function declaration in one code block will be evaluated first:
-
Developer tools window remains open on navigation. An attacker could navigate the tabs to any http(s) URL and execute Javascript on any domain.
Proof of concept
Please follow these steps to reproduce:
- Install and enable Vue.js devtools in Chrome
- Host
poc/poc1.html
in a webserver (live version), and visit it in the browser, do not close the tab. - Press F12 in any other tab that has navigated to an http(s) web page,
alert(document.domain)
will popup.
The other one poc/poc2.html(live version) iterates through some URLs and executes alert(document.domain) on each of them. Videos of both PoCs(live version: PoC1 PoC2) are attached in the poc folder.