⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit
bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。
沒有急件,工具與人工都沒找到成立的漏洞。 工具的研究員讀完三支腳本,沒有提出任何候選問題,所以三人檢查面板沒有東西可投(0 票)——這代表「沒東西可投」,不等於保證乾淨。人工照「重點看什麼」逐條追了一遍:首腦最擔心的「新映像的 bash 在驗章前就以 root 跑了」,在自動升級(升級單)那條路不成立——驗章在前,source 新映像的腳本在後。另外記下 3 條觀察(不是漏洞,是加固建議或範圍外的提醒),見下方。
Agent 線上更新的主機那一半:容器裡的 Agent 把更新包和「升級單」(一個 JSON 檔,寫要升到哪一版、包名、指紋)放進共用目錄後,主機上以 root 跑的三支 bash 腳本接手:
scripts/install/lib/upgrade_order.sh(417 行):讀升級單、驗格式、驗包的指紋(sha256)、解包、載入映像,再用現行版的執行檔驗新版程式樹的簽章(verify-tree)。scripts/install/lib/upgrade_runner.sh(557 行):升級本體,分步驟執行(預檢 → 處理執行中工作 → 備份並換設定 → 切換 → 健康檢查 → 清理),任何一步失敗就自動退回舊版。scripts/install/lib/upgrade_guard.sh(73 行):升級前查有沒有工作正在跑。共 3 檔、1047 行。為了查證呼叫順序,另外翻了範圍外的 deploy/install.sh、scripts/install/install.sh、scripts/install/lib/progress.sh、deploy/docker-compose.yml,只為查證,不當發現。
| # | 這是什麼 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼處理 | 嚴重度 |
|---|---|---|---|---|---|---|
| — | 工具沒有報任何問題 | — | — | — | — | — |
| 觀察 1 | 手動以 compose 升級(sudo guidant-agent-ctl upgrade <包>)時,只比對包裡自帶清單的 sha256,就從新映像裡 cat 出 upgrade_runner.sh 以 root source 執行,前面沒有 verify-tree 驗章 |
拿到偽造交付包的維運人員一執行,偽包的 bash 就以 root 跑 | 維運人員自己拿到並親手執行一個被換掉的交付包 | deploy/install.sh:2361-2363(範圍外)、upgrade_runner.sh:47-55 |
開卡評估:手動路徑也先用現行版 verify-tree 驗新映像,再 source 新映像的腳本 |
範圍外觀察,未經面板 |
| 觀察 2 | _upr_source 起容器 cat 檔時沒帶 --network none(upgrade_order.sh:49 的 _uo_cat 有帶) |
只是加固上不一致;cat 本身不連網,實際影響很小 | — | upgrade_runner.sh:50 |
補上 --network none 與 _uo_cat 一致 |
加固建議 |
| 觀察 3 | 「有工作在跑就不升級」的判斷,依據是容器自己寫的檔;容器被控時可以讓自動升級一直被擋 | 自動更新永遠「略過」(拖住更新,不能拿 root) | 容器已經被攻陷 | upgrade_guard.sh:13-24、upgrade_order.sh:282-285 |
設計上可接受;可補「連續略過 N 次就回報主系統」 | 接受的風險 |
工具沒有報發現,這裡沒有 F 條目。三條觀察都寫在上面的總覽表;觀察 1 的時序細節見下一段第 1 列。
| 重點 | 人工結果 | 為什麼 |
|---|---|---|
🔴 upgrade_runner.sh:50 的 _upr_source(從新映像 cat 出腳本、直接 source)是在驗章前還是驗章後執行 |
自動升級路徑:不成立(驗章在前)。手動路徑:見觀察 1 | 時序:① upgrade_order.sh:57-61 _uo_load:AGENT_IMAGE_TAG 取 .env 的現行映像,source 的是現行版的 runner。② :282 _uo_precheck 呼叫 _upr_source local_admin.sh/upgrade_guard.sh,這時 AGENT_IMAGE_TAG 仍是現行映像。③ :337-361 解包、:393-416 用現行映像($old_id)跑 verify-tree 驗新版程式樹,再 cmp 確認包內的 install.sh/docker-compose.yml 與簽章樹裡的那份完全相同。④ 這些都過了才在 :196 執行包內的 install.sh upgrade;它的 load_image 把 AGENT_IMAGE_TAG 改成新映像,然後才 source 新映像的 runner(deploy/install.sh:2363),之後 :195-196、:302、:434、:472 的 _upr_source 都讀新映像。$1 一律是程式裡寫死的檔名(local_admin.sh、upgrade_guard.sh、progress.sh、upgrade_order.sh),沒有外部輸入。所以 A3「新版程式在驗章前一行都沒執行」的結論,在升級單這條路上成立 |
_uo_precheck 的 target/bundle/sha 格式驗證之後,進了哪些指令、JSON 怎麼解析 |
不成立 | 用 sed 解析(:144),值不能含 ",也不會跨行。target 要符合 ^[0-9][0-9A-Za-z.+-]{0,63}$,bundle 必須正好等於 guidant-agent-compose-${target}.tar.gz,sha 必須是 64 個十六進位字元,job_id 也有嚴格的 regex(:30、:175)。之後的展開都有加引號,沒有 eval,拼進 docker 參數的也只有過了 regex 的值 |
$old_id 怎麼取得;cmp 之後實際執行的是哪一份 |
不成立 | $old_id 來自主機 .env 的 AGENT_IMAGE(:394)。.env 在主機上、檔案 0600,容器只能唯讀掛載升級目錄(deploy/docker-compose.yml:238-239,可寫的只有 agent-order),所以要改 .env 得先有主機 root。實際執行的是包內那份 install.sh,但它已和簽章樹裡的那份 cmp 過、完全相同 |
rm -rf 的變數會不會為空;.env 寫回的權限 |
不成立(低度加固空間) | UPR_BACKUP_DIR 固定是 ${UPGRADE_DIR}/prev;VERSION_DIR 是 ${VERSIONS_DIR}/${VERIFIED_VERSION},由宿主設好;rm -rf 一律加引號,而且沒有 "$X/" 這種寫法。.env 寫回在 ( umask 077; sed … > .env.new ) 裡做(:240-242),從一開始就是 0600,沒有 A1 M1 那種先以預設權限落地的問題。可加的保險:rm -rf "${VERSION_DIR:?}" |
UPG_LIMIT_*、UPG_TRIGGER_*、SUDO_USER、--rollback |
不成立 | 這些變數只能由啟動 root 腳本的那一方設定(systemd unit 或 sudo 的人),容器設不到。UPG_TRIGGER_* 只寫進進度檔,而且會經過 progress__str 遮罩、跳脫。退回(:343-365)只用主機上的備份與進度檔,不載入新程式碼,所以不需要再驗章 |
upgrade_guard.sh:16 的 cat /data/db/$1 |
不成立(DoS 見觀察 3) | $1 固定是 running-tasks.tsv(:8),不是外部輸入,不可能是 ../。讀到的內容只被計數和用 printf 顯示,沒有拿去執行 |
有沒有 set -euo pipefail;錯誤時進度檔寫什麼、誰能寫 |
部分(設計上如此) | 三支都是 lib,本身沒有 set;宿主腳本有:deploy/install.sh:108 用 set -Eeuo pipefail,scripts/install/install.sh:99 用 set -uo pipefail(沒有 -e,靠逐行 ` |
wf_a162e8d3-b87;revision 63334f464215(工作樹乾淨);effort low;scope 3 檔verification.status:verified;reason_kind:nullcandidates:0(沒東西可投,不等於乾淨);panel_votes:0researchers_dispatched:1;researchers_returned:1docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261105-174554-B8/