Breaking Out of Restricted Mode: XSS to RCE in Visual Studio Code

In April 2024, I discovered a high-severity vulnerability in Visual Studio Code (VS Code Cells+(?:u001b[.+?m)?Ins*[(?d+)],s*)(?line (?d+)).*/; function linkifyStack(stack: string): { formattedStack: string; errorLocation?: string } { const lines = stack.split(‘n’); let fileOrCell: location | undefined; let locationLink = ”; for (const i in lines) { const original = lines[i]; if (fileRegex.test(original)) { // … } else if (cellRegex.test(original)) { fileOrCell = { kind: ‘cell’, path: stripFormatting(original.replace(cellRegex, ‘vscode-notebook-cell:?execution_count=$’)) }; const link = original.replace(cellRegex, `‘>line $`); // [1] lines[i] = original.replace(cellRegex, `$${link}`); locationLink = locationLink || link; // [2] continue; } // … } const errorLocation = locationLink; // [3] return { formattedStack: lines.join(‘n’), errorLocation }; }
“`

createMinimalError:

“`
function createMinimalError(errorLocation: string | undefined, headerMessage: string, stackTrace: HTMLDivElement, outputElement: HTMLElement) { const outputDiv = document.createElement(‘div’); const headerSection = document.createElement(‘div’); headerSection.classList.add(‘error-output-header’); if (errorLocation && errorLocation.indexOf(‘line 6` and sets the errorLocation variable to this HTML at `[3]`. Crucially, the wildcard at the end of the regular expression it uses will swallow any text that comes after the line number, but any text immediately preceding the `Cell In` sequence will not be affected by the `replace` operation. Thus, an input like `LOLZTEXTHERECell In [1], line 6` in the ipynb. would result in the invalid markup `LOLZTEXTHEREline 6`.

In `createMinimalError`, if `errorLocation` is set and begins with `Cell In [1], line 6`, which will result in the value of `errorLocation` being ``, it will be inserted into `headerSection.innerHTML` and rendered in the webview, resulting in the JavaScript being run and `123` being logged to the console.

## Escalating to RCE

The XSS vulnerability leads to code execution within an iframe under the `vscode-app` origin, which is a frame under the main workbench window which is under the `vscode-file` origin. The main workbench window contains the `vscode.ipcRenderer` object which enables the renderer frame to send IPC messages to the main frame in order to perform filesystem operations, create and execute commands in PTYs, and so on. To access this object, we need to find a way to execute code within the `vscode-file` origin. The code for the `vscode-file` protocol handler is located in src/vs/platform/protocol/electron-main/protocolMainService.ts, with the relevant parts excerpted here:

“`
private readonly validExtensions = new Set([‘.svg’, ‘.png’, ‘.jpg’, ‘.jpeg’, ‘.gif’, ‘.bmp’, ‘.webp’, ‘.mp4’]); // https://github.com/microsoft/vscode/issues/119384 private handleResourceRequest(request: Electron.ProtocolRequest, callback: ProtocolCallback): void { const path = this.requestToNormalizedFilePath(request); let headers: Record | undefined; if (this.environmentService.crossOriginIsolated) { if (basename(path) === ‘workbench.html’ || basename(path) === ‘workbench-dev.html’) { headers = COI.CoopAndCoep; } else { headers = COI.getHeadersFromQuery(request.url); } } // first check by validRoots if (this.validRoots.findSubstr(path)) { return callback({ path, headers }); } // then check by validExtensions if (this.validExtensions.has(extname(path).toLowerCase())) { return callback({ path }); } // finally block to load the resource this.logService.error(`${Schemas.vscodeFileResource}: Refused to load resource ${path} from ${Schemas.vscodeFileResource}: protocol (original URL: ${request.url})`); return callback({ error: -3 /* ABORTED */ }); }
“`

In order to load files under the `vscode-file` protocol, they either have to be located within the VS Code app installation directory or have one of a set of valid extensions. `.svg` is a valid extension and can contain JavaScript code which will execute when loaded in an “. We can include an SVG file with our malicious repo and get a reference to the directory where it is stored through many of the DOM elements in the notebook webview which contain references to the current directory (the PoC uses the “ tag’s `href` attribute).

Within the SVG file, `top.vscode.ipcRenderer` can be used to invoke IPC handlers of the main process. Two handlers in particular, `vscode:readNlsFile` and `vscode:writeNlsFile`, were found to be vulnerable to directory traversal, enabling attackers to read and write to any file the process has permission to on the filesystem. The PoC uses this to execute code on Windows and macOS by writing to `/out/node_modules/graceful-fs.js`, which is a file that does not exist by default but VS Code attempts to import when loading a window (which we can trigger immediately by sending a `vscode:reloadWindow` IPC message). On Linux, code execution can be achieved through similar means by writing to `.bashrc` etc.

## Proof-of-Concept:

The PoC is a malicious folder containing a VS Code workspace. To trigger the vulnerability, open the folder in VS Code with the Open Folder command and open README.ipynb within the folder. This PoC has been tested on the Windows and macOS versions of VS code. The file structure of the malicious repository is as follows:

“`
not_sus_repo ├── .vscode │ └── settings.json ├── README.ipynb └── icon.svg
“`

`vscode/settings.json`:

“`
{ “notebook.output.minimalErrorRendering”: true }
“`

`README.ipynb`:

“`
{ “cells”: [ { “cell_type”: “code”, “execution_count”: 1, “metadata”: {}, “outputs”: [ { “data”: { “application/vnd.code.notebook.error”: { “message”: “error”, “name”: “name”, “stack”: “Cell u001b[1;32mIn[1], line 6″ } }, “metadata”: {}, “output_type”: “display_data” } ], “source”: [ “def make_big_err(i):n”, ” if i
“`

## Suggested Mitigations

– In `createMinimalError`, ensure that `errorLocation` only consists of an `` tag with the specified URI format before assigning to `headerSection.innerHTML`
– Use Content Security Policy in the notebook renderer webview to ensure that only trusted scripts are run when in restricted mode.

## Demo

It’s demo time.

## Timeline

– 2024-07-03 Vendor Disclosure
– 2024-07-03 Initial Vendor Contact
– 2024-07-10 Shared two other POC with vendor
– 2024-08-02 Vendor’s reply “this case has been assessed as low severity and does not meet MSRC’s bar for immediate servicing due to RCE is no longer possible without extensive user interaction (i.e., accepting a save prompt to a location controlled by an attacker).”
– 2025-05-14 Public Disclosure

Leave a Reply

Your email address will not be published. Required fields are marked *