# A1 長輪詢通道（主系統端）資安掃描報告

## 一句話結論

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

## 這棒檢查什麼

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

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

## 總覽表

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

## 每條詳述

### 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，並加排程定期刪掉已結束的列。

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

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` 是 `{**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 敲鈴。就算敲錯台，對方查表也拿不到別台的工單。**不成立。**

## 可信度兩層

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

## 執行概況

- 工具：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/`
