突破限制模式:Visual Studio Code 中的 XSS 到 RCE

突破限制模式:Visual Studio Code 中的 XSS 到 RCE

Ots安全 2025-05-21 11:19

2024 年 4 月,我发现了 Visual Studio Code(VS Code <= 1.89.1)中一个高严重性漏洞,该漏洞允许攻击者将跨站点脚本 (XSS) 漏洞升级为完全远程代码执行 (RCE)——即使在受限模式下也是如此。

桌面版 Visual Studio Code 运行在 Electron 上,渲染进程被沙盒化,并通过Electron 的 IPC 机制与主进程通信。

Jupyter 笔记本新引入的最小错误渲染vscode-app模式中存在一个 XSS 漏洞,允许在笔记本渲染器的 WebView中执行任意 JavaScript 代码。.ipynb如果用户启用了该设置,打开一个精心设计的文件即可触发此漏洞;如果用户settings.json在 VS Code 中打开一个包含精心设计文件的文件夹,并在其中打开一个恶意 ipynb 文件,则可触发此漏洞。即使启用了受限模式(这是用户未明确信任的工作区的默认设置),此漏洞也可能被触发。

在这篇文章中,我们将介绍该漏洞的工作原理以及它如何绕过 VS Code 的限制模式。

漏洞详情

Visual Studio 的默认安装为 Jupyter Notebook 提供了一些内置支持,并为一些常见的输出类型提供了默认渲染器。这些渲染器的源代码可以在 中找到
extensions/notebook-renderers/src/index.ts。对于 类型的单元格
application/vnd.code.notebook.error,渲染器调用renderError函数,该函数又调用formatStackTrace位于 中的stackTraceHelper.ts。该函数进一步调用linkify位于同一文件中的 ,将对特定单元格中行的引用转换为 VS Code 中的可点击链接。如果启用了最小错误渲染模式,程序将把结果从
formatStackTrace传递到
createMinimalError,后者执行一些进一步的处理并将结果附加到 webview 的 DOM 。此处复制了带有注释的代码相关摘录。

渲染错误:

function renderError(  outputInfo: OutputItem,  outputElement: HTMLElement,  ctx: IRichRenderContext,  trustHtml: boolean // falseif workspace is not trusted): IDisposable {    // ...if (err.stack) {    const minimalError = ctx.settings.minimalError && !!headerMessage?.length;    outputElement.classList.add('traceback');    const { formattedStack, errorLocation } = formatStackTrace(err.stack);        // ...    if (minimalError) {      createMinimalError(errorLocation, headerMessage, stackTraceElement, outputElement);    } else {      // ...    }  } else {    // ...  }  outputElement.classList.add('error');return disposableStore;}

formatStackTrace 和 linkify:

exportfunctionformatStackTrace(stack: string): { formattedStack: string; errorLocation?: string } {let cleaned: string;// ...if (isIpythonStackTrace(cleaned)) {    return linkifyStack(cleaned);  }}const cellRegex = /(?<prefix>Cell\s+(?:\u001b\[.+?m)?In\s*\[(?<executionCount>\d+)\],\s*)(?<lineLabel>line (?<lineNumber>\d+)).*/;functionlinkifyStack(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)) {      // ...    } elseif (cellRegex.test(original)) {      fileOrCell = {        kind: 'cell',        path: stripFormatting(original.replace(cellRegex, 'vscode-notebook-cell:?execution_count=$<executionCount>'))      };      const link = original.replace(cellRegex, `<a href=\'${fileOrCell.path}&line=$<lineNumber>\'>line $<lineNumber></a>`); // [1]      lines[i] = original.replace(cellRegex, `$<prefix>${link}`);      locationLink = locationLink || link; // [2]      continue;    }        // ...  }const errorLocation = locationLink; // [3]return { formattedStack: lines.join('\n'), errorLocation };}

创建最小错误:

functioncreateMinimalError(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('<a') === 0) {    headerSection.innerHTML = errorLocation; // [4]  }const header = document.createElement('span');  header.innerText = headerMessage;  headerSection.appendChild(header);  outputDiv.appendChild(headerSection);// ...  outputElement.appendChild(outputDiv);}

在[1]和 处[2],代码尝试将诸如 之类的序列
Cell In [1], line 6(可选地使用 ANSI 转义序列)转换为用于与表单链接的 HTML 标签
line 6,并在 处将 errorLocation 变量设置为此 HTML [3]。至关重要的是,它使用的正则表达式末尾的通配符会吞噬行号之后的任何文本,但紧接在Cell In序列之前的任何文本都不会受到该replace操作的影响。因此,像 ipynb. 中的输入
LOLZTEXTHERECell In [1], line 6会导致无效标记
LOLZTEXTHEREline 6

在 中
createMinimalError,如果
errorLocation设置了 并以 开头<a,则它被视为由
formatStackTrace函数生成的链接,因此直接分配给
headerSection.innerHTML。无论工作区是否受信任,此元素都会添加到输出 DOM 中。但是,由于我们可以部分控制标记的formatStackTrace生成(包括字符串的开头),因此我们可以创建一个带有堆栈跟踪的笔记本文件
Cell In [1], line 6,这将导致 的值为errorLocation。
<a href=<img[etc]由于这满足以 开头的条件,它将被插入到
headerSection.innerHTMLwebview 中并在其中呈现,从而导致 JavaScript 运行并123记录到控制台。

升级到 RCE

该 XSS 漏洞会导致在源下的 iframe 中执行代码vscode-app,该 iframe 是位于源下主工作台窗口下的框架
vscode-file。主工作台窗口包含一个vscode.ipcRenderer对象,该对象使渲染器框架能够向主框架发送 IPC 消息,以便执行文件系统操作、在 PTY 中创建和执行命令等等。要访问此对象,我们需要找到一种在
vscode-file源内执行代码的方法。协议处理程序的代码
vscode-file位于src/vs/platform/protocol/electron-main/protocolMainService.ts中,相关部分摘录如下:

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<string, string> | 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 */ });  }

为了根据协议加载文件vscode-file,它们必须位于 VS Code 应用程序安装目录中,或者具有一组有效扩展名之一。.svg是一个有效扩展名,可以包含在加载时将执行的 JavaScript 代码