A1 長輪詢通道(主系統端)資安掃描報告

§1

一句話結論

找到 1 個問題(中等):管理員替 Agent 設定的檢測工具帳密(SSH、LDAP、SonarQube 等)以明文存在資料庫 compliance.agent_requests 表,而且永遠不會刪。拿到資料庫備份的人就能讀到所有客戶的工具密碼。

§2

這棒檢查什麼

FR-181 把「主系統主動連進 Agent」改成「Agent 吊一條連線等主系統派事」(長輪詢)。這一棒看主系統這頭的 8 支檔、約 1000 行:

  • 怎麼把要 Agent 做的事寫進 agent_requests 表
  • 怎麼敲鈴叫醒吊著的連線(透過 Redis 發布訂閱)
  • 怎麼收下 Agent 的回答與回傳檔案(暫存目錄)
§3

總覽表

編號 問題 嚴重度 急不急 位置
F1 工具帳密以明文永久存在資料庫 中 落地版就打得到,建議出貨前修 app/remote_agent/service/agent_request_service.py:205
§4

每條詳述

F1 工具帳密以明文永久存在資料庫

  • 白話:管理員在「遠端 Agent → 工具帳密」頁輸入密碼後,主系統會把「請 Agent 存這組帳密」這件事寫成一列請求,交給 Agent 來拿。這列的 payload 欄位直接放了明文密碼。Agent 拿走、回答完之後,這列只改狀態、密碼還在;也沒有排程會清掉舊列。程式碼註解寫的是「帳密只在記憶體過一手、不落地」,實際上落地了。
  • 出事會怎樣:資料庫備份外流、DBA 誤看、或系統別處出現 SQL 注入(用程式拼字串查資料庫的漏洞)時,對方下一句查詢就拿到所有客戶、所有 Agent 曾設定過的檢測工具密碼。拿到這些密碼可以直接登入客戶的受稽核主機與資安工具。
  • 要先有什麼才打得到:①能讀資料庫或它的備份;②至少有人透過這個頁面設過或清過一次帳密。
  • 在哪裡:app/remote_agent/service/agent_request_service.py:205(_new_row 把 payload 原樣複製)。來源路徑:core/plugins/_agent_tool_credentials.py:75 → infra/remote_agent/agent_credential_client.py:33/42(ask(KIND_CREDENTIALS_SET, {tool, fields}))。
  • 人工核對:開檔確認 _new_row 寫的是 payload=dict(payload or {}),沒有加密也沒有遮罩;reply/finish 只改 status 與 result,不清 payload。成立。
  • 怎麼修:密碼不進 payload。可以用既有的 Fernet 加密(一種對稱加密,用同一把金鑰加解密)存、長輪詢交出時才解;也可以只放在行程記憶體裡、交出時再補進指令。列一離開 waiting 就清空 payload,並加排程定期刪掉已結束的列。
§5

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

  1. owns/reply 只用 agent.id 比對、result 沒有上限:歸屬檢查成立,finish(request_id, agent.id, ...) 帶 agent_id 當條件,別台改不了。result 在本範圍內確實沒有大小與型別檢查,要看套件端點有沒有限 body 大小(屬 J1)。_agent_channel.py:62 的 result["sha256"] 遇到怪內容只會丟例外、比對失敗,不會被拿去執行。本範圍內不構成問題,但建議 J1 驗收時確認端點 body 上限。
  2. file_path 合進 result 會不會被 Agent 蓋掉::227 是 {<strong>result, "file_path": file_path},主系統給的值放在後面,會蓋掉 Agent 自帶的值,不是反過來。grep 主系統找不到其他地方讀 result["file_path"] 去開檔。不成立。**
  3. file_spool.py 檔名、symlink、大小上限、開完即刪:檔名有白名單正規式(只准英數、底線、連字號,最長 64 字),無法跳出目錄;目錄拒用符號連結、權限 0700。save 迴圈讀到串流結束為止,本檔沒有大小上限,要靠套件端點或前面的反向代理限制上傳大小(屬 J1/部署設定)。open 先開再刪,POSIX 下開著的檔仍讀得到,同一檔第二個人會拿到「檔案不存在」,沒有競態漏洞。掃描面板另審過「/tmp/agent-outbound 被搶先建立」一條,三票都判不成立(容器內沒有第二個本機使用者)。大小上限待 J1 確認,其餘不成立。
  4. Redis 頻道能不能被別台敲鈴、搶回答插槽:鈴只傳「去查表」,不帶內容;就算被敲錯,醒來查 DB 只會拿到派給自己的列(以 agent_id 過濾)。能發 Redis 訊息的只有主系統自己的行程(Redis 不對 Agent 開放),Agent 碰不到。Redis 斷線時 ask 改每段較短間隔自己查表(_RECHECK_DEGRADED_SEC),最長 60 秒上限,不會永遠吊住。不成立。
  5. ask 配額能不能被一般使用者吃滿:全行程共用 8 條(ASK_QUOTA = 8),每條最長 60 秒,滿了直接回 503 不排隊。觸發 ask 的入口(查日誌、對帳、換證)都需要管理 Agent 的權限,一般使用者打不到。有權限的人連按確實可以讓其他 Agent 功能暫時回「忙碌」最多 60 秒,影響有限、可自行恢復。低風險,不另列。
  6. source_archive_fallback 舊路徑有沒有少守門:fallback 呼叫 AgentFileDownloadRoute().get(uid),該 route :47-48 照舊用 current_agent().agent_uid 傳進 get_file_for_agent,仍以通行證上的 Agent 綁定任務。不成立。
  7. _agent_commands.py 敲的是不是該工單那一台:_on_task_insert 記下 target.agent_id,提交後用 get_remote_agent_by_id(agent_id) 查出那台的 uid 敲鈴。就算敲錯台,對方查表也拿不到別台的工單。不成立。
§6

可信度兩層

  • 「這些問題存在嗎」:F1 三位檢查員(可達性、影響、防線三個角度)全數判成立,我也開檔核對過,確定存在。
  • 「只有這些嗎」:這是 low 強度掃描,一位研究員讀完整個範圍、另一位專找寫死在程式裡的祕密,兩位都回來了。範圍內 8 支檔都讀到,但 low 強度只跑一輪,不能保證沒有漏網的;請求 body 大小與暫存檔大小上限在套件端點(J1 範圍),本棒無法下結論。
§7

執行概況

  • 工具:Claude Code 官方 claude-security plugin 0.12.0,effort low,focus attack-surface
  • run ID:wf_5e2ac742-075,掃描版本 31345fbe0c9c,工作目錄乾淨
  • 開始 2026-11-08 17:16 UTC,耗時約 17 分鐘
  • stamp verification:status: verified、reason_kind: null、candidates: 2、panel_votes: 6、researchers_dispatched: 2、researchers_returned: 2
  • 面板完整跑完:兩條候選各三票,F1 3:0 成立,F2(暫存目錄搶先建立)0:3 駁回
  • 原始產物歸檔於 docs/features/security-scan-consolidated/runs/be/CLAUDE-SECURITY-20261108-171646/