---
title: I2 檢查結果：主專案的「意見回饋」功能
---

# I2 檢查結果：主專案的「意見回饋」功能

> 檢查日期 2026-09-09｜耗時 2 小時 16 分｜對應卡片 CM-1614

---

## 🔴 一句話結論

**六棒裡最嚴重的一棒。** 找到兩件事：**資料庫密碼被寫進 249 個文件裡**（其中一份還附上完整的連線指令），以及**意見回饋的六個功能有五個沒有檢查權限**——任何員工都能刪掉別人的回饋。

---

## 這一棒在檢查什麼

「意見回饋」是使用者在系統裡回報問題的功能。送出後，系統會：
1. 存進自己的資料庫
2. 如果管理員有開啟整合，還會**拿公司的權杖去 GitHub／GitLab 開一張真的問題單**
3. 使用者上傳的附件也一起送過去

這一棒檢查主專案這邊的 31 個檔案——**誰能用這些功能、輸入的東西怎麼流出去**。

---

## 找到什麼：10 個問題

| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 修正卡 |
|---|---|---|---|---|---|---|
| 1 | 🔴 **高** | **資料庫管理員的密碼（也是 Redis 的密碼，同一組）被寫在 249 個受版控的文件裡**。其中一份文件把「主機位址、埠號、帳號、資料庫名稱、密碼」**五樣東西放在相鄰兩行**，是一條複製貼上就能用的連線指令 | 拿得到程式碼倉庫的人可以**直接連進資料庫**。這個帳號會**繞過「每個客戶只能看自己資料」的隔離機制**，所有客戶資料可讀可寫。而那份文件指的是 **POC 機器**，本專案規定 POC 等同正式環境 | ① 讀得到程式碼倉庫<br>② 連得到那台機器（在公司內網，或有 VPN） | `docs/analysis/2026-05-28-poc-db-migration-plan.md` 第 73 行等 249 個檔 | **CM-1629** |
| 2 | 🟡 中 | **刪除意見回饋時，系統不檢查那筆是不是你的** | 任何登入者可以刪掉別人的回饋。連帶會**關閉已經同步到 GitHub／GitLab 的問題單**，而稽核紀錄還會把攻擊者記成合法的操作人 | 任何一個普通員工帳號 ＋ 知道目標的編號（**從列表就拿得到，列表也沒有權限檢查**） | `feedback_route.py:88`<br>`feedback_service.py:239` | **CM-1630** |
| 3 | 🟡 中 | **修改意見回饋同樣不檢查歸屬** | 任何登入者可以竄改別人的回饋內容 | 同上 | `feedback_service.py:196` | **CM-1630** |
| 4 | 🟡 中<br>⚠️ 範圍外 | **Google 雲端硬碟的「應用程式密鑰」**被寫在 12 個文件裡 | 攻擊者可以**冒充我們的系統**向 Google 要權限，讀取客戶存在雲端硬碟的檔案。**這把不是每套安裝各自產生的**，要到 Google 後台重設 | 讀得到程式碼倉庫 ＋ 該密鑰還沒重設 | `docs/conversation-history/...` 12 個檔 | **CM-1631** |
| 5 | 🟡 中<br>⚠️ 範圍外 | **保護客戶雲端硬碟權杖的加密金鑰**被寫在 10 個文件裡 | 配合問題 1 拿到資料庫之後，可以把**所有客戶存著的雲端硬碟權杖全部解密** | 讀得到程式碼倉庫 ＋ **要先透過問題 1 拿到資料庫** | `docs/conversation-history/...` 10 個檔 | **CM-1631** |
| 6 | 🟡 中<br>⚠️ 範圍外 | **登入憑證的簽章金鑰**被寫在 30 個文件裡 | 理論上可偽造任何人的登入身分。**但已查明每套安裝都會各自產生一把新的，外流的只是我們開發機那把** | 讀得到程式碼倉庫 ＋ 連得到開發機 | `docs/conversation-history/...` 30 個檔 | 併入 **CM-1607** |
| 7–10 | 🟡 中×3<br>⚪ 低×1 | 其餘的權限與輸入檢查缺口（列表沒有權限檢查、附件刪除、錯誤訊息透露太多等） | 影響範圍限於同一個客戶內部的意見回饋資料 | 任何登入帳號 | `api/feedback/`、`app/feedback/` | **CM-1630** |

---

## 詳細說明

### 問題 1：資料庫密碼散在 249 個文件裡（唯一的「高」）

**白話**：資料庫的管理員密碼，被寫進了 249 個會跟著程式碼一起散出去的文件裡。而且其中一份，把連線需要的**所有資訊湊在一起**：

```bash
# docs/analysis/2026-05-28-poc-db-migration-plan.md 第 71-73 行
export PGPASSWORD=<密碼>
PSQL="psql -h 192.168.50.189 -p 25432 -U cmmgr -d guidant_ai_poc ..."
#          ↑主機          ↑埠號    ↑帳號  ↑資料庫名
```

**主機、埠號、帳號、資料庫名、密碼——五樣齊全，複製貼上就能連。**

而 `cmmgr` 這個帳號的權限是「**繞過所有客戶隔離**」，也就是說連進去之後，**所有客戶的資料都看得到、改得動**。指向的那台機器是 **POC**，依專案規定「等同正式環境、對外 demo、客戶會試玩」。

**首腦自己驗證的數字**（不是採信報告）：

```
資料庫密碼   最新版 249 個檔 / 已推上遠端的主線 248 個檔
Redis 密碼   最新版 249 個檔 / 已推上遠端的主線 248 個檔   ← 和資料庫密碼是同一組字串
```

**報告沒強調、但很重要的一點：Redis 和資料庫用的是同一組密碼。**

#### 這 249 個檔是什麼？為什麼會有？

**全部是文件，沒有一支是會跑的程式碼**：

| 類型 | 數量 | 是什麼 |
|---|---:|---|
| `docs/conversation-history/` | 112 | **我們的對話紀錄** |
| `docs/features/` | 66 | 需求文件、實作計畫、交接文件 |
| `docs/features-site/` | 65 | 上面那些文件轉成的網頁（同內容重複一份） |
| 其他 | 6 | memory、issue 紀錄等 |

**根因**：密碼總共出現 2,011 行，九成以上長這樣：

```
881 行   "command": "PGPASSWORD='<密碼>...
173 行   PGPASSWORD='<密碼>...
 38 行   **Bash**(command=PGPASSWORD='<密碼>...
```

**不是有人故意把密碼寫進文件，是「工作過程被完整記錄下來」的副作用**——每次要查資料庫打的指令，連同密碼被寫進對話紀錄，對話紀錄再依規定進版控；實作計畫和交接文件也照抄同樣的指令當成「怎麼跑」的說明。

#### 🔴 這個機制已經是第三次了

1. **2026-09-08 `.env.test` 事件**——四把 API 金鑰進版控，已撤銷重發
2. **FR-079**——登入憑證簽章金鑰散在 30 個檔
3. **這次**——資料庫／Redis 密碼散在 249 個檔

**所以只清檔案是治標。** 修正卡 CM-1629 要求先做治本：**改用 `~/.pgpass`（psql 原生支援，指令裡就不必出現密碼）**、**對話紀錄歸檔前先遮罩**，再清檔案。否則下次還會再長出來。

**怎麼修**：詳見 CM-1629。要注意的是**換密碼這件事**——DEV 和基線庫可以規劃，但 **STG／POC 屬於環境異動，依專案鐵律要決策者當次明確指示才可以動**。

### 問題 2、3：意見回饋的六個功能，只有一個檢查權限

**白話**：意見回饋有六個功能。**只有「匯出」那個會檢查你有沒有權限，其他五個任何登入者都能用。** 而且修改和刪除**從來不檢查那筆資料是不是你的**。

首腦逐條打開檔案核對：

```
api/feedback/routes/feedback_route.py

第 24 行  列表  只有「要登入」          ← 只有這個
第 42 行  讀取  只有「要登入」
第 50 行  新增  只有「要登入」
第 67 行  修改  只有「要登入」
第 85 行  刪除  只有「要登入」
第 95 行  刪附件 只有「要登入」
第 104 行 匯出  要登入 ＋ 要有「匯出」權限   ← 只有匯出有檢查權限
```

**同一支檔案裡就有反例**——這證明「這裡本來就該有權限檢查」不是猜測。

再往下追一層：

```
app/feedback/service/feedback_service.py

第 196 行  update_feedback(uid, ..., user_login_name=None)   ← 有收到「誰在操作」
第 233 行      feedback_issue.updated_user = user_login_name  ← 但只拿去填「最後修改人」欄位
第 239 行  delete_feedback(uid, login_user_name)            ← 有收到「誰在操作」

整支檔案沒有任何一行，拿這個人的身分去跟「建立者」比對。
```

**攻擊情境很簡單**：一個普通員工登入 → 打列表拿到別人的回饋編號 → 送出刪除請求 → 對方的回饋沒了，連帶關閉了 GitHub 上的問題單，而稽核紀錄記的是「這個員工合法操作」。

**這是本專案第三次犯同樣的錯**：CM-1585、CM-1589（都已修完）、FR-079 的公告功能（待修）。**修法可以直接抄 CM-1589。**

### 問題 4、5、6：三把金鑰外洩，但性質不同

三把都是被寫進對話紀錄的（**和問題 1 同一個根因**），但**處置方式不一樣**：

| 金鑰 | 每套安裝各自產生嗎？ | 影響 | 怎麼辦 |
|---|---|---|---|
| **Google 應用程式密鑰**（問題 4） | ❌ **不是**。是 Google 後台的一組，所有環境共用 | 外洩就是真的外洩 | **要到 Google 後台重設**再發到各環境 |
| **雲端硬碟加密金鑰**（問題 5） | ❌ 不是，是設定檔裡的固定值 | 配合問題 1 可解密客戶權杖 | 換新金鑰，**但要先寫好「把既有權杖重新加密」的程式**，否則客戶的整合會全部失效 |
| **登入憑證簽章金鑰**（問題 6） | ✅ **是**。每套安裝跑 `openssl rand` 各自產生 | **外流的只是我們開發機那把** | 降為清理工作，併入 CM-1607 |

**這個對比很重要**：同樣是「金鑰外洩」，問題 6 只影響我們自己的開發機，問題 4 卻影響所有客戶。**判斷嚴重度時，第一個要問的是「客戶用的是不是同一把」。**

---

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

### 「這十條是真的嗎」→ ✅ 可信度是六棒最高的

**這是六棒裡唯一拿到完整 `verified` 標記的一棒。** 十條發現**每一條都經過三個獨立檢查員投票、三票全過**，30 票全數投出、沒有漏投、沒有中斷。

**而且檢查員主動把四條降級了**（原本研究員判「高」的被降成「中」）——**這代表他們真的在做對抗性判斷，不是橡皮圖章。**

**首腦也沒有只採信工具**：
- 問題 1 的 249 個檔是自己數的（與報告吻合），還多發現「Redis 和資料庫共用同一組密碼」
- 問題 2、3 的六條功能守門形狀是自己打開檔案看的
- 249 個檔的來源分類是自己統計的

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

**第一層**：這是快篩模式，沒有全面盤點、沒有威脅建模。

**第二層（這棒特有的）**：**五條「範圍外」的發現，不代表那些地方被檢查過。** 那五條是「密鑰專項掃描」順手撈到的，而 `docs/`、`scripts/` 這兩個目錄**從來不是這次的檢查目標**。也就是說，那兩處等於**沒檢查過**。

---

## 執行概況（技術細節）

| 項目 | 數字 |
|---|---|
| 檢查範圍 | 31 個檔案 |
| 派出／回報的研究員 | 2 / 2 |
| 候選問題 → 去除重複 | 13 → 10 |
| 投票數 | **30**（10 條 × 3 個檢查員） |
| 沒投到票的 | 0 |
| 投票中斷的 | 0 |
| 被檢查員降低嚴重度的 | **4** |
| 耗時 | 2 小時 16 分 |
