⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit
bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。
卡片 CM-3517|母卡 CM-3507|掃描日 2026-11-05|revision
749d3b42529f|run IDwf_af582674-52c|歸檔runs/CLAUDE-SECURITY-20261105-165639/
找到 2 個中度(MEDIUM)問題,沒有高風險。兩個都是同一個根本原因:主控台「手動掃描」把使用者填的參數幾乎原樣交給掃描工具,只擋了 _ 開頭的參數;其中有兩個欄位(InSpec 的 profile、SonarQube 的 repo_url)不在授權清冊(allowed_targets,「Agent 准連哪些主機」的名單)的檢查範圍內。
主控台上四組會讓 Agent「對外動手」的路由:
| 檔案 | 做什麼 | 要什麼權限(scope) |
|---|---|---|
guidant_agent/console/api/scan_routes.py |
手動發掃描、取消、上傳源碼 | run-local-scan |
guidant_agent/console/api/netdiag_routes.py |
網路診斷、下載/預覽報告 | net-diag(下載要 download-files) |
guidant_agent/console/api/files.py |
證據檔列表、下載、預覽 | files/download-files |
guidant_agent/console/api/tools.py |
工具帳密設定、測試連線 | configure |
術語:scope=主控台登入票上的權限名,路由沒有對應 scope 就進不去。清冊=主系統下發的授權目標名單。面板=工具裡三個獨立檢查員,分別從「打得到嗎/影響多大/有沒有防線」三個角度投票,2 票以上同意才留。
| # | 嚴重度 | 一句話 | 誰打得到 | 急不急 |
|---|---|---|---|---|
| M1 | 中 | 手動掃描可指定任意 InSpec profile,Agent 會在自己身上執行那段程式 | 有「手動掃描」權限的主控台使用者 | 下一版修 |
| M2 | 中 | 手動掃描的 SonarQube repo_url 不過清冊,可叫 Agent 連清冊外的內網主機 |
同上 | 下一版修 |
兩條的修法可以一起做:手動掃描的參數改成「每個工具只收白名單欄位」,不再採用「只擋 _ 開頭的參數」這種黑名單做法。
cinc-auditor)的「profile」是一包 Ruby 程式,會在 Agent 自己的機器上執行。正常情況下 profile 應該是主系統下發、存在本機快取的那幾份(參數 _profile);但手動掃描允許使用者自己填 params.profile,而且填什麼都照用。run-local-scan(離線操作者也算);清冊與操作者名單已同步;本機已設好 inspec 或 gcb 帳密;清冊裡至少有一台目標。guidant_agent/console/api/scan_routes.py:159(_build_params 只擋 _ 開頭的鍵)→ guidant_agent/scan/app/local_runs.py:114(只查清冊鍵)→ guidant_agent/scan/adapters/connectors/inspec.py:872-878(沒有 _profile 時直接回傳 params.profile)。inspec.py:874-878 的註解寫著「手填路徑(P6):原樣取用」,可見這是刻意保留的舊行為;但手動掃描這個入口讓「手填」變成主控台使用者也能用。工具原本判 HIGH,面板三票一致降成 MEDIUM,理由是要先有已授權的操作者身分。profile,只能從 profile_sync.local_choices 選主系統下發的 profile;或者每支工具列出使用者可設定的參數白名單,其他一律丟掉。repo_url 不過清冊(CWE-918 伺服器端請求偽造)git clone 使用者填的 repo_url 再分析。這個網址只檢查開頭是不是 http/https,沒有比對清冊。run-local-scan 權限、本機已設好 SonarQube 帳密、Agent 上有 git。guidant_agent/console/api/scan_routes.py:159 → guidant_agent/sync/app/allowed_targets.py:37-40(TARGET_PARAM_KEYS/URL_PARAM_KEYS 都沒有 repo_url)→ guidant_agent/scan/adapters/connectors/sonarqube.py:435-443(只驗 scheme)。後兩個檔在範圍外,只為了查證而讀,不當成本棒的發現。repo_url 加進 URL_PARAM_KEYS,讓它解析出的主機也要在清冊內;或者手動掃描只開放 upload 模式。| 卡上重點 | 人工結果 | 為什麼 |
|---|---|---|
TARGET_PARAM 表漏工具(sonarqube server_url、zap context) |
部分成立 → 見 M2 | scan_routes.py:37-38 的表只決定 targets[] 欄要放進哪個參數;真正擋人的是 local_runs.start 用 allowed_targets.iter_targets 檢查,看的鍵是 hosts/targets/host/ips/target_url/urls/login_url。sonarqube 的 repo_url 不在裡面(M2)。server_url/base_url 是工具自己的服務位址,註解說刻意不比對(屬帳密設定,configure 權限),不另報。zap 的 context 不是連線目標。 |
extra_args/options 原樣進 connector 變成指令注入 |
路由層成立(無過濾)→ 見 M1 | 路由層只擋 _ 開頭的鍵,其他鍵全部原樣交下去;profile 就是實例(M1)。connector 有沒有把其他欄位拼進 argv 屬第三期範圍。 |
netdiag /estimate、/runs 的 target 過清冊 |
不成立 | /runs 在 :138 由 netdiag.check 同步檢查清冊,排到才跑時 netdiag.run 會再比一次。/estimate 只計算不連線。 |
| 每 session 目標上限、能不能開多個 session 繞過 | 不成立(附註) | 上限 SESSION_MAX_TARGETS = 20(netdiag/domain/limits.py:34),而且是算在每個 session 上的,開新 session 確實能重置計數。但每個目標本來就要在清冊內,另外還有全域的每分鐘輪數上限(limits.ROUNDS),繞過上限也連不到清冊外的主機,所以不構成問題。 |
報告下載用 download-files、預覽用 net-diag |
不成立 | 兩者是同一份檔,但預覽走 send_customer_file(inline=True),只能 inline 顯示白名單內的型別,其他回 415、不給附件(files.py:151-154)。設計就是「能看不能拿走檔案」,跟證據檔的 files 對 download-files 同型。 |
_base_name 拼 target ip 進檔名、uid 沒驗格式 |
不成立 | target.ip 是 netdiag.check 驗過的 IP,只可能有數字、.、:;檔名只是存檔名,不拿來拼路徑。uid 只拿來查 local_runs 表(ORM 參數化),查不到就 404。 |
files.py 列表過濾、uid 歸屬、header 注入 |
不成立 | 列表與單檔查詢都只取 storage_scope='customer'(customer_files_repo.py:53);下載、預覽共用 _owned_blob,先查歸屬才取檔。下載檔名由 Flask send_file 處理,X-Filename 經過 quote(file_protect.py:97),不會造成 header 注入。 |
tools.py GET 回密文?測試連線收整組設定? |
不成立 | GET 走 secret_form.masked,祕密欄只回 {"set": true}(secret_form.py:116)。測試連線只用已存的帳密(:115),body 只收 target_host 與 credential_group;target_host 先過清冊(:90),run_probe 內部又再查一次。跟 A7 LDAP 不同型,不會把密碼送到任意位址。 |
| 讀寫同 scope、body 大小限制 | 不成立(附註) | scan 和 netdiag 都掛了 body_limit.guard(64KB/1KB)。tools 的 PUT 讀寫同用 configure,這是設計(沒有獨立的唯讀設定 scope);tools/files 沒有掛 blueprint 層級的 body 上限,但用的是 console_bp,是否有全域上限屬 A6 範圍,不重報。 |
allowed_targets 本體(B6a)不在本棒範圍。CLAUDE-SECURITY-REVISION-749d3b42529f.json 的 verification:
status: verified,reason_kind: nullcandidates: 2,panel_votes: 6researchers_dispatched: 1,researchers_returned: 1