FR-177 B6b 掃描報告:主系統通道、設定拉取、心跳狀態、工具 schema

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

§1

🟢 一句話結論

這一棒沒有要立刻處理的問題。 工具找到 0 個問題,「重點看什麼」7 項我逐條人工查過,也沒有成立的漏洞。但這不等於這塊完全乾淨:工具一條候選都沒提出,檢查員面板(三個獨立檢查員投票的那一關)沒東西可投。另外有 3 件「設計上就信任主系統」的事,寫在文末「留意事項」,給決策者知道邊界在哪,不算漏洞。

§2

這一棒在檢查什麼

  • Agent 連主系統的唯一出口 cloud_http.py:TLS 驗證(確認對方真的是主系統的那道檢查)、逾時、DPoP 標頭(每個請求現簽的「持有私鑰證明」,綁這次的網址,防止被重送);
  • cloud_channel.py:派工回報、上傳、串流下載都走這條;
  • 設定通道 config_pull.py:Agent 定期向主系統拉設定,逐類交給「套用者」(applier,負責把某一類設定寫進本機);
  • 自報狀態 agent_status.py:Agent 回報給主系統的狀態欄位;
  • 工具表單定義 tool_schemas.py、顯示名 agent_name.py、下載加密開關 file_protect_sync.py、金鑰頁 keys_view.py、主控台票券入口 console_ticket.py;
  • sync 模組的組裝點 sync/__init__.py。

範圍共 10 檔、約 1006 行。為了查證呼叫端,我有翻範圍外的 sync/app/enroll.py、identity/__init__.py、console/api/__init__.py、scan/app/secret_form.py、platform/ops.py、jedi_agent_protocol/events.py。這些檔只為查證而讀,不當作發現。

§3

掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 怎麼修 嚴重度
— 工具沒有提出任何候選問題 — — — — —

沒有 HIGH/CRITICAL,所以不需要逐條開檔核對;「重點看什麼」的人工查證見下一節。

§4

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

  1. cloud_http.py:能不能關掉驗證、逾時多少、會不會跟著跳轉、DPoP 綁什麼

    • request(:49-55)和 stream(:71-77)建 httpx.Client 時,verify 寫死為 cloud_tls_verify(cfg)。呼叫端多帶的 **kwargs 只會傳給 client.request(...),而 httpx 的 request 不收 verify,所以沒有任何參數能覆寫驗證。
    • 逾時:request 預設 30 秒;stream 規定呼叫端一定要給(cloud_channel.get_stream 給的是 connect 30/read 120 秒)。
    • 跳轉:httpx.Client 預設 follow_redirects=False。全 repo grep 過,範圍內四個呼叫點(config_pull.py:140、enroll.py:116、cloud_channel.py:67、:105)都沒有開它。3xx 不會被跟著導到 http 或別的主機。
    • DPoP:auth_headers(method, url, ...) 的 url 是完整網址 cfg.cloud_endpoint + path(:52、:73),不只是 path。query 另外交給 httpx 接上,不進簽章範圍,這是刻意設計(cloud_channel.py:63 有註明)。
    • 小留意:request 的 **kwargs 理論上可以帶 follow_redirects=True,目前沒有任何呼叫端這麼做。
    • 結論:不成立。
  2. config_pull.py:主系統能遠端改哪些設定

    • 全部 appliers 共 13 類:sync 9 類在 enroll.py:252-262,identity 4 類在 identity/__init__.py:56-59。
    • 會動到門禁的有這幾類:
      • allowed_targets/allowed_target_names:決定 Agent 能連誰(B6a 在看);
      • console_allow_cidr:只能收窄(A7 已核);
      • offline_operators、offline_login_days:離線期間誰能登入、能登幾天(A7);
      • ldap_config:LDAP 連線設定;
      • trust_anchors:Agent 信任哪幾把主系統公鑰(cloud_trust.apply_trust_anchors,範圍外);
      • file_protect:見第 6 項。
    • 其餘 error_tracking、vulndb、profiles、tool_schemas、agent_name 不直接開門。
    • 遠端改不到:cloud_endpoint(只從 env CLOUD_ENDPOINT 讀,config/config.py:167)和 CIDR 安裝底線。
    • 套用失敗的處理:逐類各自處理(:109、:121-134)。一類失敗只記 log、不記那一類的版本,下次重給;其餘類照樣套用。所以不是整批回滾,而是「一類一類各自生效」。檔頭有寫明,這是 SR-04.6 要求的「各 observer 互不影響」。
    • 回放舊版:versions 只是 Agent 告訴主系統「我手上有哪版」,Agent 不檢查新收到的版本有沒有比較新。但要回放,得先能冒充主系統的 TLS 回應,而通道有 TLS 驗證加 DPoP。主系統本身若被攻下,本來就能直接下新設定,不需要回放。
    • 結論:不成立(主系統被攻下時的放大效應,見「留意事項」①)。
  3. agent_status.py:心跳送出哪些狀態欄位,有沒有洩漏內網拓撲

    • 送出的欄位受契約 AGENT_STATUS_KEYS 限制(jedi_agent_protocol/events.py:39-43),契約外的欄位在 snapshot()(:36-42)會被丟掉。
    • 契約內共 15 個欄位:vulndb_date、credential_store、configured_tools、storage、browser_port、outbox_size、local_admin_set、local_admin_password_set_at、ldap_source、sync_state、last_restart_reason、console_allow_cidr_installed、name、name_updated_at。
    • 沒有內網 IP、主機名或磁碟路徑這類欄位。name 只在使用者從主控台改過名、還沒回報時才帶(agent_name.py:88-93),不會自動送出主機名。
    • 沒逐支追的:storage、console_allow_cidr_installed 的內容由範圍外的模組產生,沒有逐支追到底。依欄位名稱推,分別是儲存統計與安裝時設定的 CIDR 白名單,後者本來就是主系統要知道的設定值。
    • 結論:不成立(storage 欄的實際內容留給 files 模組那一棒確認)。
  4. tool_schemas.py:工具表單定義有沒有簽章、default/options 會不會被當成可信值

    • 沒有簽章,只靠通道(TLS+DPoP)保護。apply(:69-77)只檢查形狀(是 dict、tools 是 list),就原子寫進檔案。
    • 使用端在 scan/app/secret_form.py(範圍外):
      • options 只用來限制下拉選單能選的值(:65-66);
      • default 在使用者沒填、而且不是祕密欄時,當成欄位值存起來(:89-90)。也就是說,主系統給的 default 會一路流到 connector 的參數。
    • 能不能拼進指令要看各 connector,屬於第三期 connectors 的範圍,這裡不判。
    • 結論:不成立(不是這一層的漏洞;風險轉交第三期,見「留意事項」②)。
  5. keys_view.py:38:不驗簽解通行證,會不會把 token 本體回給主控台

    • _pass 只回 not_after(從 exp 換算)和 issuer(iss),不回 token 原文。
    • build 回的欄位:
      • certificate:只有到期日與換證狀態;
      • trust_anchors:ring.describe(),依檔頭寫法只有 kid/用途/指紋;
      • enrollment:token_configured 是布林值,不是 token 本身;
      • main_system:主系統網址。
    • 不驗簽只用在畫面顯示,沒有拿來當授權依據。
    • 結論:不成立。
  6. console_ticket.py、file_protect_sync.py

    • 票券入口:console_ticket 整支模組直接別名到 agent_auth。呼叫端 console/api/__init__.py:106-108 傳的 agent_uid 是 state.agent_uid,也就是本機註冊狀態,不是從請求讀來的,攻擊者控制不了。
    • 下載加密能被關掉嗎:能。主系統送 {"enabled": false},主控台下載就改給明文(:49-66)。這是 FR-169 D6 的設計:開關本來就由主系統決定,會記 log「關閉(主控台下載改給明文)」。
    • 會不會不小心變成不加密:不會。檔案不存在、壞掉、形狀不對,一律當作開啟(:44-46),不會因為格式錯誤而誤關。
    • 結論:不成立(主系統能遠端關加密,見「留意事項」③)。
  7. sync/__init__.py:模組註冊、services() 持有什麼、背景元件誰起停

    • SyncServices 只持有 4 把鎖(:19-30),沒有帳密或其他狀態。
    • config_pull.COMPONENT 和 agent_status.COMPONENT 在 enroll.py:376-380 接好設定、交進元件清單,由 platform 統一呼叫 start()/stop()。
    • ConfigPull 自己開的保底計時器 thread,會在 stop() 裡收掉(config_pull.py:72-77)。
    • clear_tenant_files(:113-119)用的路徑來自 env AGENT_CERT_DIR 加固定檔名,外部輸入控制不了。
    • 結論:不成立。
§5

留意事項(設計上的邊界,不是漏洞)

這三件都屬於「主系統是信任根」的既定設計。列出來是讓決策者清楚:主系統一旦被攻下,影響會放大到哪裡。

  • ① 主系統被攻下時,影響會擴及所有 Agent。 設定通道能改授權清冊、離線操作者、LDAP 設定、信任公鑰(trust anchors)、下載加密開關。改不到的是 cloud_endpoint 和 CIDR 底線,而且 CIDR 只能收窄。其中「信任公鑰也能由主系統下發」最值得注意:主系統能換掉 Agent 信任的簽章鑰。這部分在範圍外的 cloud_trust.py,建議下一期專門看。
  • ② 工具表單定義沒有簽章,default 值會流進 connector 的參數。第三期掃 connectors 時要對照:connector 有沒有把參數放進參數陣列、有沒有拼成字串。
  • ③ 主系統能遠端關掉主控台下載加密(FR-169 D6 的設計),關掉時有 log 可查。
§6

這份結果可信到什麼程度

  • 「找到的存在嗎?」 不適用,因為工具沒找到任何東西。
  • 「只有這些嗎?」 不能保證。這次是低強度掃描:只派一位研究員、他沒提出候選,面板 0 票。「沒東西可投」不等於乾淨。人工也只照「重點看什麼」7 項查證,結論全部來自讀程式碼,沒有跑測試、沒有實際打過。
§7

執行概況

項目 值
run ID wf_8e2e3ef2-e8f
掃描版本 63334f464215c18fa89c334b360a86f31679c1f8(工作目錄乾淨)
強度與範圍 low,10 檔
verification.status verified(reason_kind 為 null)
candidates(候選問題) 0:沒東西可投,不等於乾淨
panel_votes(檢查員投票) 0
researchers_dispatched / returned(派出/回來的研究員) 1 / 1
耗時 約 7.6 分鐘,研究員約 8.4 萬 token
原始產物 docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261105-174550/