FR-177 C8 掃描報告:網路診斷的執行、判讀、儀表板、報告與限制

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

§1

🔴 一句話結論

沒有急件,最嚴重是低風險。 工具只找到 1 個問題(F1,低),三個獨立檢查員 3 比 0 確認:代理伺服器(proxy)的設定如果沒寫 http:// 開頭,帳號密碼就不會被遮掉,會原樣出現在儀表板、診斷報告和支援診斷包裡。首腦標的「重點看什麼」7 條我逐條開檔看過:報告的 XSS(網頁被塞腳本)不成立,因為輸出一律 html.escape,而且報告根本不存 .html;限制面有兩件設計面的事值得記(目標數限制只算「每個登入 session」,重新登入就歸零;清冊如果收了 127.0.0.1,就能拿 Agent 打自己),都不升級成發現。

§2

這一棒在檢查什麼

guidant_agent/netdiag/app、guidant_agent/netdiag/domain、guidant_agent/netdiag/__init__.py,共 10 檔、1289 行。這是網路診斷的上層:收到主控台請求後挑哪些探測、估要跑多久、依序跑、把結果判讀成「哪裡壞了」、組本機網路儀表板、產 JSON/純文字/PDF 報告,以及每分鐘輪數、每 session 目標數、連線間隔這些限制。

為了查證,翻了範圍外的 console/api/netdiag_routes.py(呼叫端)、sync/app/allowed_targets.py(清冊)、netdiag/adapters/probes/cloud.py(儀表板代理資訊流向)、platform/support_bundle.py。這些只為查證,不當發現。

§3

掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 怎麼修 嚴重度
F1 代理網址沒寫 scheme(user:pass@proxy:3128)時,「遮帳密」函式把原字串連密碼整段吐回 只有 net-diag 權限的主控台使用者看得到公司代理帳密;帳密也寫進報告檔、支援診斷包,被帶出客戶環境 管理員把代理設成不帶 http:// 的寫法(httpx、curl 都接受這種寫法,所以真的會有人這樣設) dashboard.py:105-114(_strip_userinfo,:112 回原字串) 沒有 :// 就先補 http:// 再解析;解不出主機名就回固定字樣,絕不回原字串;順便接住 parts.port 會丟的 ValueError 低
§4

每條發現的詳述

F1 — 代理帳密在「沒寫 http://」時不會被遮

白話: Agent 對外連主系統可以經過公司的代理伺服器,代理位址從環境變數 HTTPS_PROXY/HTTP_PROXY 讀,常夾帳密。儀表板要顯示「有沒有設代理」,所以 proxy_info()(dashboard.py:117-124)把每個值交給 _strip_userinfo 遮掉帳密。這個函式用 Python 的 urlsplit 拆網址,拆不出主機名就直接 return url[:200](:111-112),也就是原字串。

我自己跑過:urlsplit("user:pass@proxy:3128") 得到 scheme=user、netloc 空、hostname=None,所以整串(含密碼)原樣回傳。

出事會怎樣: 這個值會出現在三個地方:

  1. GET /console/api/netdiag/dashboard 回應的 info.proxy.https_proxy,只要有 net-diag scope 就看得到。
  2. 主系統連線診斷(cloud 探測)的「proxy」那一步(adapters/probes/cloud.py:140 把 proxy_info() 整包塞進結果),因此寫進存證的 JSON/txt/PDF 報告。
  3. 支援診斷包的儀表板快照(platform/support_bundle.py 呼叫 collect_dashboard),這包會寄給廠商。

要先有什麼: 管理員把代理設成不帶 scheme 又含帳密的寫法;攻擊者要有 net-diag 主控台 session、能下載報告,或拿到支援診斷包。

在哪裡: guidant_agent/netdiag/app/dashboard.py:105-114。

怎麼修: 見總覽表。重點是「解析失敗就當作不安全」,回固定字樣而不是原文。

§5

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

首腦標的重點 人工查過
report.py:HTML 報告有沒有跳脫、是不是 inline 的 .html、subprocess 跑什麼 XSS 不成立。 to_html(:91-112)每個插值都過 e = html.escape,包含 raw 原始輸出(:99)、摘要、判讀文字、目標主機名。而且 HTML 只是 PDF 轉檔的中間檔,放在暫存資料夾、轉完就刪;store(:140-161)只存 json/txt/pdf 三種,沒有存 .html,主控台預覽(netdiag_routes.py:207-210 inline=True)拿到的不會是 HTML。subprocess 是 LibreOffice 轉 PDF(:128-130):參數陣列、經 run_cmd、60 秒逾時、獨立 profile 目錄、HOME 指向暫存資料夾,沒有使用者值進參數(檔名固定 report.html)。另記(低、不升級): HTML 交給 LibreOffice 轉檔,若內容含 <img src="http://..."> 可能讓 soffice 對外抓圖;但所有內容都 escape 過,塞不出標籤,所以不成立。
dashboard.py、runner.py:怎麼排探測、並行幾個、逾時殘留子行程;dashboard 跑什麼指令 不成立。 runner.run(:170-206)拿 run_lock 全域一把鎖,一次只跑一台、依固定順序、總逾時 120 秒,超時的項目標 skipped。dashboard 的指令全是固定參數陣列(ip -j addr/route/neigh、ss -Hltn、timedatectl、nmcli、networkctl、resolvectl、ethtool),每支 3 秒逾時;唯二帶變數的:ethtool <網卡名>(網卡名先過 ^[A-Za-z0-9_.:@-]{1,15}$,來自本機 ip 輸出)與 ip neigh show <閘道IP>(來自 /proc/net/route 解成 IPv4),都不是使用者值。逾時後子行程怎麼收是 shared/timeouts.py 的 run_cmd 的事,在範圍外,這次沒有追。
limits.py:限制存記憶體嗎、多 session 能不能疊加繞過、estimate 會不會變免費 DNS 查詢器 estimate 不成立: domain/limits.estimate(:75-95)是純計算,只看勾選與埠數,不碰目標、不解析 DNS。限制在記憶體:成立(設計面,記錄不升級)。 RoundLimiter 是模組內記憶體(註解寫「重啟即清可接受」),重啟就歸零。多 session 疊加:成立(設計面,記錄不升級)。 目標數上限 20 存在 sess.diag_targets(netdiag_routes.py:143),是「每個登入 session」各 20;登出再登入或開多個 session 就能各打 20 台。不過目標一律要在雲端下發的授權清冊內(runner._address_target :124),加上每台每分鐘 3 輪、連線間隔 1 秒、全域一次一台,能掃的範圍和速度都被清冊與全域鎖封頂,所以不升級。門檻寫死在 domain/limits.py、請求改不了,這點成立的是好事。
verdict.py:會不會把 raw 原文放進判讀訊息 部分成立、但無害。 判讀文字會帶探測摘要的欄位(例如 tls.verify_message、dns.rcode、HTTP 狀態碼、埠號,:121、:171、:197),不帶 raw 整段。這些值進 HTML 前一律 escape(見第 1 條),進 JSON/txt 則是純文字,沒有被執行的地方。
ports.py、__init__.py:報告存檔的檔名、歸屬、下載 scope 不成立。 檔名 netdiag-<IP 或 cloud>-<時間>(netdiag_routes.py:117-119),IP 已經過 ipaddress 正規化、: 換 -,沒有路徑字元。歸屬 uploader=started_by=登入者 sub(:151)。下載走 download-files scope(:202-204),預覽走 net-diag。ports.py 每次現從容器拿、不存模組變數,符合註解。
domain/ 與目標:除了固定目標,能不能把 Agent 自己(127.0.0.1、8443/8444)當目標 程式本身不擋 loopback:成立(設計面,記錄不升級)。 _address_target(runner.py:115-126)只擋 multicast、0.0.0.0、廣播位址,不擋 127.0.0.1/::1,唯一的牆是授權清冊 is_allowed。清冊由主系統下發,一般不會含 loopback,所以實務上打不到;但主系統被打穿或設錯時,Agent 就能對自己的 8443/8444 做 port、http 探測。建議之後加一道「address 類目標一律拒 loopback」,與清冊無關。固定目標(主系統、閘道、DNS)的位址都由 Agent 自己算、請求只指定「哪一類」;DNS 類可以帶 ip,但必須是本機 resolv.conf 列出的伺服器之一(:146-148)。
report.py 與代理資訊 工具的 F1 就從這條延伸出來,見上。
§6

這份結果可信到什麼程度

  • 工具這一層: low 強度,1 個研究員讀整個範圍,加 1 個找寫死帳密的研究員,2 派 2 回。1 個候選問題,三個檢查員 3 比 0 確認(兩票 LOW、一票 MEDIUM,所以從 MEDIUM 降成 LOW)。stamp 是 verified。全部都是讀程式碼得出的結論,沒有執行 Agent。
  • 人工這一層: F1 我自己用 urlsplit 跑過一次,確認回傳原字串;三個流向(儀表板、cloud 探測、支援診斷包)都 grep 到呼叫點。「重點」7 條逐條開檔看過。run_cmd 逾時後怎麼收子行程,在範圍外,沒有追。
§7

執行概況

項目 值
run ID wf_1a8fedab-fcd
掃描版本 6d1e6ca3836c389d0b26b8cb8dd1f870f9172ac8(工作區乾淨)
強度與範圍 low,10 檔,focus=attack-surface
verification.status verified(reason_kind 為 null)
candidates(候選問題) 1(去重後 1)
panel_votes(檢查員投票) 3,全部投出
researchers_dispatched / returned(派出/回來的研究員) 2 / 2
耗時 約 34 分鐘,5 個 agent,約 29 萬 token
原始產物 docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261106-035657/