⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit
bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。
卡片:CM-3512(母卡 CM-3507)|掃描 run ID:
wf_4fa8f010-296|程式版本:bab2aa31f00d(branchws-CM-3507-CM-3512)|2026-11-05
沒有找到需要馬上修的問題。 工具只提出 1 條疑點,三個獨立檢查員一致投「不成立」;人工照「重點看什麼」逐條核對,7 項都守住了,另外記下 2 個很小的觀察(不急,要不要開卡由決策者決定)。
Agent 裝在客戶機器上,它怎麼確認「這份東西是原廠出的、沒被改過」:
common/integrity/verify.py):原廠用私鑰簽名,Agent 用寫死在程式裡的公鑰核對。更新清單、漏洞庫清單、啟動檢查全靠它。common/integrity/startup_gate.py):Agent 每次開機前先核對自己的程式檔有沒有被改過,被改過就拒絕啟動。guidant_agent/sync/app/vulndb_sync.py、vulndb_bundle.py):從主系統拉新版漏洞資料包、解開、驗章、切換。| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 | 結論 |
|---|---|---|---|---|---|---|
| 工具 C1 | 驗章前先把信封內容解壓,解壓沒設上限(「壓縮炸彈」:小檔解開變超大) | Agent 記憶體被吃光、當掉 | 要先控制主系統本身(資料包走驗證過對方身分的加密連線,且指紋要符合主系統公布的值) | common/integrity/verify.py:102 |
可選:解壓改用 zlib.decompressobj().decompress(data, max_length) 設上限 |
不成立(三票全否:能打的人本來就能做更糟的事) |
| 人工 O1 | 啟動檢查只核對「清單上列的檔」,清單外多出的檔不會被發現 | 有人在程式目錄放額外檔案,開機檢查不會發現 | 要先能寫入 Agent 安裝目錄(通常只有 root) | common/integrity/startup_gate.py:146-161 |
比照漏洞庫 check_files 的做法:掃目錄、清單外有多的檔就拒啟(要排除 log/暫存類) |
觀察,低;是否成立要看 Nuitka 產物會不會載入多出的檔,未驗證 |
| 人工 O2 | 判斷「新版不能比舊版舊」時,舊版日期讀的是本機那份沒重新驗章的 manifest;它壞掉時當成「沒裝過」 | 可以塞回一包原廠簽過的舊版漏洞庫(降版) | 要先能寫入漏洞庫目錄,且主系統要真的送出舊包 | guidant_agent/sync/app/vulndb_sync.py:195-199、:254 |
本機 manifest 讀不到但 current 存在時,當錯誤停下而不是當首次安裝 |
觀察,低 |
manifest.sig 由主系統送來,走驗證過主系統身分的加密連線(cloud_trust.py:57-66),整包指紋要等於主系統自己公布的值(vulndb_sync.py:223-225);簽章檔上限 8MB(vulndb_bundle.py:130)。啟動檢查的 manifest.sig 在安裝目錄裡,能改它的人本來就能直接換掉程式。verify.py:102 改成 zlib.decompressobj().decompress(raw, 16*1024*1024),解完 unconsumed_tail 非空就拋 VerificationError。common/integrity/startup_gate.py:146-161 _compare_files 只跑 manifest["files"] 列出的檔。vulndb_bundle.py:158-173 check_files,加一步「目錄實際檔案集合 ⊆ 清單」。vulndb_sync.py:195-199 installed_date() 讀不到日期就回 None;vulndb_bundle.py:154 遇到 previous is None 就放行。vulndb_sync.py:109 已經先比過一次)。current 連結存在但 manifest 讀不到時,回報錯誤、不同步,不要當成首次安裝。| 項目 | 結果 | 為什麼 |
|---|---|---|
verify.py:61-81 從檔案讀公鑰 |
不成立 | 預設只查寫死在程式裡的 PUBLIC_KEYS(:61-65)。從檔案讀公鑰的 release_key_lookup 只用在 Agent 更新清單(update_console.py:178),目錄寫死為 /opt/guidant-agent/current/release-keys(update_console.py:34)。上傳包裡的鑰一律不看;kid 只收 16 位十六進位(verify.py:47),擋 ../;捷徑檔(symlink)、超過 4KB、非 Ed25519 的公鑰都拒收(:78、:84)。能寫 /opt/guidant-agent 的人等同 root。附帶一點:release-keys/ 有沒有列在啟動 manifest 裡受保護,不在本棒範圍,沒查。 |
verify.py:114-152 alg/序列化/類別不符 |
不成立 | alg 不是 Ed25519 一律拒收(:128),而且公鑰物件本身就是 Ed25519,不存在換演算法的問題。簽名驗的是「解壓後的原始位元組」(:152),不會在 Agent 端重新排序 JSON,所以兩邊序列化不一致的問題不存在。類別不符直接拋錯拒收(:157-158),不是只發警告。 |
startup_gate.py:105 測試開關、:55-58 覆寫路徑 |
不成立 | 打包模式下(由 __compiled__/frozen 判斷,resource_path.py:27-29),驗證一律開啟(:103-104),環境變數只能讓驗證更嚴、不能關掉。驗證目錄在打包模式固定在 binary 所在目錄(:60-61),不吃環境變數覆寫。 |
startup_gate.py:24 刪 manifest 能不能繞過 |
不成立 | 簽章檔不存在就拋錯(:118-121),清單空的也拒啟(:138-142),失敗一律 SystemExit(1) 拒絕啟動(:181),不是記 log 後繼續跑。 |
vulndb_bundle.py:79-94 自寫的解壓 |
不成立 | 絕對路徑、..、空字元一律拒收(:103);realpath 確認目標還在資料目錄內(:105-107);連結與特殊檔整包拒收(:95-97);實際寫入量超過 tar 標頭宣告值就拒收(:118-119),標頭說謊沒用;檔數、總量都有上限;`O_EXCL |
vulndb_bundle.py:135-172 清單外的檔、切換是否原子 |
不成立 | 清單外多出的檔整包拒收(:168-170)。全部檢查做完才切換。os.rename 搬進正式目錄名,再用 os.replace 原子地換掉 current 連結(vulndb_sync.py:264-272),掃描任何時刻看到的不是舊版就是新版。 |
vulndb_sync.py 來源/頻率/降版/失敗是否刪舊庫 |
不成立(見 O2) | 來源是主系統 /api/1.0/agents/vulndb/bundle(:32),走同一條驗證過身分的控制通道(:247),由心跳設定通道觸發;同一包失敗後冷卻 30 分鐘(:44)。降版有兩道擋:主系統宣告的日期(:109)、簽章內的日期(:254)。失敗時只清暫存,current 不動(:239-241),不會刪掉舊庫。 |
public_keys.py 幾把鑰、dev 鑰 |
不成立 | 只有一把 PROD 鑰(public_keys.py:31-38),沒有 dev/stg 鑰跟著出貨。 |
update_console.py、cloud_trust.py、resource_path.py)只為了確認呼叫端而翻過幾行,沒有審。沒有執行任何程式或測試,所有結論都來自讀程式碼。CLAUDE-SECURITY-REVISION-bab2aa31f00d.json)| 欄位 | 值 |
|---|---|
| status | verified |
| reason_kind | null |
| candidates | 1(工具提出 1 條疑點) |
| panel_votes | 3(三個檢查員對 1 條各投一票,3 票全投出,沒有漏投;結果 0 票成立) |
| researchers_dispatched / returned | 1 / 1 |
| 範圍檔數 | 6 |
| 耗時 | 1042 秒(約 17 分鐘) |
| 歸檔 | docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261105-152756/ |
verified 在這裡的意思是「唯一的那條疑點,三人投票完整跑完」。0 條發現是「投票全數否決」,不是「沒東西可投」。