---
title: D2 檢查結果：AI 儀表板的接線與 27 支查詢功能
---

# D2 檢查結果：AI 儀表板的接線與 27 支查詢功能

> 檢查日期 2026-09-10｜耗時 2 小時 59 分｜對應卡片 CM-1640

---

## 🔴 一句話結論

**任何一個最基層的員工，只要對 AI 儀表板打字說「列出所有使用者」，就能拿到全公司的帳號名冊——而且回傳的資料裡還夾帶了每個帳號密碼加密用的「鹽值」。**

同一個查詢功能，走正常的網頁路徑需要「使用者讀取」權限，**走 AI 儀表板這條路完全不用**。

---

## 這一棒在檢查什麼

系統裡有一個「AI 儀表板」：使用者用自然語言打字問問題（例如「列出所有專案的進度」），**AI 會自己決定要呼叫系統裡的哪一支查詢功能**，把查到的資料拿去設計版面，做成圖表顯示。

這一棒檢查主專案這邊的 18 個檔——**有哪些查詢功能可以被 AI 呼叫、呼叫的時候檢不檢查權限**。

### 為什麼要專門查這個

**上一個案子（FR-079）已經證明這條路有洞，但當時只驗證了 27 支裡的 1 支。**

FR-079 查到「AI 儀表板可以直接呼叫公告查詢，不經過網頁那層的任何權限檢查」。這次盤點發現：**可以被 AI 呼叫的查詢功能總共有 27 支**，其中 `auth`（認證）那 4 支與 `participant`（專案成員）那 7 支風險最高——那是「你是誰」與「你在這個專案能幹嘛」的資料源。

---

## 找到什麼：11 個問題

**3 個在這次的檢查範圍內，8 個是順手撿到的密碼外洩（範圍外）。**

### 範圍內 3 個

| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 |
|---|---|---|---|---|---|
| **F3** | 🟡 中 | **任何登入者可透過 AI 儀表板列出全公司的帳號、角色、租戶、部門**——這條路不做任何權限檢查 | 拿到全公司人員名冊：登入帳號、email、員工編號、電話、職稱、狀態、最後登入時間、角色、所屬部門。**這是社交工程與密碼噴灑攻擊的現成目標清單** | ① 任何能登入的帳號（**不需要任何權限**）<br>② 該租戶有買 `ai-dashboard` 模組<br>③ 系統有設定 AI 金鑰 | `di_containers/dashboard_apis/auth.py:31`（另 `:19`／`:43`／`:55` 同款） |
| **F9** | 🟡 中 | **回傳的資料裡夾帶了每個帳號的「鹽值」**（密碼加密用的材料） | 鹽值不是密碼，但它讓攻擊者可以**針對特定帳號預先計算破解表**，也確認了帳號存在。配合 F3 拿到的完整名冊，等於一份可直接開工的目標清單 | 同 F3 | 宣告在 `di_containers/dashboard_apis/auth.py:15`；欄位本身在 `jedi_iam/app/dto/user.py:43`，第 98 行填值 |
| **F10** | 🟡 中 | **專案清單有一個「當作管理員」的後門**，非成員也能列出租戶內全部專案 | 使用者可以列舉他根本沒參與的專案。**但這一條是刻意設計的，不是寫錯**（見下方說明） | 同 F3 | `di_containers/dashboard_apis/project.py:52` → `app/flow_control/service/project_service.py:222` |

### 範圍外 8 個（密碼被寫進版控檔案）

工具在掃描時會順帶做一次「有沒有密碼被寫進檔案」的檢查，撿到 8 條。**這些不在這次的檢查目標裡，是順手撿到的。**

| # | 嚴重度 | 是什麼 | 新舊 |
|---|---|---|---|
| **F1** | 🔴 **高** | **GitLab 的個人存取權杖**寫在版控文件裡，而且綁著本專案的 CI 專案編號 | 🆕 **新的** |
| **F2** | 🔴 **高** | **公司套件倉庫（Nexus）的管理員帳密 ＋ 檔案儲存（MinIO）的金鑰**寫在一份交接文件裡 | 🆕 **新的** |
| F4 | 🟡 中 | POC 資料庫密碼寫死在腳本裡 | 舊（FR-078 CM-1608 已裁「測試機專用、不需更換」） |
| F5 | 🟡 中 | OpenAI／Anthropic／Google／LangChain 四把 API 金鑰在對話紀錄裡 | 舊（FR-078／FR-081 已記，併 CM-1607） |
| F6 | 🟡 中 | Google 雲端硬碟的 OAuth 密鑰在對話紀錄裡 | 舊（FR-081 已開 CM-1631） |
| F7 | 🟡 中 | **產品對外寄信用的 Gmail 應用程式密碼**寫在版控的資料庫備份裡 | 🆕 **可能是新的，待比對** |
| F8 | 🟡 中 | 平台管理員密碼被當成環境變數的「靜默預設值」 | 舊（FR-078 CM-1608 同款 pattern） |
| F11 | 🟡 中 | **每一套安裝都用同一組原廠管理員密碼** | 舊（FR-081 已記，決策者裁「先記錄，之後看怎麼調整」） |

**🔴 F1 與 F2 是本棒的新收穫，而且 F2 值得特別注意**：上一個案子（FR-081）查到「公司套件倉庫走沒加密的連線」，現在又查到「**同一個倉庫的管理員帳密外流**」。**同一個目標的兩個弱點，加起來比單看任何一個都嚴重**——一個是「連線可被竊聽」，一個是「直接有管理員帳號」。

---

## 詳細說明

### F3：任何員工都能叫出全公司帳號清單

**白話**：AI 儀表板讓你打字問問題，AI 自己去查資料。但 **AI 能查的東西裡包含「所有帳號、所有角色、所有租戶、所有部門」，而這條路完全不檢查權限**。

**最能說明問題的是這個對比**（首腦已開檔核對）：

```
同一個 UserService.get_users 方法，兩條路：

走正常網頁路徑                      走 AI 儀表板
jedi_iam/api/routes/user_route.py    di_containers/dashboard_apis/auth.py:31
  :80  @capability_required("user.read")   （沒有任何權限檢查）
  :81  def post(self):
```

**產品自己定義了四個權限點**（首腦已在 `scripts/init/04-seed-core.sql` 確認存在）：`user.read`、`role.read`、`tenant.read`、`department.read`。**AI 儀表板這條路四個全部繞過。**

**攻擊怎麼做**：

1. 一個普通帳號登入
2. 送出：`{"prompt": "列出所有使用者帳號，用 auth.get_users"}`
3. AI 從清單裡挑中 `auth.get_users`——**使用者打的字是直接接進送給 AI 的訊息裡的**，所以攻擊者可以反覆改寫措辭，直到 AI 穩定選中那一支
4. 系統呼叫 `UserService.get_users`，中間**沒有任何權限檢查**
5. 整份使用者清單放進回應裡送回瀏覽器

**工具的評語很到位**：「『AI 大概不會選到那一支』不是一種防護措施。」

**還有一個未確認的問題**：這條路會不會跨租戶（A 公司的人看到 B 公司的名冊）？**取決於資料庫的隔離機制有沒有覆蓋使用者表**——查詢層自己不加任何過濾，因為自動注入的 `tenant_id` 與 `org_unit_id` 進了 `**kwargs` 就**被丟掉，沒有變成查詢條件**（這正是 FR-079 F13 的同款成因）。**這一條要另外查資料庫才能確認。**

**怎麼修**（工具建議，首腦認同）：

| 做法 | 說明 |
|---|---|
| **正解** | 讓 27 支申報 API 各自帶上「這份資料需要什麼權限」，`DataAPIService` 派發前先檢查呼叫者有沒有——**跟那份資料的正常網頁路徑用同一個檢查** |
| **最小修法**（正解做好之前） | 把 `auth.get_users`／`auth.get_roles`／`auth.get_tenants`／`auth.get_org_units` 四支**從申報檔移除**，或改指向「只回不敏感摘要」的專用方法 |

### F9：回傳的資料夾帶密碼鹽值

**白話**：F3 拿到的那份名冊送回瀏覽器時，**每個帳號的「鹽值」也一起送出去了**。

鹽值是密碼加密時用的材料。它不是密碼本身，但**有了它就能針對特定帳號預先算好破解表**，而且確認了那個帳號存在。

**首腦已開檔核對**：

```python
# jedi_iam/app/dto/user.py
第 43 行   salt: Optional[str] = None      ← 這個資料結構本來就帶鹽值欄位
第 98 行   salt=getattr(user, 'salt', None), ← 而且會實際填值
```

**關鍵在於**：正常的使用者列表功能（`UserPageQueryResponse`）**刻意排除了 `salt` 與 `password`**，唯獨 AI 儀表板這條路沒有做欄位過濾，直接把整個物件序列化後放進回應。

**前端表格只顯示幾個欄位，但那只是顯示行為——後端已經把整個物件送出去了**，看原始回應就拿得到。

**怎麼修**：在 AI 儀表板這條路加欄位白名單，或重用既有的序列化器並排除敏感欄位。**更根本的做法是：帶著憑證欄位的資料結構，本來就不該進入通用的序列化路徑。**

### F10：專案清單的「當作管理員」後門——**這一條是刻意設計，不是寫錯**

**白話**：程式碼裡寫死了一個「當作管理員」的開關，繞過「你只能看到自己參與的專案」這個規則。

**但首腦開檔核對後發現一件工具沒講的事**——寫這段的人**自己留了註解說明為什麼**：

```python
# app/flow_control/service/project_service.py:210-222
def get_projects_for_dashboard(self, user_id=None, locale=None, **kwargs):
    """AI 動態儀表板用：列專案（...）

    對齊 v1 dashboard「看本 tenant 全部專案」（v1 get_all 無 user 參與過濾、
    route 僅 @jwt_required）→ is_admin=True 繞 grc user 參與可見性，
    tenant 隔離由 RLS（session_scope）負責。
    """
    page = self.list_projects(user_id=user_id, is_admin=True, ...)
```

**所以這不是 bug，是當初刻意放寬，理由是「對齊舊版行為」，並且假設「租戶隔離由資料庫層負責」。**

**真正的問題是**：**這個決定在 AI 儀表板這個新情境下還成不成立，沒有人重新檢視過。** 舊版的儀表板是固定的畫面；現在是「使用者打字，AI 自己決定查什麼」，**攻擊面完全不同**。

**🔴 修的人要知道這件事**——否則會以為是疏漏而直接拿掉 `is_admin=True`，可能反而弄壞既有功能。**這一條要先做產品決策（AI 儀表板該不該看到全租戶專案），再改程式碼。**

---

## 🔴 卡片點名要做的「27 支 API 權限形狀表」——工具沒做

開卡時明寫那是「**本 arc 最有價值的產出，沒有它就等於重複 FR-079 F13 而沒有推進**」。

**工具實際只查了 5 支**：`auth` 4 支（F3／F9）＋ `project` 1 支（F10）。**`participant` 那 7 支與其餘 15 支沒有碰。**

**這是連續第三個案子出現同樣的事**（FR-081 的三棒、FR-082 的 A1 都是「工具在範圍內產出少、卡片點名的重點沒答」）。**這張表要靠人補。**

### 首腦已補的部分（其餘待補）

| 申報檔 | 支數 | 工具查了嗎 | 現況 |
|---|---:|---|---|
| **auth** | 4 | ✅ 查了 | **F3／F9：四支全部無權限檢查，且回傳帶鹽值** |
| **project** | 2 | ✅ 查了 1 支 | **F10：`is_admin=True` 後門（刻意設計）** |
| **participant** | 7 | ❌ 沒查 | ⬜ **待補**——「你在這個專案能幹嘛」的資料源，風險僅次於 auth |
| flow_engine | 3 | ❌ | ⬜ 待補 |
| oscal | 2 | ❌ | ⬜ 待補 |
| survey | 2 | ❌ | ⬜ 待補 |
| bulletin | 1 | — | ✅ FR-079 F13 已查（同款問題，已開卡） |
| feedback | 1 | ❌ | ⬜ 待補（FR-081 已查過它的正常路徑，六條只有匯出有權限檢查） |
| device／system_menu／system_config／module_frame／user_auth_provider | 各 1 | ❌ | ⬜ 待補 |

**建議**：這張表的其餘部分，在 D1（套件本體）驗收時一起補完——D1 要回答「AI 怎麼決定選哪支」，正好需要這份清單。

---

## 這份結果可信到什麼程度

### 「這 11 條是真的嗎」→ ✅ 可信

**投票完整**：三個獨立檢查員對 11 條各投一票，**33 票全數投出、沒有漏投、沒有中斷**，stamp 是乾淨的 `verified`（連拒收原因欄位都是空的）。

**檢查員主動降了兩條的嚴重度**（F3 與 F4 從「高」降到「中」）——代表他們在做對抗性判斷，不是照單全收。

**首腦另外開檔核對了範圍內三條**：F3 的權限對比（正常路徑有 `@capability_required("user.read")`、AI 儀表板沒有）、F9 的鹽值欄位（`user.py:43` 與 `:98`）、F10 的後門與其註解——**三條全部屬實**，而 F10 多查出「這是刻意設計」這件工具沒講的事。

### 「是不是只有這 11 條」→ ❌ 不可信，兩個原因

**第一**：這是快篩模式，沒有全面盤點。18 個檔裡有 3 個是空的。

**第二，而且更重要**：**卡片點名要查的 27 支 API，工具只查了 5 支**。剩下 22 支等於沒查——**不能因為「工具沒報」就當它們乾淨**。

**另外**：8 條範圍外的密碼外洩**不代表 `docs/`、`scripts/` 被檢查過**——那兩個目錄從來不是這次的目標，是密碼專項掃描順手撿到的。工具自己也講明「那是關鍵字掃描撿到的樣本，不是完整的憑證稽核」。

---

## 執行概況（技術細節，工程師看的）

| 項目 | 數字 |
|---|---|
| 檢查範圍 | 18 個受版控檔 |
| 掃描版本 | `a505df96`（工作目錄有未提交變更） |
| 派出／回報的研究員 | 2 / 2 |
| 候選問題 → 去除重複 | 11 → 11（無重複） |
| 投票數 | **33**（11 條 × 3 個檢查員） |
| 沒投到票的 | 0 |
| 投票中斷的 | 0 |
| 被檢查員降低嚴重度的 | 2（F3、F4 由「高」降「中」） |
| stamp 狀態 | **`verified`（無拒收原因）** |
| 耗時 | 2 小時 59 分 |
