Full Disclosure: 1-Click GitHub Token Stealing via a VSCode Bug
Full Disclosure: 1-Click GitHub Token Stealing via a VSCode Bug
完整披露:通过 VSCode 漏洞实现一键窃取 GitHub Token
Just by clicking a link, it’s possible for an attacker to steal a GitHub token that can read and write to your repos, including private ones. 只需点击一个链接,攻击者就有可能窃取你的 GitHub Token,从而对你的代码仓库(包括私有仓库)进行读写操作。
Background
背景
Did you know GitHub has this really cool feature called github.dev? On any repository you have access to, if you can change the url from github.com to github.dev or you click this little menu item: You’ll be launched into a little light-weight version of VSCode that runs entirely in your browser (I guess that’s one advantage of having your app written with electron). 你知道 GitHub 有一个非常酷的功能叫 github.dev 吗?在你拥有访问权限的任何仓库中,只要将 URL 从 github.com 改为 github.dev,或者点击那个小菜单项,你就会进入一个完全在浏览器中运行的轻量级 VSCode(我想这就是用 Electron 编写应用的一个优势吧)。
This browser instance of VSCode is pretty powerful, you can view all the files in the repo (even if it’s a private one), you can send out pull requests and even make commits. This functionality is achieved by github.com POSTing over an OAuth token to github.dev that allows it to interact with GitHub on your behalf. The token is not scoped to the particular repo you interacted with, meaning it has full access to every other repo that you have access to. 这个浏览器版的 VSCode 功能非常强大,你可以查看仓库中的所有文件(即使是私有仓库),可以提交 Pull Request,甚至可以直接提交代码(Commit)。这一功能是通过 github.com 将 OAuth Token 以 POST 方式发送给 github.dev 来实现的,从而允许它代表你与 GitHub 进行交互。该 Token 并不局限于你当前操作的特定仓库,这意味着它拥有你所能访问的所有其他仓库的完全访问权限。
The presence of this token and the fact that this web-app is running almost the entire brunt of VSCode’s million line Typescript codebase makes it a great target for anyone looking into VSCode bugs. That sort of bug is what we’ll explore here and show how an attacker can use it to exfiltrate your GitHub token. 这个 Token 的存在,加上这个 Web 应用运行着 VSCode 数百万行 TypeScript 代码库的大部分核心逻辑,使其成为任何研究 VSCode 漏洞的人的绝佳目标。我们将在这里探讨这类漏洞,并展示攻击者如何利用它来窃取你的 GitHub Token。
VSCode Webview Security Model
VSCode Webview 安全模型
Being an electron app on the desktop, executing arbitrary Javascript inside of VSCode would be tantamount to full remote code execution. This is why VSCode implements some sandboxing approaches, the one we’ll focus on here is VSCode’s webviews. Webviews use an <iframe> with a different origin to the main VSCode window to ensure that any JavaScript executed inside of them is fully isolated.
作为桌面端的 Electron 应用,在 VSCode 内部执行任意 JavaScript 等同于完全的远程代码执行(RCE)。这就是为什么 VSCode 实施了一些沙箱机制,我们这里重点关注的是 VSCode 的 Webview。Webview 使用与主 VSCode 窗口来源(Origin)不同的 <iframe>,以确保在其中执行的任何 JavaScript 都被完全隔离。
These webviews are used for features such as Markdown previews or editing Jupyter notebooks: The output of the cell is rendered into an <iframe> from the origin vscode-webview://..., as opposed to the main electron window which has the origin vscode-file://.... This means that even if the Jupyter notebook uses the built-in features of displaying HTML or using Javascript for interactive widgets, the actual core VSCode application is protected from it. One cannot use Electron’s integration with Node.js APIs inside this iframe or call into VSCode’s APIs from this frame.
这些 Webview 被用于 Markdown 预览或编辑 Jupyter Notebook 等功能:单元格的输出被渲染到来源为 vscode-webview://... 的 <iframe> 中,而主 Electron 窗口的来源则是 vscode-file://...。这意味着即使 Jupyter Notebook 使用了显示 HTML 或交互式组件的 JavaScript 等内置功能,VSCode 的核心应用程序也能得到保护。你无法在这个 iframe 内部使用 Electron 与 Node.js API 的集成,也无法从该框架中调用 VSCode 的 API。
Great, that gives us the ability to render content, but just static content is boring. How do we implement features like having the Markdown preview show you which source line you currently have highlighted or updating the preview live as we edit it? The same cross-origin policy that gives us security also prevents our main editor window from interacting with the DOM in the vscode-webview://... frame.
太棒了,这赋予了我们渲染内容的能力,但仅仅是静态内容很无聊。我们如何实现诸如“Markdown 预览显示当前高亮的代码行”或“编辑时实时更新预览”这样的功能呢?提供安全保障的同源策略(Cross-origin policy)同时也阻止了主编辑器窗口与 vscode-webview://... 框架中的 DOM 进行交互。
The only way to allow this behavior is to have the two web pages in the different origins cooperate with each other using the Window.postMessage() API. This method allows sending JavaScript objects across the different windows.
允许这种行为的唯一方法是让两个不同来源的网页通过 Window.postMessage() API 进行协作。该方法允许在不同的窗口之间发送 JavaScript 对象。
The Bug
漏洞
So our security boundary for webviews roughly looks like this: but in terms of UI, our webview sits right here in the window. People expect basic things like clicking links, drag and or pressing Ctrl+F to work inside of them. Hence, VSCode implements a bunch of basic functionality through the message passing mechanism to enable these features. 因此,我们的 Webview 安全边界大致是这样的:但在 UI 层面,我们的 Webview 就位于窗口中。用户期望在其中点击链接、拖拽或按下 Ctrl+F 等基本操作能够正常工作。因此,VSCode 通过消息传递机制实现了一系列基本功能来支持这些特性。
Speaking of keyboard shortcuts, the astute reader who has dealt with <iframe>s may have already picked up on the issue. As with most things cross-origin, the browser offers a good amount of isolation between the two frames. If you had a page on hackerman.com and you iframed google.com/login, you would not want the hackerman page to be able to attach a keyboard listener onto the iframe. That would let them see all your keystrokes on google.com, allowing them snoop your password.
说到键盘快捷键,处理过 <iframe> 的敏锐读者可能已经发现了问题。正如大多数跨源场景一样,浏览器在两个框架之间提供了良好的隔离。如果你有一个 hackerman.com 的页面并嵌入了 google.com/login,你肯定不希望 hackerman 页面能够在该 iframe 上附加键盘监听器。那将允许他们看到你在 google.com 上的所有按键,从而窃取你的密码。
Okay given that information, try clicking inside a VSCode webview and then pressing Ctrl+Shift+P to bring up the command palette. Oh yay, that works. Wait. Oh. Oh no. So, to avoid the terrible user experience of your keyboard shortcuts not working when you happen to be clicked inside of a webview, the default set of webview message handlers have an event called did-keydown.
好了,有了这些信息,试着点击 VSCode Webview 内部,然后按下 Ctrl+Shift+P 调出命令面板。哦,太棒了,它能用。等等。哦。哦,不。为了避免当你点击 Webview 内部时键盘快捷键失效的糟糕用户体验,默认的 Webview 消息处理器中有一个名为 did-keydown 的事件。
When you load a webview, the following code runs inside the webview to register a handler for it: 当你加载一个 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
});
};
How convenient, so webviews just bubble up keydown events so the main VSCode window can treat them… 真方便,Webview 只是将 keydown 事件冒泡上去,以便主 VSCode 窗口可以处理它们……