不急,沒有會讓惡意 Agent 包混進來的洞。 只找到 1 個低風險問題(F1):任何一台已註冊的 Agent 按「檢查更新」時,主系統會把「上一次是誰按檢查」連同全機最舊的 Agent 版號一起回給它,所以別的客戶的管理員帳號名稱,或別家 Agent 的編號,會被看到。三個獨立檢查員投票 3 比 0 判定成立,我也自己開檔核對過。下載鏈本身(驗章、核 sha256、限定下載主機)照卡片「重點看什麼」逐條人工查過,都守得住。
主系統替 Agent 去原廠更新站查新版、下載 Agent 包,再推給各台 Agent。這一棒看下載這一段,有五步:
主系統自己的更新包也走同一支驗章程式(package_verifier.py)。
範圍 4 檔,約 1096 行。為了確認 F1 是從哪條路打得到,翻了範圍外的 core/plugins/_agent_console_update.py 與 config/config.py,只為查證,不另列發現。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 | 嚴重度 |
|---|---|---|---|---|---|---|
| F1 | Agent 按「檢查更新」拿回的結果,帶著「上次是誰按的」和全機最舊的 Agent 版號 | 別的客戶看得到總部管理員的登入帳號名稱(可以拿來猜密碼)、另一家客戶 Agent 的編號,以及全機最舊的 Agent 版號(可以挑舊版的洞打) | ① 手上有一台已註冊、還在用的 Agent ② 多個客戶共用一台主系統而且彼此不互信(SaaS,或總部底下的子公司) ③ 5 分鐘內剛好有別人按過檢查 | app/product_update/service/agent_release_service.py:488(public_check);入口 core/plugins/_agent_console_update.py:56-58 |
給 Agent 的回應另外做一份「只放這幾欄」的白名單(status、latest、compatible、reason、reason_text、at),by、agent_base、host_version、error 一律不給 |
低 |
為什麼是低不是中: 漏出去的只是帳號名稱和版號,沒有密碼、金鑰,也改不了任何東西。而且要有多個不互信的客戶共用一台主系統才有意義,第一版的落地版通常只有一家客戶。
白話: 主系統的「上一次 Agent 檢查結果」整台機器只存一份,畫面跟各台 Agent 共用。這份紀錄裡有 by(誰按的)、agent_base(全機最舊的 Agent 版號)、host_version、error。拿給 Agent 時用的是 public_check,它只拿掉 signed_manifest(簽章清單原文)這一欄,其他欄位全部照給。這種「黑名單」寫法(只列出不准給的,其餘照給)會漏東西。
出事會怎樣: B 客戶的 Agent 呼叫 POST /agents/update/check。如果 5 分鐘內有總部管理員在更新中心按過檢查,回應的 last_check.by 就是那位管理員的登入帳號;如果是 A 客戶的 Agent 主控台按的,看到的就是 agent-console:<A 客戶的 Agent 編號>。
要先有什麼才打得到:
在哪裡:
app/product_update/service/agent_release_service.py:488:out = {k: v for k, v in record.items() if k != "signed_manifest"};app/product_update/service/agent_release_service.py:197:紀錄寫入 by、host_version、agent_base;core/plugins/_agent_console_update.py:56-58:check_for_agent 把 public_last_check() 或 check() 的結果原樣回給 Agent。怎麼修: 在 check_for_agent 回傳前,用白名單只挑 Agent 需要的欄位(status、latest、compatible、reason、reason_text、at)。也可以把 public_check 本身改成白名單,by 只在管理員總覽另外加上。
runner 開檔核對: 成立。 上面三個位置都開過,跟描述一致。
package_verifier.py:116-170:印章編號、印樣目錄、清單型別、預覽版比版號
_KID_RE,:34、:118),所以 ../ 這類路徑擋得掉;印樣檔排除捷徑檔(symlink),大小上限 4KB;只從本機印樣目錄讀。payload_type(清單型別)和 kid(:166-168),拿主系統的清單頂替 Agent 清單會被拒,反過來也一樣。compare_versions(:98-110)照 semver 比,同版號時預覽版排在正式版前面,所以 1.2.0-beta 會被當成比 1.2.0 舊,拿它來降版會被擋。降版這件事本來就要原廠私鑰簽過的清單才做得到。agent_release_service.py:127-146 bundle() 只比大小、不重算 sha256
manifest.sha256(:146),來自驗過章的清單,不是讀本機檔重算的。:130 這樣寫;Agent 那一側不在範圍內,沒有開檔核對)。:243-260 download:續傳的 .part 檔被預先放一份
package_downloader.download 從 .part 現有的大小接著下載(:220)。下載完 install_verified 會對整個檔案核大小和 sha256,核過才改名成正式檔(:242)。update_site_client.py:59-80:限 https、不跟轉址、下載主機白名單、_dev_loopback
_post 帶 follow_redirects=False(:79),所以更新站叫它轉去別的網址也不會跟。DOWNLOAD_HOSTS(:28、:153),網址裡帶帳號密碼也拒收(:61)。ENV_LABEL 取自環境變數 ENV,而預設值就是 DEVELOP_PREMISE(config/config.py:95),被 _DEV_ENV_LABELS 當成開發機。所以正式機的 ENV 如果沒設,就會放寬成「可以用 http 連本機(127.0.0.1/localhost/::1)」。UPDATE_SITE_URL 指過去,這兩件事都要先拿下主機。ENV= 的地方(只有 .env.sample 有);正式安裝 stack 實際設什麼,這一棒沒有查證。same_version.py:PRODUCT_UPDATE_ALLOW_SAME_VERSION=1
package_verifier.py:224 的 allow_same)。簽章、清單型別、主版號、sha256 照樣全驗,降版和跨主版號照樣擋。same_version.py:41)。upgrade_order.sh 也要另外開同名開關才會真的重裝。送給更新站的身分(授權照全文、機器指紋)有沒有進 log
update_site_client.py 的 log 只記路徑、HTTP 狀態碼、錯誤種類,還有「沒有身分票」這句提醒(:113)。授權照全文、指紋、身分票都沒有寫進 log。type(exc).__name__,:82),不會帶出網址或請求內容。| 項目 | 值 |
|---|---|
| run ID | wf_08cfcc94-7fa |
| 掃描版本 | eee99445e72a74662da483920d61ecfad81209ad(工作目錄乾淨) |
| 強度與範圍 | low,4 檔 |
| verification.status | verified(reason_kind 為 null) |
| candidates(候選問題) | 1(去重後 1) |
| panel_votes(檢查員投票) | 3:三個檢查員對 1 條各投一票,全部投出 |
| researchers_dispatched / returned(派出/回來的研究員) | 2 / 2(一位讀全範圍、一位專查寫死的密碼) |
| 耗時 | 約 37 分鐘,5 個 agent,約 53 萬 token |
| 原始產物 | docs/features/security-scan-consolidated/runs/be/CLAUDE-SECURITY-20261108-180451/ |