FR-177.A8 掃描報告:Agent 證據檔(blob API、瀏覽器直傳 8444、儲存設定、下載加密)

⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。

卡片:CM-3515(母卡 CM-3507)|範圍 guidant_agent/files(19 檔)|掃描日 2026-11-05|程式版本 3813abd

1. 🔴 一句話結論

這一塊沒有掃到需要修的資安問題。 工具只提出一條疑點:「沒設定的話 /blob 不用登入就能下載」。三個獨立的檢查員都投票否決,理由是兩種出貨安裝方式都會把登入檢查打開。卡片「重點看什麼」列的 7 項我逐項開檔看過,有 2 個小地方值得記一筆,都不需要現在修(見第 4 節)。

2. 這一棒在檢查什麼

證據檔是客戶最敏感的資料(掃描報告、設定檔、系統清冊)。它進出 Agent 有四條路:

  • 主系統把檔存進來、拿出去(blob API,走 8443 埠)
  • 使用者從瀏覽器直接上傳到 Agent(8444 埠),憑主系統簽發的一次性「上傳票」
  • 檔案存放位置的設定:本機磁碟,或客戶自己的物件儲存(SeaweedFS/MinIO,一種放檔案的伺服器)
  • 下載時加密打包

這一棒看的是這四條路有沒有沒鎖好的門。

3. 掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 怎麼修 結論
工具 C1 程式內建的預設值是「不檢查登入」,萬一沒設定,誰都能下載或刪除證據檔 拿得到 Agent 網址的人可以下載、刪除證據檔 安裝時沒寫 AGENT_AUTH_MODE=full,而且埠對外開放。兩種出貨安裝都不會這樣 config/config.py:166、guidant_agent/sync/api/data_plane_auth.py:90-91 不用修(可另議:把程式內建的預設值改成 full) ❌ 三票否決
人工 M1 8444 上傳失敗時,回應裡「允許哪個網站讀這個回應」照抄請求者自己的網站 別的網站只讀得到錯誤代碼,讀不到檔案、也拿不到登入狀態 沒有實質可利用的路 guidant_agent/files/api/browser_plane.py:145 可不修;想收緊就改成只在驗票成功後回 備註,不算問題
人工 M2 擋「同一張上傳票用兩次」的紀錄只放在記憶體 Agent 重啟後,還沒過期(5 分鐘內)的票可以再用一次 要先偷到一張有效票,而且剛好碰上 Agent 重啟 guidant_agent/sync/domain/agent_auth.py:261-276(browser_plane.py:134 使用) 可不修(程式註解已寫明這是刻意接受的風險) 備註,已知且接受

4. 每條詳述

工具 C1:程式預設值是「不檢查登入」(三個檢查員否決)

  • 白話:設定檔沒寫 AGENT_AUTH_MODE 時,程式退回 none,也就是不檢查主系統簽發的登入憑證。這時 /blob/<uid> 的下載與刪除誰都能打。
  • 為什麼否決:原生安裝腳本 scripts/install/install.sh:1125 會寫入 AGENT_AUTH_MODE=full,而且把不加密的那個埠只綁在本機(127.0.0.1,外面連不到)。docker 交付包同樣會打開檢查。所以「預設就開著門」的情況實際出貨時不存在。三個檢查員各從「打不打得到」「後果多大」「有沒有其他防線」看,三票都判否決。
  • 首腦參考:要的話可以另開卡,把 config/config.py:166 的程式內建預設值改成 full(出錯時預設擋下來)。這是加一層保險,不是修漏洞。

人工 M1:8444 回錯誤時的跨網站讀取設定(備註)

  • browser_plane.py:145:驗票成功時,回應只允許票上記的那個網站讀取。驗票前(瀏覽器的事前詢問、驗票失敗),則照抄請求者帶來的 Origin(請求從哪個網站發出的)。
  • 影響很小:回應沒有開放帶 cookie 讀取,而且沒有票就上傳不了任何東西。別的網站最多讀到「票無效」的錯誤代碼。

人工 M2:防重放紀錄只在記憶體(備註)

  • 「重放」就是把同一張票拿來用第二次。TicketReplayGuard 用一個記憶體裡的 dict 記用過的票編號。上傳票只活 5 分鐘,程式註解(agent_auth.py:262)明說「重啟後的窗口可接受」。Agent 是單一行程,不存在多個工作程序紀錄不同步的問題。

5. 「重點看什麼」逐條人工查證

  1. _verify_ticket/防重放/CORS 退路(browser_plane.py:106-149):票會驗簽章、綁 agent_uid 與 scan_uid,同一張票不能用第二次(agent_auth.py:282-303)。瀏覽器帶的來源網站和票上的不同就拒絕(:302)。防重放紀錄只在記憶體,見 M2。CORS(瀏覽器的「允許別的網站讀回應」機制)的退路見 M1。不成立(只留備註)。
  2. 檔名與落地路徑、大小上限、內容類型(browser_plane.py:66-103):檔名先取最後一段、再用 secure_filename 消毒,.. 和斜線都會被拿掉。scan_uid 先用正規式驗過才拼進路徑(:168)。檔案先寫進暫存檔 .upload-<jti>.part,超過上限就拋錯,except 會把暫存檔刪掉(:100-101)。上傳的檔沒有限制格式,但它是源碼包,只給掃描工具讀,不會被主控台當網頁打開,所以沒有 XSS(存進去的內容被當程式執行)的風險。不成立。
  3. blob_route.py 三處 send_file:uid 是拿去資料庫查,不是直接拼成路徑。本機儲存讀檔前會確認真實路徑在 BASE_DIR 底下(_read_bytes :28-40)。PDF 預覽用 LibreOffice 轉檔,只轉白名單裡的 office 副檔名(:25)。這幾支都在 8443,必須帶主系統簽的登入憑證(見 C1)。主控台的預覽另有 inline_policy(讀檔頭判斷格式),不在本棒範圍。不成立。
  4. 儲存設定(storage_settings.py):主控台只能選「系統內建 SeaweedFS」(FIELDS :36-39;verify :118 其他型別一律拒)。所以主控台不能填任意網址,沒有「騙 Agent 去連攻擊者指定的網址」(SSRF)的空間。自訂端點只能用本機指令 configure-storage。金鑰存在加密的 SecretFieldStore 裡。storage_trust.py 只是把 CA 憑證追加進信任清單,沒有關閉憑證檢查的寫法。不成立。
  5. 下載加密(file_protect.py):用 jedi-file-protect,註解寫明是 AES-256 zip,不是可以破解的舊式 ZipCrypto。密碼由前端產生(19 字元),只放在這一次請求的 header 裡。兩處稽核(:47、:59)只記檔名與 encrypted,沒有記密碼。保護開啟卻沒帶密碼時回 400,不會偷偷給沒加密的檔。不成立(加密演算法本身在 jedi-file-protect 套件內,不在本棒範圍)。
  6. 8444 掛了什麼(files/__init__.py、browser_plane.py):8444 是獨立的 app,只掛 /scan-input/<scan_uid> 上傳、根目錄轉址、主控台(registry.mount_browser_plane)。/blob 只由 register 掛在主 app(8443)。沒有 blob 下載掛在 8444。不成立。
  7. SQL(customer_files_repo.py、migrations.py):全部用 SQLAlchemy 的查詢語法,沒有自己拼 SQL 字串。關鍵字搜尋有 autoescape;排序欄位用白名單 SORTS 對照。查詢都限定 storage_scope='customer'。Agent 只服務一個客戶,不需要依客戶過濾。不成立。

6. 這份結果可信到什麼程度

  • 「這些存在嗎」:工具那條疑點經三個獨立檢查員投票,三票否決。人工兩條備註都附了行號,可以照著開檔核對。
  • 「只有這些嗎」:這次用低強度掃描(low),一個研究員看完整個範圍,另一個專門找寫死在程式裡的密碼,兩個都完成了。低強度不會做逐元件的深挖,所以不能宣稱這一塊完全乾淨,只能說「工具與人工都沒找到」。jedi-file-upload、jedi-file-protect 套件內部,以及 sync、console 的實作都不在範圍內。

7. 執行概況(照 stamp 抄)

項目 值
run ID wf_7fd381a8-b9d
掃描版本 3813abd05232(branch ws-CM-3507-CM-3515,沒有未提交的改動)
effort low;聚焦 attack-surface(只看攻擊者摸得到的正式程式,測試與產生的程式碼當背景)
verification.status verified
reason_kind null
candidates 1(一條疑點進投票,不是 0,所以這次「verified」代表真的投過票)
panel_votes 3(三個檢查員對 1 條各投一票,3 票全數投出,全判否決)
researchers_dispatched/returned 2/2
耗時 約 22 分鐘(1362 秒)
歸檔 docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261105-155941/