⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit
bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。
找到 2 個問題,沒有高風險。最嚴重的一條(中風險):只有「設定」權限的人,可以把 LDAP 改成指向自己架的假伺服器,然後冒充名單上任何一位權限更大的操作者登入主控台。
主控台(客戶在自己機器上用瀏覽器開的 Agent 管理頁,8444 埠)除了本機管理員帳號,還有兩條登入路:
另外還有一張「只准哪些網段連進 8444」的白名單。這一棒看三件事:門有沒有關好、客戶 AD 服務帳號的密碼有沒有保管好、名單會不會被人塞人。
| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修(摘要) |
|---|---|---|---|---|---|---|
| F1 | 中 | 只要有「設定」權限,就能把 AD 登入改成問攻擊者自己的伺服器,等於讓他決定誰的密碼「對」 | 能冒充權限更大的操作者:下載證據檔、裝更新、重啟,稽核紀錄記在被冒充的人頭上;別的操作者用 AD 登入時,密碼也會被他收走 | ① 自己已經有主控台「設定」權限 ② 名單上有權限比他大的人,且離線登入開著 ③ 他架的 LDAP 伺服器 Agent 連得到 | guidant_agent/console/api/ldap_routes.py:79、:94;驗密碼在 guidant_agent/identity/adapters/ldap_source.py:249-253 |
改登入來源與本機 LDAP 設定改成最高權限(只准本機管理員);來源一改就踢掉所有 AD 登入的 session |
| F2 | 低 | 主系統拿掉某位操作者的部分權限(人還在名單上)時,他正在用的登入狀態仍保有舊權限 | 被收回的權限只要他一直在用就持續有效,重開機也還在 | ① 他本來就是合法操作者、正在登入中 ② 主系統只縮權限、沒整個撤銷 | guidant_agent/identity/app/offline_operators.py:133(replace_all) |
replace_all 也回傳「權限變少」或「AD 綁定改了」的人,讓 apply 一併踢掉 |
為什麼 F1 是中、不是高:攻擊者必須先是有「設定」權限的自己人,不是外人打得進來。但它打破了「只有設定權的人不能重啟、不能下載」這條權限切分。
白話:AD 登入時,Agent 會拿你輸入的密碼去問 AD 伺服器「這組密碼對不對」,伺服器說對就放行。問題在於「去問哪一台伺服器」,只要有「設定」權限就能改(PUT /console/api/ops/ldap/local 填伺服器位址,PUT /ldap/source 選「本機自設」)。改成攻擊者自己架的 LDAP 後,他想讓誰的密碼「對」就讓誰的對。
出事會怎樣:
另外,真的操作者之後用 AD 登入時,輸入的密碼會被送到假伺服器(可以設成「不加密」),密碼就被收走了。
要先有什麼才打得到:見總覽表。三個條件都要成立。
在哪裡:
guidant_agent/console/api/ldap_routes.py:77-85(改來源)與 :88-97(存本機設定):只檢查 configure。guidant_agent/identity/adapters/ldap_source.py:248-253:非 AD 模式下直接拿名單上的 op.sub 當帳號,去問設定好的那台伺服器。怎麼修:在 ldap_routes.py 的 put_ldap_source、put_ldap_local、delete_ldap_local 把權限從 require_session("configure") 提高到只准本機管理員(或要求持有全部主控台權限)。同時在來源或位址改動後,呼叫 console_session 踢掉所有用 LDAP 方式登入的 session。
人工核對:已開檔確認。三個路由都只掛 require_session("configure")。_validate(ldap_routes.py:55-62)只檢查埠號與格式,沒限制位址。檔頭第 4 行自己也寫「選錯來源或填錯伺服器,下一個人登入時 AD 密碼就送到那台機器」,表示設計者知道風險,但把它和「設定」權限綁在一起。成立。
白話:主系統每次心跳(Agent 定期跟主系統報到)都會推下最新名單。Agent 只把「被撤銷」或「從名單拿掉」的人踢下線(offline_operators.py:133、:142-144)。權限是在登入那一刻抄進登入狀態的,之後不會再對名單。所以只縮權限的人,舊權限會一直留著。
出事會怎樣:主系統拿掉某人的「下載檔案」權限,但他已登入的分頁仍能下載。登入狀態每 30 分鐘會自動延長,重開機也不會消失。
要先有什麼才打得到:他本來就是合法操作者且正在登入中。危害有限,所以列低風險。
在哪裡:guidant_agent/identity/app/offline_operators.py:120-133(replace_all)、:136-144(apply)。
怎麼修:在 replace_all 讀出舊列時比對 scope,新 scope 不是舊的超集合、或 ad_account 不同時,把該 sub 一起放進回傳集合;apply 就會照原本的迴圈呼叫 revoke_by_sub。
人工核對:已開檔確認回傳式只有撤銷與移除兩種,沒有比對 scope。成立。
「測試連線」端點收前端送來的整組設定,可以叫 Agent 連任意內網位址。三票都認為不成立:能叫這個端點的人本來就有權設定任意伺服器,多這條路並沒有多拿到東西。而且改了位址、加密方式或埠時,存著的密碼不會跟著送出去(PINNED_KEYS,ldap_source.py:50,要重填)。根本問題其實是 F1。
| 項目 | 人工結果 | 說明 |
|---|---|---|
ldap_source.py:206-207 服務帳號密碼存哪、回畫面有沒有遮 |
不成立(做得對) | 存在 credential_store(Fernet 加密,經 _secrets() → SecretFieldStore),不是明文 JSON。畫面一律走 secret_form.masked(ldap_routes.py:32、:35)。:9 的「密文會被 AD 反鎖」問題的解法:心跳收到 gAAAAA 開頭的密文就把該欄丟掉(_sanitize,:164-166),只收明文(走 mTLS)。 |
verify_user 帳號 filter escape、空密碼匿名 bind、encryption="none" |
前兩項不成立(已防);第三項部分成立 | Agent 沿用 jedi-ldap kernel 庫(ldap_source.py:20),也繼承了 FR-075/CM-1560 的修正:jedi_ldap/infra/directory.py:158 空密碼直接拒絕;:224、:260 搜尋前用 escape_filter_chars 轉義。「不加密」仍是可選項(:48、:63,同步模式舊設定空值也當不加密,:159),連線結果只給 PLAINTEXT_WARNING、不擋。正式環境允許明文是設計選擇,但配合 F1 會讓密碼被收走更容易,建議修 F1 時一併評估是否禁止自設模式選明文。 |
ldap_routes.py 測試連線收整組設定 → 內網探測、密碼送到假 LDAP |
不成立(附註) | 確實收前端整組設定(:51 merged)。但改了 server/port/encryption 時,舊密碼不會沿用(PINNED_KEYS),所以不會把客戶的 AD 密碼送出去。內網探測需要「設定」權限,而這些人本來就能設伺服器。三個檢查員一致否決。 |
make_guarded_server 判來源用 TCP 還是 header;env 格式錯;下發格式錯生效哪組;能否放寬底線 |
不成立(做得對) | 用的是 socket.accept() 回的 TCP 位址(allow_cidr.py:180-181),不看 HTTP header,而且在 TLS 握手前就擋。env 格式錯時只放行本機(:38-42,出錯預設擋下來)。主系統下發格式錯時 return 不呼叫 _set_cloud(:76-80),沿用上一份已生效的收緊值(或沒收過時用底線),不會退回比較寬。intersect 只留落在底線內的那段(domain/allow_cidr.py:39-46),底線不空時無法放寬。例外:底線為空(安裝時沒設=不限)時,主系統下發的照單全收(:59-60),這是檔頭寫明的設計。 |
offline_operators.py:139-182 名單驗來源、ad_bindings 被改、RECEIVED_FILE 權限 |
部分成立 | 名單沒有自己的簽章,信任只來自心跳通道(mTLS+https,見 sync 規則);主系統被打穿就能塞人,這是架構上的信任根選擇,不在本棒範圍內修。ad_bindings 來自同一份名單,Agent 本機沒有改綁定的路,綁定改了也不會讓 AD 使用者對到別人的 scope:撞到兩人以上一律當找不到(:186)。但綁定改了不會踢掉舊 session(同 F2 的根因)。RECEIVED_FILE 用一般 open(tmp,"w") 寫(:101),權限跟著 umask、沒有明確設 0600;內容只有收到時間、不含祕密,影響極小。 |
source_cidr_guard 只有 8444 用?8443、8000 呢 |
不成立(刻意) | 全 repo 只有 files/api/browser_plane.py:216(8444)呼叫。8443 是資料面,走 mTLS,客戶端憑證必驗,只有主系統拿得到憑證;白名單是給瀏覽器面用的。8000 不在本棒範圍,未深查。 |
local_auth.py、session.py)只在追 F1、F2 時順帶看過。同一份程式再掃一次可能找到不同的東西。| 項目 | 值 |
|---|---|
| Workflow run ID | wf_7a9459b4-6fc |
| 掃描版本 | 3813abd(branch ws-CM-3507-CM-3514,工作區乾淨) |
| 範圍 | 6 檔(file_count: 6),effort low |
verification.status |
verified |
reason_kind |
無(null) |
candidates |
3(去重後 3) |
panel_votes |
9(三個檢查員對 3 條各投一票,9 票全數投出,沒有漏投) |
| 確認成立 | 2(F1、F2 皆 3/3);否決 1(0/3) |
researchers_dispatched / returned |
1 / 1 |
| 耗時 | 約 51 分鐘(10 個 agent,約 63 萬 token) |
| 原始產物 | docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261105-155942/ |