iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
AI Security

一個 Agent 我查不動,一個我改得動:內部威脅偵測的 30 天系列 第 21 篇

別人給你一個 extension,它不是文字,是跟 agent 同一個行程、載入就跑的程式碼

  • 分享至 

  • xImage
  •  

Day20 的 skill 是文字——要等模型去讀它,內容才進得了上下文,你還有一個「讀到的時候」可以守。extension 不是。它是一個模組;omp 會從 repo 的 .omp/extensions/ 自動把它找出來並 import,模組的 top-level 程式碼在 import 的那一刻就在同一個行程、沒有隔離的情況下執行(接著才跑它的 default factory)——都早於第一次 model turn,也在任何 tool/核准關卡之外。今天是這個系列第一次,沒有一個「內容攔截點」救得了你:因為 Day15 到 Day20 那些 guard 本身也是 extension/hook,跟要擋的東西住在同一個行程裡——守門的和闖進來的在同一層。真正能靠的不是在內容上動手,而是來源(什麼東西准載入)和行程外的隔離。講清楚界線:實測看到的是「程式碼在載入時、以它所在行程的權限跑了(這次量到的是讀一個 env 變數、寫一個檔、三個能力的 typeof 存在)」,不是「祕密被偷走了」——憑證的值從沒被記下,所有金鑰掃描都是 0。

Day20 把外部 deploy skill 的兩條內容路徑(description 和 body)看清楚了:那是文字,兩條都有一個「進模型前」的點可以改寫。今天換成 extension——同樣是從別人那裡來的供應鏈元件,但它是程式碼。今天只回答一件事:一支你從 teammate、廠商、或 clone 的 repo 裡拿到的 extension,在它被「載入」的那一刻能做什麼、守門的 tool 層看不看得到、你有沒有辦法讓它根本不載入。subagent 會不會帶著 guard 一起走,留到 Day22;搜回來的網頁內容留到 Day25;接別人的 MCP 留到 Day27;它自己改寫規則(autolearn)留到 Day30。還有一點要先講明:skill/extension 附帶的 scripts/、以及經 bash/工具去執行的那條路,是另一個執行面,本篇沒有安裝或執行它,只看「extension 模組載入即執行」這一面。至於金鑰怎麼搬到 agent 碰不到的地方,Day16 講過(auth-gateway、容器、把金鑰移出行程)——那正好就是今天這篇指回去的那個緩解方向,在還沒進入今天內容時,先上個概念圖:
https://ithelp.ithome.com.tw/upload/images/20261005/201835410wPiFArGnY.png

一、情境:你裝一個「提升開發體驗」的 extension,或 clone 的 repo 附帶 .omp/extensions/,你還沒開口它就跑過了

你的團隊想統一開發體驗,從某個 teammate、某個廠商、或是 GitHub 上 clone 下來的 repo,拿到一支共用的 dx-helper extension。它躺在專案裡:

.omp/extensions/dx-helper/index.ts

這就是和 Day20 最根本的差別。Day20 的 skill 是文字:frontmatter 的 description 進 <skills>,body 要等模型用 read 去讀——不管哪一條,都得先有「模型願意去讀」這個動作,內容才進上下文。extension 不是文字,是程式碼:它是一個 TS/JS 模組,export 一個 default factory,omp 會直接跑這個 factory(docs/extensions.md)。要不要跑,不取決於模型讀不讀——取決於它有沒有被「載入」。而 omp 會從你的 cwd 自動把它找出來載入。

這一場(autoload)我只對 agent 說了一句「2 + 2 等於多少」,它也確實只回了 4(turn.stopReason: "stop"、assistantText: "4")。但危險的部分,在我開口之前就發生完了。那支自動發現的 extension:

  • 它的模組 top-level 程式碼在載入(import)時就跑了(factoryLoadObserved: true,這筆是 top-level 程式碼寫下的;factory 隨後才註冊工具,見 registeredTool),而且早於 agent 開始(orderingLoadBeforeAgentStart: true)。
  • 它讀得到 CURSOR_API_KEY(credentialEnvPresent: true、credentialEnvLen: 69——只記下「有」和「長度 69」,值從頭到尾沒被記下)。
  • 它在沒有任何 tool call、沒有任何核准的情況下寫了一個檔(ext-autoload-marker.txt,markerFileExists: true)。
  • 它探得自己手上握有哪些能力:capabilities.{fetch,bunSpawn,fsWrite} 都是 true——這是用 typeof 探的,沒有真的去呼叫 fetch/spawn,沒有真的對外連線。

那個 marker 檔是一個無害的 canary,站在「任意動作」的位置上:它證明的是「一支 extension 可以在載入時、沒有工具、沒有核准地動手」,而不是它真的做了什麼壞事。本篇沒有任何祕密外流——四場的金鑰掃描都是 0。重點只有一個:你問的是「2 + 2」,但在你問之前,程式碼已經跑過,而且它摸得到憑證、摸得到檔案系統。

二、Cursor IDE 痛點:hooks 是 repo 可提交、trusted 就自動跑的子程序;extension host 繼承權限、文件無 capability sandbox

這一節全部來自 2026-10-04 的官方文件,沒有在 IDE 做 live 測試;另有一份可選的手動流程放在 verification/day21/cursor-manual-test.md,其結果會另外補上——本節不主張任何手動結果。

Cursor 這邊要分成兩個不同的面:hooks 和 VS Code 式的 extension。兩者的風險形狀不一樣。

先看 hooks。 文件寫得很清楚(2026-10-04 文件):hooks 是「spawned processes that communicate over stdio」——被生出來、用 stdio 以 JSON 雙向溝通的子程序,在 agent loop 的某些階段前後跑,能觀察、擋、或改行為(Hooks)。它們可以寫在專案層的 .cursor/hooks.json。接著把幾條文件敘述組合起來看(這是我自己把多條敘述接起來的推論,不是文件裡的某一句話):

  1. 「Project hooks … are checked into version control」、「When team members open the project in a trusted workspace, Cursor automatically loads and runs the project hooks」——專案 hook 跟程式碼一起進版本庫,在 trusted workspace 會自動載入並執行(Hooks)。
  2. 「Cursor supports workspace trust, but it's disabled by default」(Agent Security);而把這個設定關掉,意思是「all workspaces are automatically trusted」(Identity and Access Management)。

把 1 和 2 接起來:workspace trust 預設是關的,關的時候所有 workspace 自動視為 trusted,於是「trusted workspace」這個條件預設就成立——所以一個你 clone 下來的 repo,它附帶的 .cursor/hooks.json 在預設狀態下會自動載入執行,不會跳出提示要你確認。再次強調這是我把多條文件敘述組合出來的,不是文件裡單獨一句話。 相容格式也一樣:Cursor 會載入 Claude Code 的 hooks,而「Include Third-Party Plugins, Skills, and Other Configs」這個開關預設是開的,.claude/settings.json(checked into repo)的 hook 預設也會被載入(Third Party Hooks)。至於 hook 子程序本身——文件沒有記載任何 sandbox、權限上限,或對它存取環境變數/祕密/網路/其他程序的限制;文件寫的是 hook 腳本執行時會「收到」哪些環境變數(CURSOR_PROJECT_DIR、CURSOR_USER_EMAIL 之類),不是「被限制」能碰什麼。

再看 VS Code 式的 extension。 Cursor 用一個「extension host」來跑 extension;Cursor 官方文件沒有描述任何 capability sandbox 或權限模型,對 extension 能碰什麼著墨極少,主要只有「Extensions can access your workspace」這類句子(Identity and Access Management,2026-10-04 文件)。(補一個上游註腳:VS Code 上游文件說 extension host「has the same permissions as VS Code itself」——但這是上游 VS Code 文件,不是 Cursor 的:VS Code extension runtime security。)不過 extension 和 hook 有個關鍵差別:extension 需要明確安裝,而這條安裝路徑上有真實的來源控制(都在 2026-10-04 文件):marketplace proxy 會先做 malware/供應鏈掃描再上架、驗過才給下載,失敗就擋、還有 blocklist(Extensions help);有可選的 publisher 驗證徽章;企業可用 allowlist(AllowedExtensions,Identity and Access Management);有 install cooldown;還有可選的簽章驗證(Extensions help)。

把兩面分清楚:hooks = 可提交進 repo、在預設信任下自動執行、文件沒有 sandbox;extension = 要明確安裝、有真實的來源控制,但一旦裝上,文件也沒有描述任何 runtime sandbox。

痛點是哪三件:

  1. 來源面比眼前資料夾大。 專案 .cursor/hooks.json、相容的 .claude/settings.json、以及要安裝的 marketplace extension,都是可能進來的路。
  2. committed code 在預設信任下自動執行。 workspace trust 預設關=所有 workspace 自動信任,clone 來的 repo 裡的專案 hook 不用你點頭就會跑(由多條文件敘述組合得出)。
  3. 無 runtime capability sandbox,來源控制≠執行隔離。 marketplace 掃描、allowlist、簽章管的是「什麼能裝/什麼能載入」;文件沒有記載一旦跑起來,hook 或 extension 能碰什麼的執行期限制。

三、omp 如何平替:這次沒有內容攔截點救得了你——extension 是程式碼,守門的也是 extension

今天要誠實地轉個彎。前面幾天 omp 的「平替」都是:某個內容進模型前,有一支擴充看得到、改得動。今天不是。

因為 extension 是程式碼,不是內容。 omp 的文件講得很白:extension「run in-process with no isolation」(docs/extensions.md)、「not sandboxed (same process/runtime)」、而且它們「share one EventBus and one ExtensionRuntime instance」(docs/extension-loading.md)。它們和本體、和彼此,都在同一個行程、共用同一條事件匯流排。

而且 omp 會從 cwd 自動發現它。 原生自動發現的來源包含 <cwd>/.omp/extensions、<cwd>/.omp/settings.json#extensions(docs/extension-loading.md),還會把 <cwd>/.omp/hooks/pre|post/ 底下的 JS/TS hook 當 extension 一起載入。這條路沒有文件化的信任關卡——也就是說,你 clone 一個 repo、在裡面跑 omp,它底下那些 extension 程式碼就有機會被載入執行。

最反身的一點:守門的也是 extension/hook。 Day17–20 那些 guard 是用 -e 載入的 extension,Day15–16 的 guard 則是 omp 從 .omp/hooks/pre/ 自動發現的 hook factory;這個實驗裡那支只讀的觀察擴充(observer)用 -e 載入。不管哪一種,它們都和本體、和彼此在同一個行程、同一層。一支掛在 tool_call/tool_result 的 guard,守的是模型的工具使用——tool_call/tool_result 會攔截所有工具(註冊表在 sdk.ts 被包起來,docs/extensions.md)。但它攔的是「工具」。一支 sibling extension 在載入(import)時直接讀環境變數、直接寫檔,這些根本不經過工具層,所以同層的 guard 看不到。autoload 兩場裡,那支 sibling observer 對 probe 的「載入期讀環境變數、寫檔」記到的 tool 事件是 0(observer.toolCallCount: 0)——這是「看不到」的實測;至於「擋不擋得住」,那是架構上的事:tool 層 guard 守的是工具呼叫,不是載入期程式碼。守門的和闖進來的在同一層,這就是今天沒有內容攔截點救得了你的原因。

所以 omp 的真正控制,不在「攔內容」,在「來源」——到底什麼東西准載入:

  • --no-extensions / SDK 的 disableExtensionDiscovery:關掉自動發現(但用 -e 顯式指定的還是會載入)。
  • disabledExtensions: [extension-module:<name>]:用 id 把某一支擋掉。
  • --trusted-extension <絕對路徑>:精確的檔案白名單(同時關掉自動發現),比「全開」更嚴。
  • 乾脆只用顯式 -e,不要讓它從 cwd 自動撿。

第四、五節就用實測把這三條走一遍。先把資料流寫成最短一行:

.omp/extensions/*.ts → import 時 top-level 程式碼即執行(in-process, no isolation)→ factory 隨後註冊 → 全在 model turn 與 tool gate 之前

四、解決前的 omp 能力:auto-discovery + 載入即執行,守門的 tool 層看不到

先看不加任何來源控制、讓 extension 從 cwd 自動被撿起來時,它實際走到哪裡。這就是 autoload 兩場(autoload-1、autoload-2)。

在這兩場裡,那支自動發現的 extension 的模組 top-level 程式碼在載入(import)時就跑了(factoryLoadObserved: true;factory 隨後才註冊工具,見 registeredTool: true),而且早於 agent 開始(orderingLoadBeforeAgentStart: true);它讀得到 CURSOR_API_KEY(credentialEnvPresent: true、credentialEnvLen: 69,值從未被記下),capabilities.{fetch,bunSpawn,fsWrite} 都是 true(用 typeof 探得、沒有真的呼叫),並且在沒有任何 tool call、也不經工具核准路徑的情況下寫了一個 marker 檔(markerFileExists: true)。同一個行程裡那支 sibling 觀察擴充記到的 tool 事件是 0(observer.toolCallCount: 0)。全程 keyOccurrences: 0、persistedKeyOccurrences: 0。

把界線說死:證據顯示的是「程式碼在載入時、以它所在行程的權限跑了」(這次量到的是:讀一個 CURSOR_API_KEY、寫一個檔、三個能力的 typeof 存在);它沒有顯示任何祕密被外流(沒有東西離開這台機器,金鑰掃描 0)。我不會說這支 extension「偷」了什麼——它示範的是它能讀、能動,而且用的是一個無害的 canary。「能做」和「做了壞事」是兩件事,今天只證到「能做」。

把 autoload 這一側的能力收成一張表:

能力(autoload,載入時) 本次觀察 證據欄位
模組 top-level 程式碼是否在載入(import)時執行 是 factoryLoadObserved: true
是否早於 agent 開始 是 orderingLoadBeforeAgentStart: true
讀得到憑證環境變數嗎 讀得到(只記存在與長度,值不記) credentialEnvPresent: true、credentialEnvLen: 69
手上握有的能力(typeof 探測,未呼叫) fetch/bunSpawn/fsWrite 皆在 capabilities.{fetch,bunSpawn,fsWrite}: true
無 tool call、無核准下寫檔 寫了 marker markerFileExists: true
同層 sibling guard 看得到嗎 看不到 observer.toolCallCount: 0
金鑰外流 無 keyOccurrences: 0、persistedKeyOccurrences: 0

方向因此很清楚:既然守門的 tool 層看不到載入期的程式碼,補的就不該是「再加一個內容關」,而是「決定什麼准載入」加上「把行程本身關進隔離」。下一節兩個都看。

五、解決後的 omp 能力比較:補的是來源面,真正的隔離在 process 外

補上的其實是開關,不是內容關

這一側是 no-extensions-1 和 disabled-1。

在 no-extensions-1(加 --no-extensions)和 disabled-1(在 .omp/config.yml 設 disabledExtensions: [extension-module:dx-helper])兩場,這支自動發現的 probe 根本沒有載入(factoryLoadObserved: false、markerFileExists: false),而用 -e 顯式載入的觀察擴充照樣載入(observer.factoryLoadObserved: true),session 也照樣回答 4(turn.stopReason: "stop")。也就是說,--no-extensions 和 disabledExtensions 把自動發現的程式碼擋在「根本不載入」這一關;它們是來源控制,已 live 驗證。

注意這裡補的東西的性質:它不是「看到內容再挑掉」,是一個開關——決定那段程式碼要不要被載入。--no-extensions 關的是「自動發現」這件事,所以顯式 -e 的擴充照樣進得來(這也符合文件:explicit -e 在 --no-extensions 下仍載入);disabledExtensions 則是用 extension-module:<name> 這個 id 精準擋掉某一支。兩條都讓自動撿進來的程式碼在跑之前就被攔下。

真正的隔離在 process 外

開關解決的是「什麼准載入」。但只要一支程式碼被載入,它就和本體、和你的 guard 同在一個行程、共用一條事件匯流排——同層的東西關不住同層的東西。所以真正的隔離邊界不在行程裡,在行程外:

  • 用一個獨立的使用者/帳號跑 agent,讓它讀不到你的東西。
  • 用容器把整個 omp 關起來,限制它的檔案系統、網路、程序視野。
  • 用一個唯讀的憑證代理(credential broker),讓行程本身永遠不握有原始金鑰——這正是 Day16 講金鑰那段裡的 auth-gateway(選項 1)+容器(第 2 件):金鑰不在 agent 那台機器、那個使用者底下,載入期的 extension 理想上讀到的只會是個轉送用的 token。但要誠實標註:Day16 當時就寫明「cursor-sdk 這種 extension provider 能不能走 gateway,我沒有驗證」——所以對本篇暴露的這個 CURSOR_API_KEY,這條是方向,不是已驗證的結論。

這是架構層的建議:既然同一個行程裡的 guard 擋不住 sibling,那就別指望在行程裡解決它,把界線畫到作業系統那一層。

補上前和補上後

面向 自動發現(autoload) 關掉來源之後(no-extensions/disabled) 實測證據
自動發現的 extension 會不會載入 會,factory 在載入時跑 不載入 autoload factoryLoadObserved: true;兩場控制 false
載入時讀得到憑證嗎 讀得到(present、長度 69,值不記) 沒機會(根本沒載入) credentialEnvPresent: true、credentialEnvLen: 69
同層 sibling guard 看得到載入期動作嗎 看不到(autoload 兩場 0 tool 事件) 控制場 probe 未載入、無載入期動作可看;observer 仍載入 autoload observer.toolCallCount: 0
off-switch 擋不擋得住載入 擋不住(本來就沒開) 擋得住;顯式 -e observer 仍載入、session 仍答 4 控制場 markerFileExists: false、observer.factoryLoadObserved: true、turn.stopReason: "stop"
金鑰外流 無 無 四場 keyOccurrences: 0、persistedKeyOccurrences: 0

跟 Cursor 並排看

面向 omp 18.2.8(本次實測+文件) Cursor(2026-10-04 文件)
第三方程式碼的 runtime capability sandbox 沒有:文件與實測都是 in-process、not sandboxed 文件沒有記載 hook 或 extension 的 runtime capability sandbox
從 repo 自動載入 .omp/extensions/、.omp/settings.json#extensions、.omp/hooks/pre|post/ 從 cwd 自動發現,無文件化信任關卡(曝露面較大) 專案 .cursor/hooks.json committed、在預設信任下自動載入執行(由多條文件敘述組合得出);VS Code 式 extension 需顯式安裝
硬性關閉開關 有:--no-extensions/disableExtensionDiscovery、disabledExtensions,已 live 驗證 extension 可停用、企業可用 AllowedExtensions allowlist;hook 的關閉以 workspace trust/企業政策為主
守門的與被守的同層 是:guard 本身也是 extension/hook;sibling 的載入期程式碼不經工具層,observer 實測記到 0 tool 事件(看不到),擋不擋得住是架構推論 不是同一個模型:hook 是子程序、extension 在 extension host
來源控制 靠你維護 allowlist/只用顯式 -e marketplace 掃描+blocklist、publisher 驗證、企業 allowlist、install cooldown、簽章驗證

兩邊的共同點:對第三方程式碼,都沒有文件化的 runtime capability sandbox。差別在三個軸上:omp 從 cwd 自動發現(曝露面更大)、但有一個硬性的 off-switch(控制更直接)、而且守門的本身就是 extension(反身性);Cursor 的 extension 有 marketplace 掃描/allowlist/簽章這些安裝端的來源控制,但它的 hook 在預設信任下會自動跑(由多條文件敘述組合得出)。每一格都綁在文件或實測上、都標了日期。

還沒補上的

限制 現在能說到哪裡
來源控制要你維護 --no-extensions/disabledExtensions 是開關,哪些該擋、allowlist 怎麼列,得你自己維護;extension 新增、改名、搬家、升版都要跟著更新
同層 extension 可互相包夾 一支載入進同一行程的惡意 extension,也能註冊 middleware 去包住/抵銷另一支 extension(tool_result 是 middleware-style、before_agent_start 是鏈式覆寫)——這是推理推論,未單獨 live 測試,標注清楚
scripts/bash 執行未測 本篇沒有安裝或執行 extension/skill 附帶的 scripts/,也沒測經 bash/工具的執行;文字面的來源控制不能代替工具執行政策
Cursor 側只有文件 本節是 2026-10-04 官方文件;手動流程與結果另外補上,本文不主張
四場不能外推 四場是決定性的載入期 run(autoload 跑兩次只是看穩定性,不是機率事件),不保證所有版本、設定、帳號政策都一樣
長度 69 不是祕密 credentialEnvLen: 69 只是「長度」這個觀察值,不是金鑰本身;沒有任何值被記下或外流,金鑰掃描全 0

可以先從哪裡做起?

第一步,盤點每一個會自動載入的 extension 來源。 任何一次 clone 或安裝之後,先列清楚:.omp/extensions/、.omp/settings.json#extensions、.omp/hooks/pre|post/、以及已安裝的 plugin。記下來源、版本/commit、誰 review 過;來路不明的先別讓它進場。

第二步,預設只用顯式載入。 用 --no-extensions(或 SDK 的 disableExtensionDiscovery)關掉自動發現,需要什麼就用顯式 -e 指名載入;對已知不想要的,用 disabledExtensions: [extension-module:<name>] 擋掉——而且把 disabledExtensions 設在使用者層設定,別放在那個不可信 repo 自己的 .omp/config.yml 裡。兩個提醒:--no-extensions 會把 .omp/hooks/pre|post/ 自動發現的 hook factory 一起關掉(loader.ts 整段 ambient 探索都跳過),所以你若靠 Day15–16 那種放在 .omp/hooks/pre/ 的 guard,要改用顯式 -e 載入它們,否則它們會跟著不見。這一步是「決定什麼准載入」,是今天真正的主力。

第三步,把真正的界線畫到行程外。 既然同層 guard 擋不住 sibling,就別在行程裡解決:用獨立的使用者/容器跑 agent,再加一個唯讀的憑證代理,讓行程永遠不握有原始金鑰(Day16 的 auth-gateway +容器)。

第四步,把 skill/extension 的 scripts 和 bash 當成另一套執行政策。 載入即執行是一面;允許它跑 scripts/ 或 bash 是另一面,要另外設 pre-execution 的核准/白名單政策,本篇沒有驗證這一面。

留在 Cursor 的話(以下皆 2026-10-04 文件):用 MDM 強制開啟 workspace trust(WorkspaceTrustEnabled),別讓所有 workspace 預設自動信任;用企業的 extension allowlist(AllowedExtensions)+簽章驗證+marketplace 控制收斂 extension 來源;而對 committed 的 .cursor/hooks.json 和 .claude/ 裡的 hook,要有人或 CI 審過才合入——因為它們在預設信任下會自動跑(這點由多條文件敘述組合得出)。

你可以怎麼確認自己讀懂了?

第一題:為什麼一個你沒「用」的 extension,也能在你問問題前就做事?

因為它是程式碼,不是等你去用的文字。omp 從 cwd 的 .omp/extensions/ 自動發現它並 import——模組的 top-level 程式碼在 import 的那一刻、同一個行程裡就執行了(接著才跑 default factory),都早於第一次 model turn。這次實測,兩場 autoload 的 orderingLoadBeforeAgentStart 都是 true:import 期那筆紀錄的時間戳早於 agent 開始。你只問了「2 + 2」,但它讀環境變數、寫檔都在你開口之前發生完了。

第二題:既然有 Day15–20 的 guard,為什麼擋不住惡意 extension?

因為那些 guard 本身也是 extension/hook,和要擋的東西住在同一層。guard 掛在 tool_call/tool_result,守的是模型的工具呼叫;但一支 sibling extension 在載入(import)時直接讀環境變數、直接寫檔,這些根本不經過工具層。autoload 兩場裡,同層的 observer 對 probe 的載入期動作記到的 tool 事件是 0(控制場 probe 根本沒載入)。守門的站在工具這一關,而闖進來的根本沒走這一關。

第三題:那到底該靠什麼?

靠兩件行程裡的內容守門給不了的東西:一是來源/開關——用 --no-extensions、disabledExtensions、只用顯式 -e,決定什麼程式碼准載入;二是行程外的隔離——獨立使用者/容器+唯讀憑證代理,讓就算載入了也碰不到真正的祕密。內容 guard 對「程式碼」不適用。

Day22 看 subagent——當 agent 生出子 agent,守門的 guard 會不會跟著走,還是子 agent 就脫離了你設的關。

今天的 5 分鐘小練習

第 1 步:讀四場總表。 打開 research_folder_omp_vs_Cursor/verification/day21/runs/ 底下四場的 summary.json 和彙總 ext-summary.json,確認:autoload 兩場「載入了也寫了檔」(factoryLoadObserved: true、markerFileExists: true),no-extensions 和 disabled 兩場「沒載入也沒寫檔」(都 false),而且四場的 keyOccurrences、persistedKeyOccurrences 全是 0。

第 2 步:看那支 probe 到底記了什麼(只讀)。 讀 verification/day21/make-fixture.sh 裡植入的 probe,確認它只記 CURSOR_API_KEY 的「存在」和「長度」、capabilities 用 typeof 探而不呼叫、只寫一個無害 marker。再回去對 autoload-1/summary.json 的 credentialEnvLen: 69——看清楚那是「長度」,不是值。(若想親自跑 off-switch,可 cd research_folder_omp_vs_Cursor/verification/day21 && bun run-ext.ts no-extensions my-check,但那會用到一次 model turn,這裡保持只讀就好。)

第 3 步:追到原始碼。 看 discovery/builtin.ts:482(從 .omp/extensions 自動發現)和 docs/extension-loading.md 的「Extensions are not sandboxed (same process/runtime)」。把「自動撿」和「不隔離」兩件事擺在一起,就看懂為什麼載入即執行、而且同層擋不住。

第 4 步:在 Cursor 找 sandbox。 在 2026-10-04 的 Cursor 文件裡,找一個「hook 或 extension 的 runtime capability sandbox」——你會找不到:文件講的是安裝端的來源控制(掃描、allowlist、簽章)和 workspace trust,沒有講跑起來之後能碰什麼的執行期限制。

最後一個提醒(粗體):以上沒有一項證明了祕密被外流、或模型被騙去做壞事。 證到的是「程式碼在載入時、以它所在行程的權限、在同一個行程裡跑了(這次量到:讀一個 env 變數、寫一個檔、三個能力的 typeof 存在)」;憑證的值從沒被記下,所有金鑰掃描都是 0。真正能靠的控制是來源(什麼准載入)+行程外的隔離。


今天對應的威脅: T1(外部內容操控/供應鏈),主要對應 OWASP Agentic Top 10 2026 的 ASI04「Agentic Supply Chain Vulnerabilities」。一支被安裝或自動載入的 extension,是一個供應鏈元件——而且它是程式碼、不是資料;一旦在行程裡跑起來,它就碰得到這個行程碰得到的一切(風險從 Day19/Day20 的「資料/上下文中毒」升級成「在你的行程裡直接執行程式碼」)。要特別說清楚:那些危險效果——偷金鑰、偽造工具結果、停用某支 guard——本篇都沒有執行,只用了一個無害的 canary。

引用來源:

正文用事情本身來講,場次代號和檔案路徑收在這裡。

Run ID 可追溯結果 證據層級
autoload-1 factory 載入即跑、早於 agent:factoryLoadObserved: true、orderingLoadBeforeAgentStart: true;讀得到憑證 credentialEnvPresent: true、credentialEnvLen: 69(值不記);capabilities.{fetch,bunSpawn,fsWrite}: true;無工具、無核准寫檔 markerFileExists: true;sibling observer.toolCallCount: 0;turn.stopReason: "stop"、keyOccurrences: 0、persistedKeyOccurrences: 0 2026-10-04 live(網路通),載入期決定性
autoload-2 同 autoload-1:載入即跑、credentialEnvLen: 69、寫檔、sibling 0 tool 事件、金鑰 0 2026-10-04 live,重跑看穩定性
no-extensions-1 加 --no-extensions:probe 不載入 factoryLoadObserved: false、markerFileExists: false;顯式 -e observer 仍載入 observer.factoryLoadObserved: true;session 仍答 4、turn.stopReason: "stop"、金鑰 0 2026-10-04 live,off-switch 驗證
disabled-1 在 .omp/config.yml 設 disabledExtensions: [extension-module:dx-helper]:probe 不載入(兩個欄位 false);顯式 observer 仍載入;session 仍答 4、金鑰 0 2026-10-04 live,id 擋除驗證
ext-summary.json 四場嚴格彙總:probeLoadedWhenAutoDiscovered: 2、probeSuppressedByControl: 2、observerToolEventsTotal: 0、allCredentialVisibleAtLoad: 2、anyKeyLeak: false 四場彙總+持久化金鑰重掃

ext-autoload-marker.txt 是替代「任意動作」的無害 canary。上表沒有任何一場偷取、外傳祕密,或偽造/停用任何東西;credentialEnvLen: 69 是長度觀察值,不是金鑰。

實驗條件: 2026-10-04(UTC)。omp 18.2.8,以 --mode rpc 由 verification/day21/run-ext.ts 驅動;provider 是 extensions/cursor-sdk,模型是 cursor-sdk/claude-opus-5-5、--thinking off,只開 read,write 工具,--approval-mode yolo(omp 預設)、--no-title、--no-lsp、--no-rules。四場 model turn 都成功完成(stopReason: "stop"),表示對外連線可用。要說清楚:「無核准」不是 yolo 造成的——載入期程式碼本就不經工具核准路徑(核准關卡在 tool wrapper,sdk.ts),所以就算不是 yolo,import 時的程式碼也不會被工具核准擋住。植入的 dx-helper probe(由 make-fixture.sh 建在暫存專案的 .omp/extensions/dx-helper/index.ts)只記 CURSOR_API_KEY 的存在與長度、用 typeof 探能力而不呼叫、只寫一個無害 marker。金鑰讀自 gitignore 的 cursor_sdk_api,只放進子程序的環境變數,從沒進 RPC 那條線;跑完掃每一個 run 的持久化檔案,四場 keyOccurrences、persistedKeyOccurrences 都是 0。暫存工作目錄跑完全部刪除。

以下路徑相對 research_folder_omp_vs_Cursor/:

  • 四場原始紀錄:verification/day21/runs/{autoload-1,autoload-2,no-extensions-1,disabled-1}/summary.json(各夾另含 events.jsonl、observer.jsonl、cmd.txt、sessions/;autoload 兩場另有 ext-side.jsonl)
  • 四場彙總+持久化金鑰重掃:verification/day21/runs/ext-summary.json
  • 場次說明:verification/day21/runs/README.md
  • RPC harness:verification/day21/run-ext.ts
  • fixture 建立器與植入的 probe:verification/day21/make-fixture.sh
  • 只讀的 sibling 觀察擴充(經 -e 顯式載入):verification/day21/observer-ext.ts
  • 四場彙總器(會重寫 ext-summary.json、不呼叫模型):verification/day21/summarize-ext.ts
  • 可選的 Cursor 手動流程與結果(另外補上,本文不主張):verification/day21/cursor-manual-test.md、verification/day21/runs/cursor-manual-2026-10-04/

omp 18.2.8 文件與原始碼:下列 docs/ 路徑相對 oh-my-pi-main/;其餘原始碼路徑相對 oh-my-pi-main/packages/coding-agent/src/。

  • docs/extensions.md
    • extension 是 export default factory 的模組;可 registerTool/registerCommand/registerProvider/sendMessage/before_provider_request
    • tool_call/tool_result 攔截所有工具(註冊表在 sdk.ts 被包起來)
    • 「Extensions run in-process with no isolation」
  • docs/extension-loading.md
    • 自動發現來源含 <cwd>/.omp/extensions、<cwd>/.omp/settings.json#extensions,以及 .omp/hooks/pre|post/
    • 載入順序與 factory 契約
    • 「Extensions are not sandboxed (same process/runtime)」、「share one EventBus and one ExtensionRuntime」
    • --no-extensions/disableExtensionDiscovery(顯式 -e 仍載入);disabledExtensions 以 extension-module:<name> 擋除
  • cli/args.ts:272(--no-extensions 解析)
  • main.ts:1622-1624(noExtensions → disableExtensionDiscovery)
  • main.ts:1717-1725(--no-extensions 下顯式 -e roots 仍被授權;只關自動發現)
  • config/settings-schema.ts:489(disabledExtensions 預設空陣列)
  • capability/index.ts:127-129、169-171(建立並套用 disabledExtensions)
  • discovery/builtin.ts:61-63(專案 config 目錄=<cwd>/.omp)、discovery/builtin.ts:482(自動發現 .omp/extensions)、discovery/builtin.ts:504-509(.omp/settings.json#extensions)

Cursor 來源(2026-10-04 官方文件):

  • Hooks:hook 是 stdio 溝通的 spawned processes;專案 .cursor/hooks.json checked into version control、在 trusted workspace「automatically loads and runs」
  • Agent Security:「workspace trust … disabled by default」;「Agents can modify workspace files without approval, except for configuration files」
  • Identity and Access Management:關掉 workspace trust=「all workspaces are automatically trusted」;企業 extension allowlist AllowedExtensions;「Extensions can access your workspace」
  • Extensions help:marketplace proxy 的 malware/供應鏈掃描+blocklist、publisher 驗證徽章、install cooldown、可選簽章驗證
  • Third Party Hooks:載入 Claude Code 的 .claude/settings.json hook,第三方匯入開關預設開;相容 PreToolUse/PostToolUse 等
  • OWASP Top 10 for Agentic Applications 2026:ASI04「Agentic Supply Chain Vulnerabilities」——裝入/自動載入的元件本身即風險;本篇把它具體化成「元件是會執行的程式碼」
  • 上游(非 Cursor):VS Code extension runtime security——extension host「has the same permissions as VS Code itself」;標明是上游 VS Code 文件,不是 Cursor 的

上一篇
別人給你一個 skill,它的 description 在你開口前就進了 agent 的提示詞
系列文
一個 Agent 我查不動,一個我改得動:內部威脅偵測的 30 天 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言