---
title: FR-077 總結報告 — 遠端 Agent 控制鏈資安檢查
---

# FR-077 總結報告：遠端 Agent 控制鏈資安檢查

> 檢查期間 2026-09-07～17｜分五次檢查完成｜首腦已驗收

---

# 🔴 結論：整條控制鏈只有一件大事，但那一件很大

分五次檢查了 99 個檔案（套件本體 72 個、主專案接線 16 個、借用的檢測套件 11 個），**五次一共通過 24 條發現**。去除重複之後：

| | 幾條 | 說明 |
|---|---:|---|
| **併進既有的修正卡** | **22** | 反覆指向同一件事：客戶機房的代理程式跟雲端之間的通訊，雲端不檢查對方是誰 |
| **全新的問題** | **2** | 都是**檢查範圍以外**順手撈到的密碼外洩，跟遠端代理程式本身無關 |

**這個套件真正的資安問題只有一個，就是 CM-1595。** 它是六個資安檢查案裡**唯一一件被評為「緊急」的**。五次檢查有四次各自獨立地撞到它，每一次都從不同角度多補一塊拼圖——現在它的樣貌已經完整到可以直接動工修了（**現況**：已修，CM-2052）。

**修正現況**：當時（09-17）決策者決定修正卡先不派、等全部掃完分類後統一開工，避免同型問題散在不同卡片各修一半；09-21 起改照內化版問題總表（`docs/security-report/SUMMARY.md`）統一派工。結果：CM-1595（控制面不驗身分）已修（CM-2052，1.21.0 出貨）；CM-1596 健康檢查 SSRF 於 1.21.1 補修（CM-2370）；CM-1597 註冊 Token 裁定不修（落地版僅內網可達，SaaS 前再評估到期或一機一碼）；CM-1598 版控 .env 已撤出（jedi 套件庫 `f6459efd`）。早期四張卡已作廢。

---

## 一、五次檢查各看了什麼、找到什麼

| 次別 | 檢查什麼（白話） | 檔數 | 通過幾條 | 範圍內新問題 |
|---|---|---:|---:|---|
| **R1** | 代理程式的身分與註冊——它怎麼證明「我是誰」 | 42 | 7 | 6（開出 CM-1595～1598 四張修正卡） |
| **R1b** | 密碼學核心——簽憑證、簽通行證、連線加密 | 10 | 2 | 0（另撈到範圍外的 GitLab 權杖 → 總表第 83 項） |
| **R2a** | 任務下發與狀態機＋對外介面與守門殼 | 30 | 5 | 0 |
| **R2b** | 代理程式來拿檔案、雲端連回代理程式 | 13 | 4 | 0（另撈到範圍外的 GitLab／GitHub 權杖） |
| **R3** | 主專案這端怎麼把套件接上來 | 16 | 6 | 0（另撈到公開文件站洩密 → 總表第 84 項） |

**怎麼讀這張表**：R1 那次是第一次掃，所以問題都是新的；**後面四次「範圍內新問題」全是 0**，意思不是白跑——意思是**那四次把同一條攻擊路徑翻來覆去驗證了四遍，都指向同一個結論**。這反而是好消息：問題不是散的，是集中的。

## 二、24 條通過發現怎麼歸併的

| 歸到哪裡 | 幾條 | 哪幾次撞到 |
|---|---:|---|
| **CM-1595**（緊急：控制面端點不檢查身分；✅ 已修 CM-2052） | **13** | R1 四條、R2a 四條、R2b 三條、R3 兩條 |
| CM-1596（測試連線可當內網跳板；✅ 已修 CM-2370） | 2 | R1、R2a |
| CM-1597（註冊通行碼永不過期；🚫 裁定不修） | 2 | R1、R1b |
| CM-1598（套件的 `.env` 進了版控；✅ 已修 jedi f6459efd） | 1 | R1 |
| CM-1629（資料庫密碼散在 249 個檔；✅ 已清 CM-2048） | 1 | R3 的 F1 |
| 總表第 3 項（原廠管理員密碼每套安裝都一樣） | 1 | R3 的 F6 |
| GitLab／GitHub 權杖（併 CM-1573 那條線） | 2 | R1b、R2b |
| **🆕 總表第 83 項**（jedi-issue 歷史檔裡的第二把 GitLab 權杖） | **1** | R1b，越界 |
| **🆕 總表第 84 項**（公開文件站洩露資料庫超管密碼） | **1** | R3，越界 |
| **合計** | **24** | |

**整個 arc 的淨新增只有兩條，而且兩條都是「越界」撈到的密碼外洩**——工具因為設了攻擊面重點，會順手跑一輪密碼掃描，這兩條就是那輪撈到的，跟遠端代理程式本身沒有關係。

**第 84 項值得單獨看一眼。** 它的密碼跟 CM-1629 是同一把，本來不該算新的。但它新在**位置**：那份寫著密碼的需求文件在 `docs/features/` 底下，而那個目錄**每次推上主線就會自動部署到公開文件網站**。首腦 2026-09-17 親自打開那個網址看過——**頁面在線上、密碼字串在上面、四個帳號名可見**。CM-1629 原本的前提是「拿得到程式碼倉庫的人才看得到」，現在變成「**任何人，不用帳號、不用內網，開瀏覽器就看得到**」。這是性質改變，所以另立一項。

---

## 三、CM-1595 現在長什麼樣子

五次檢查把這件事拼完整了。**它不是一個洞，是一整片沒上鎖的牆。**

### 一句話

**我們發給客戶、裝在他們機房的代理程式，跟雲端之間的五條通訊管道，全部不檢查對方是誰。** 程式碼註解裡寫著「不用檢查，因為外層的 nginx 會驗客戶端憑證」——**但出貨的 nginx 設定檔裡，那一行根本不存在**（三個設定檔全部找不到）。

### 五個不檢查身分的端點

| 端點 | 做什麼的 | 打進去能拿到什麼 |
|---|---|---|
| **註冊**（register） | 代理程式第一次安裝時報到 | 取得一張身分憑證 |
| **心跳**（heartbeat） | 每隔一段時間報到、領工作 | 🔴 **客戶自己機器的明文帳號密碼**（SonarQube 權杖、SSH／WinRM 帳密） |
| **收到工作**（ack） | 回報「我收到這份工作了」 | 搶走別台的工作 |
| **回報結果**（result） | 回報「我做完了，結果在這」 | 🔴 **偽造或抹掉稽核證據** |
| **下載檔案**（files） | 來拿要掃描的源碼 | 🔴 **客戶上傳來掃描的源碼壓縮包** |

前四條是套件那側的，第五條是主專案自己手動掛上去的裸端點。

### 「你是哪一家客戶」有三種鍵可以選，全都由攻擊者填

這是這件事最麻煩的部分。伺服器決定「這次動作算在哪一家客戶頭上」的做法是：**拿請求自己送上來的那個編號，在一個刻意繞過客戶隔離的查詢裡去找，找到之後把整個執行環境切換成那一家**。

可以用來選的鍵有三種：

1. **代理程式編號**——任何已登入的使用者在管理頁上就看得到
2. **機器編號**——同一份 VM 樣板複製出來的機器，這個值一模一樣
3. **任務編號**——沒有身分檢查的心跳端點，回應裡就會把它送給你

**三種都要堵，只堵一種等於沒堵。**

### 四個放大器

五次檢查陸續補上的細節，每一個都讓這件事更嚴重：

1. **授權過期進入唯讀模式也照樣打得到**——唯讀中介層的白名單明文豁免了整個 `/agents/` 前綴。授權過期的環境不會因此變安全。
2. **雲端會主動去抓攻擊者指定的檔案**——回報結果時可以附一個檔案編號，雲端的收尾程式會**照那個編號去把檔案抓回來，存成這次檢測的稽核證據**。攻擊者從「能寫一筆假紀錄」升級成「能把自己準備的任意檔案塞進客戶的稽核案卷」。
3. **管理頁把機器指紋回傳出去**——等於送出冒充用的第二把鑰匙。
4. **註冊用的通行碼四層都沒有到期機制**——整家客戶共用同一組、永遠有效、撿到就能用（這一條是 CM-1597，R1b 把它從「引用別處說法」變成「逐檔實證」）。

### 修法主軸

四句話：

1. **呼叫者的身分要由簽章證明**，不是相信請求裡自報的編號。
2. **歸屬比對要做到 domain 層**——不能只在最外層掛一個守門裝飾器，因為只要之後多一條呼叫路徑（批次補登、維運工具、資料修復腳本）就會再繞過一次。
3. **五個端點要一起修**——只修其中幾個，剩下的還是門戶洞開。
4. **nginx 補上客戶端憑證驗證只是連帶**——它只證明「你是一台登記過的代理程式」，**不證明「這張工單是你的」**，所以應用層的比對不能省。

---

## 四、預計怎麼修復

> **以下是掃描當時（2026-09-17）規劃的修法；原四張卡已作廢，實際處理現況見上方「修正現況」。**

### 🔴 CM-1595：五個端點不檢查身分（唯一的「緊急」，優先做）

> **現況**：已修（CM-2052，1.21.0 出貨；原卡作廢）。

**分四步，順序不能顛倒。**

**第一步：先決定用哪一種身分證明 ⚠️ 這步要決策者拍板**

有兩條路，**選一條做主、另一條最多當輔助**：

| 方案 | 怎麼運作 | 優點 | 代價 |
|---|---|---|---|
| **短效通行證**（建議） | 平台本來就會簽這種通行證，讓代理程式每次呼叫都帶上 | **程式本身就有，不必動基礎設施**；應用層自己驗得了，客戶怎麼架都不受影響 | 要處理通行證的發放與更新 |
| **客戶端憑證** | 由 nginx 在最外層驗，驗過的身分往後傳 | 連線層就擋掉，應用層負擔輕 | **要改客戶機房的 nginx 設定**，落地版客戶各自裝機，改動成本高；且它只證明「你是一台登記過的機器」 |

**建議走短效通行證當主軸**——理由是不用碰客戶端的基礎設施。客戶端憑證可以之後再補，但**不能拿它當唯一手段**。

**第二步：五個端點一起掛上驗證**

- 從驗過的通行證推出「你是哪一台代理程式」，**不再讀請求裡自報的編號**
- 同時比對通行證綁定的裝置指紋與資料庫裡存的那個
- **當認證設定是「完整」模式時，沒有驗過身分就直接拒絕**（現在的預設值是「不驗」）

**第三步：歸屬比對要做進 domain 層**

不是在路由上加一個裝飾器就好。要讓「這張工單是不是你的」變成狀態轉換函式的**必填條件**——任何呼叫端都不可能繞過。具體是兩個斷言：工單記的代理程式要等於呼叫者、工單的客戶要等於呼叫者的客戶。

還有一件事必做：那個**繞過客戶隔離的查詢要收窄**，只能用來「查呼叫者自己是誰」，**絕對不能用來查一個由呼叫者指名的東西**。這是整條攻擊路徑成立的關鍵。

**第四步：把機器編號這條路作廢**

心跳端點不得再接受「機器編號」當作身分的替代依據。這條一作廢，管理頁回傳裝置指紋就不再是問題（放大器③自動解除）。

**做完怎麼驗**：
- **手動測試**：不帶任何憑證直接打這五個端點，全部要被擋（401／403）；帶 A 客戶的合法憑證去回報 B 客戶的任務編號，要被擋。
- **要寫測試**（權限與身分驗證屬專案測試政策的例外，必須寫）：把歸屬比對那行拿掉，測試要變紅。
- **回歸測試**：真實的代理程式要還能正常註冊、心跳、領工作、回報——**這條不能漏，改壞了客戶機房的掃描會整條停擺**。

---

### CM-1596：測試連線可以當成內網跳板

> **現況**：已修（CM-2370，1.21.1；原卡作廢）。

**改動小，但要決定「限制到多嚴」。**

管理頁上的「測試連線」按鈕，使用者填什麼網址就去打什麼，沒有任何限制。攻擊者可以拿我們的伺服器當跳板探測內網。

**兩層做法，建議都做**：

1. **送出前驗網址**：只准 http(s)；把主機名稱解析出來後拒絕本機位址、link-local 與私有網段——**解析之後要再檢查一次**，避免解析結果中途被換掉。
2. **回傳改成只說「通或不通」**，不要把對方的狀態碼與原始錯誤訊息照原樣吐回去（攻擊者就是靠這個分辨「服務在但拒絕」與「根本沒東西」）。

**更嚴的選項**（要決策者裁）：乾脆只准打「已經登記在該客戶名下的代理程式位址」。**好處是徹底堵死，代價是管理員沒辦法用這個功能測試一台還沒登記的機器**——這是產品體驗的取捨。

**做完怎麼驗**：手動填幾個內網位址（資料庫、Redis、授權服務）進去測，應該全部被擋；填一台合法的代理程式位址，應該正常回報通或不通。

---

### CM-1597：註冊用的通行碼永遠不會過期

> **現況**：🚫 裁定不修（落地版僅內網可達，SaaS 前再評估；原卡作廢）。

**⚠️ 這張卡依賴 CM-1595，要等它先做完。**

代理程式第一次安裝時要拿一組通行碼來註冊。現在這組碼**整家客戶共用同一組、四層都沒有到期機制、用幾次都可以**。撿到的人可以隨時註冊一台自己的代理程式進去。

**三件事**：

1. **加上到期時間**（建議一次性或短期有效）
2. **加上使用次數上限**（用過就作廢最乾淨）
3. **重新註冊時要求持有證明**——證明你就是原本那台，不是撿到碼的人

**做完怎麼驗**：手動測試——拿一組用過的碼再註冊一次，要被擋；拿一組過期的碼註冊，要被擋；正常的首次安裝流程要還能走完。

---

### CM-1598：套件的 `.env` 檔進了版控

> **現況**：已修（jedi 套件庫 `f6459efd`；原卡作廢）。

**最單純的一張。** `jedi-common` 的 `.env` 被提交進版控了，裡面有設定值。

**做法**：從版控裡移除（保留本機檔案）、改放一份不含真實值的範本、`.gitignore` 補上。

**⚠️ 如果那個檔裡有真的密碼，要換**——移除檔案不會讓版控歷史裡的值消失。動手前先確認裡面到底有什麼。

**做完怎麼驗**：確認版控裡查不到那個檔，且開發環境照範本設定後還能正常啟動。

---

## 五、這次檢查可信到什麼程度

**要分兩件事回答。**

### 第一層：「這些問題是真的嗎」→ ✅ **可信**

| 次別 | 檔數 | 候選→通過 | 投票數 | 漏投 | 中斷 | 驗證章 |
|---|---:|---|---:|---:|---:|---|
| R1 | 42 | 7→7 | **0** | 🔴 21 | 🔴 全滅 | `unverified` |
| R1b | 10 | 5→2 | 15 | 0 | 0 | 渲染退件 |
| R2a | 30 | 7→5 | 15 | 0 | 0 | ✅ `verified` |
| R2b | 13 | — →4 | 12 | 0 | 0 | ✅ `verified`（工作區 dirty） |
| R3 | 16 | 8→6 | **21** | 0 | 0 | ✅ `verified`（工作區 dirty） |

**怎麼讀這張表**：工具會派三個獨立檢查員對每條發現各投一票。「投票數 21」就是「7 條候選 × 3 個檢查員，21 票全投出」。**漏投 0、中斷 0，代表每一條都被完整審過。**

R1 那次投票全滅（額度用盡），所以它的七條**是靠人工逐條開檔核對出來的**。但後面四次把同一條路徑又驗了四遍、每次都 3:0 通過——**R1 的結論等於被重新背書了四次**，這比它自己一次就投票成功還可靠。

**而且首腦沒有只採信工具**：CM-1595 的核心（「切換成哪一家客戶是由攻擊者填的值決定的」）是首腦 2026-09-16 自己開檔比對出來的，當時甚至**推翻了自己前一天把它降級的決定**；R3 那條公開網站洩密，是首腦**親自打開網址看到頁面才登記的**，不是讀程式碼推的。

### 第二層：「是不是只有這些」→ ⚠️ **中等偏高，但仍不能說「掃乾淨了」**

**好的一面**：

- **整條鏈五棒全掃完了**——套件本體、借用的檢測套件、主專案接線，沒有留空白段
- 後四次都**面板完整跑完、驗證章通過**
- 卡片點名要查的重點，工具沒碰到的部分**首腦全部人工補查過**，而且結論都是乾淨的（R3 八個重點裡六個是人工補的，六個全乾淨）

**不能宣稱完整的一面**：

1. **五次全是低強度快篩**——沒有元件盤點、沒有威脅建模、沒有廣度掃蕩
2. **沒有逐檔閱讀紀錄**——工具的閱讀帳本是空的（`coverage.research` 是 `null`），無法證明每個檔都被讀到結論
3. **R2b 與 R3 掃描時工作區不乾淨**——有別條線的未提交改動，那些改動不在檢查範圍內、不影響結論，但嚴格說掃的不是一棵乾淨的樹

**結論：這些問題都是真的；「這條控制鏈只有這些問題」這句話不成立。**

---

## 六、這次換到的兩條經驗

### ① 一次 42 個檔會全滅，拆小四棒全部跑完

R1 那次一口氣掃 42 個檔，結果**105 個檢查員全部因額度用盡掛掉、21 張票一張都沒投出來**——工具的正式產出是空的，七條發現全靠人工補核才留下來。當時決策者因此**把整個 arc 暫停**，理由很直接：「這樣的產出無法用來證明這個套件檢查過了」。

續行時改了尺，拆成四小棒（10／30／13／16 檔），**四棒的面板全部完整跑完**。

**差別不只在檔數**——R2a 那棒也有 30 個檔，一樣跑完了。真正的差別是**有沒有跟別的工作搶額度**。所以規矩不是「檔數要少」，是「**一次只跑一棒，不併行**」。

### ② 同一件事被四棒從四個角度撞到，才拼出完整形狀

如果只看 R1，CM-1595 是「四個端點沒認證」。五棒跑完之後它變成「**五個端點、三種可選的身分鍵、四個放大器**」——而這些不是靠更用力掃同一個地方掃出來的，是**換角度看出來的**：

- R2a 從任務服務那一側看 → 發現任務編號也能拿來選客戶
- R2b 從狀態機那一側看 → 發現只在路由掛守門不夠，要做到 domain 層
- R3 從主專案接線那一側看 → 發現明文帳密是在哪一行生出來的、還多一個裸掛的端點

**「範圍內新問題 0」的四棒，價值全在這裡。** 換角度比換力氣有用。

---

## 七、五份詳細報告

| 報告 | 檢查什麼 | 範圍內新問題 |
|---|---|---|
| [R1 代理程式的身分與註冊](scan-R1-agent-identity.md) | 42 檔 | **6（開出四張修正卡，含唯一的「緊急」）** |
| [R1b 密碼學核心](scan-R1b-crypto-core.md) | 10 檔 | 0（越界撈到總表第 83 項） |
| [R2a 任務下發與狀態機](scan-R2a-task-dispatch.md) | 30 檔 | 0（補完 CM-1595 的四點細節） |
| [R2b 檔案取用與 agent 連線](scan-R2b-file-access.md) | 13 檔 | 0（找到第五個裸奔端點） |
| [R3 主專案宿主接線](scan-R3-host-wiring.md) | 16 檔 | 0（越界撈到總表第 84 項，公開站洩密） |

---

## 八、交付狀況

🔴 **五棒之中，三棒的 runner 四件全未交**（沒寫報告、沒 commit、沒回寫 Notion、沒回報）：R1b、R2a、R3。這三份報告都是**首腦到掃描目錄自己撈產物、自己逐條開檔核對、自己補寫**的。R2b 那棒交付齊全。

三次分別是全計畫的第九、第十、第十一次 runner 交付掛零。
