ChatGPT 帳戶接管攻擊鏈解析:MathLive postMessage 漏洞及訊息監聽器靜態檢查
We took over ChatGPT accounts: the MathLive postMessage chain, and the 20-line audit for your own listeners
Parameter 披露的攻擊鏈利用 MathLive 未驗證訊息來源的 postMessage 監聽器,透過共享 ChatGPT 對話連結接管帳戶,受害者只需開啟連結。
文中提供可掃描前端程式碼的靜態檢查器,並説明它如何識別未驗證訊息來源的監聽器及自身限制,讓同類檢查的適用範圍更清楚。
昨日 Parameter 披露的漏洞,是本月最精巧的網頁漏洞利用攻擊:只要受害者開啟共享對話連結,攻擊者便能完全接管其 ChatGPT 帳戶。最終版本無須貼上內容、登入或點擊:從連結開啟到 JavaScript 以 chatgpt.com 的來源執行,約需 4.5 秒,期間可讀取私人對話及工作階段權杖。漏洞於 10 月 18 日報告,10 月 21 日由 P2 升級至 P1,並於 10 月 26 日修復,漏洞獎金為 $5,000。問題根源是一類現今幾乎所有網頁應用程式都會帶有的漏洞,因此本文會逐步剖析整條攻擊鏈,並介紹我編寫、在受影響程式庫上執行的 100 行靜態審核工具:它準確標示出導致漏洞的四個監聽器,你今晚也可以用同樣方法掃描自己的程式碼庫。
攻擊鏈:七個步驟
所需條件:一個共享對話(公開功能)、MathLive(當訊息包含互動式圖表時,ChatGPT 會載入的數學輸入程式庫),以及在獨立沙盒來源上執行的 HTML Preview 窗格。
- 誘餌。攻擊者分享一段包含真實互動式圖表的對話(這會令 chatgpt.com 載入 MathLive 並安裝其虛擬鍵盤訊息監聽器),以及一個 HTML 區塊。
-
未設防的入口。經沙盒隔離的 HTML Preview 會傳送
postMessage呼叫。MathLive 的監聽器從未檢查訊息來自哪裏(研究人員稱「來自任何網頁地址」),並接受了一則update-setting訊息,其中鍵盤layouts[].markup欄位帶有攻擊者控制的 HTML,接着再接受一則show訊息。 -
危險接收點。版面配置透過
keyboardContainer.innerHTML呈現,並使用createHTML輔助函數;該函數是 Trusted Types 包裝器或恆等函數,並非清理器。我已在正式發佈的 bundle 中確認這個接收點:s.innerHTML = Qi.createHTML(a)等函數在 MathLive 0.100.0 中仍然存在。 -
來源繼承。注入的有效負載是一個
<iframe srcdoc>,而且沒有 sandbox 屬性。srcdoc iframe 會沿用嵌入文件的來源:新框架的來源就是 chatgpt.com。 -
繞過 CSP。Content-Security-Policy 阻擋內嵌指令碼,但允許 YouTube。因此,有效負載會載入
www.youtube.com/oembed?...&callback=<js>;void;這是一個類似 JSONP 的回應,其主體以攻擊者指定的 JavaScript 開頭。CSP 看到的是對 youtube.com 的請求,因而允許通過。瀏覽器會以指令碼形式執行 callback。 -
權杖。程式碼以 chatgpt.com 的來源執行,從頁面讀取工作階段初始化資料(
#client-bootstrap),並取得存取權杖。 -
接管帳戶。取得權杖後:
GET /backend-api/conversations...,列舉、讀取資料,再透過看似無害的圖片請求外傳資料。「以受害者的 ChatGPT 帳戶名義執行任何操作。」
每個個別環節都是教科書式的安全問題。教訓在於這些環節組合起來的效果:程式庫中寬鬆的監聽器,加上沙盒窗格、寬鬆的 CSP,以及未經沙盒隔離的 srcdoc,便足以接管全球最多人瀏覽的 AI 網站上的帳戶。
審核:今晚檢查你的監聽器
這類漏洞有一個特徵:addEventListener('message', ...) 處理常式從未引用 event.origin 或 event.source。我根據這個特徵編寫了一個靜態掃描器(採用啟發式方法、適用於壓縮後的 bundle,約 100 行),並用它掃描兩個關鍵項目:
$ python3 postmessage_audit.py mathlive-0.100.0.mjs
[RISK] mathlive-0.100.0.mjs:2841 message listener -> NO SENDER CHECK (.origin/.source absent)
[RISK] mathlive-0.100.0.mjs:2931 message listener -> NO SENDER CHECK (.origin/.source absent)
[RISK] mathlive-0.100.0.mjs:2931 message listener -> NO SENDER CHECK (.origin/.source absent)
[RISK] mathlive-0.100.0.mjs:3084 message listener -> NO SENDER CHECK (.origin/.source absent)
$ python3 postmessage_audit.py mathlive-0.111.1.mjs # current release
(no message listeners flagged in the main bundle)
$ python3 postmessage_audit.py ~/code/my-app/src/ # your turn
檢查的核心部分(完整、獨立的腳本內嵌於下方):
PATTERNS = [re.compile(r"addEventListener\(\s*['\"`]message['\"`]\s*,"),
re.compile(r"onmessage\s*=")]
ORIGIN_OK = re.compile(r"(?:event|e|ev|msg|evt)\.(?:origin|source)\b")
DANGEROUS = re.compile(r"innerHTML|insertAdjacentHTML|document\.write|outerHTML|\beval\(|new Function\(")
# for each listener: extract the handler body; flag if ORIGIN_OK absent,
# and flag louder if a DOM sink lives inside the same handler
坦白説明其限制:這是對來源文字進行的啟發式檢查(代理處理程式、包裝過的框架或動態註冊的監聽器可能避過檢查),掃描結果乾淨並不能證明安全;執行階段的黃金標準,是開啟 DevTools,觀察哪些監聽器會對來自外部來源的合成 postMessage 作出反應。不過,它標示的正是本月導致 OpenAI 出現 P1 問題的模式,而且會偵測到有問題的版本,同時讓沒有問題的版本通過。
完整腳本(放進程式碼庫,或在 CI 中對前端程式包執行):
#!/usr/bin/env python3
# postmessage_audit.py <file-or-dir> [...] : flag message listeners without sender checks
import re, sys
from pathlib import Path
RX = [re.compile(r"addEventListener\(\s*['\"`]message['\"`]\s*,"), re.compile(r"onmessage\s*=")]
ORIGIN = re.compile(r"(?:event|e|ev|msg|message|evt)\.(?:origin|source)\b")
SINK = re.compile(r"innerHTML|insertAdjacentHTML|document\.write|outerHTML|\beval\(|new Function\(")
def scan(path):
try: text = path.read_text(errors="replace")
except Exception: return
for rx in RX:
for m in rx.finditer(text):
win = text[m.end():m.end()+2000]; depth=0; body=""
for i,ch in enumerate(win):
if ch=="{": depth+=1
elif ch=="}":
depth-=1
if depth==0: body=win[:i+1]; break
else: body=win[:1500]
line = text[:m.start()].count("\n")+1
v=[]
if not ORIGIN.search(body): v.append("NO SENDER CHECK")
if SINK.search(body): v.append("DOM sink in handler")
if v: print(f"[RISK] {path.name}:{line} -> {'; '.join(v)}")
for a in sys.argv[1:]:
p=Path(a)
for f in (sorted(p.rglob("*")) if p.is_dir() else [p]):
if f.suffix in {".js",".mjs",".ts",".jsx",".tsx",".html",".vue",".svelte"}: scan(f)
四項防禦措施,按影響力排序
-
務必檢查發送者。 每個訊息處理程式的第一行:
if (event.origin !== EXPECTED) return;。只需一行,就能消除整類問題。全球其中一個頂尖網站所嵌入的程式庫,正正缺少了這一行。 -
使用能清理內容的 Trusted Types 包裝器,否則就不要用。 只原樣傳遞位元組的
createHTML,就像在敞開的大門上掛一個合規徽章。如果採用 Trusted Types,政策必須真正限制內容(使用 DOMPurify 或同等方案),而不只是包裝一層。 -
為 srcdoc 文件加上沙箱。 沒有
sandbox的<iframe srcdoc>會繼承你的來源。sandbox="allow-scripts"(沒有allow-same-origin)會令注入的文件無法對你的文件構成威脅。預覽窗格有加沙箱;攻擊者的框架則沒有。 -
CSP 就像資源預算,應審核允許清單。 任何會原樣回傳 callback 參數的端點,都是可執行腳本的原語(JSONP 並未消亡,它仍存在於 oembed 端點中)。CSP 中的
youtube.com並非「只是影片」:應理解為「YouTube 決定我的頁面上會執行哪些 JavaScript」。
這對 AI 平台有甚麼特別含意
AI 聊天應用程式的信任模型與眾不同:用戶生成的內容會變成應用程式介面(分享的對話會在第一方來源內顯示圖表、預覽及嵌入內容)。平台載入來呈現這些內容的每個程式庫,都會承襲平台的權限;攻擊者則可以選擇用哪些內容觸發哪些程式庫。OpenAI 的修正方案(拒絕跨框架的 MathLive 訊息,並調整 CSP)是正確的,並在八天內推出。順帶一提,旗艦產品的完整帳戶接管漏洞賞金只有 $5,000,這反映出的定價訊號頗耐人尋味。
簡而言之:共用的 ChatGPT 連結 + 信任任何發送者的 MathLive 訊息監聽器 + innerHTML 寫入點 + 未加沙箱的 srcdoc + YouTube JSONP 繞過 CSP = 工作階段權杖及所有私人對話;相關漏洞已於 October 26 修補。這類漏洞就是未經來源檢查的 addEventListener('message');上文的 100 行審核程式會在你的程式包中標示這類問題(而且正確標示有漏洞的 MathLive,並讓已修補版本通過)。每個監聽器只需一行 event.origin 驗證,就能徹底防範。
來源:Parameter:我們接管了 ChatGPT 帳戶 · MathLive · 審核已針對 mathlive 0.100.0(4 個監聽器,沒有發送者檢查,存在 innerHTML 寫入點)及 0.111.1(主程式包中沒有該模式)完成驗證
來源:DEV Community · dev.to