---
title: FR-081 總結報告 — 問題追蹤功能資安檢查
---

# FR-081 總結報告：問題追蹤功能資安檢查

> 檢查期間 2026-09-09～10｜分六次檢查完成｜首腦已驗收

---

# 🔴 結論：要修 6 件事，其中 1 件緊急

分六次檢查了 158 個檔案，去除重複後**歸納成 6 件要修的事**：

| 緊急程度 | 幾件 | 說明 |
|---|---:|---|
| 🔴 **高（建議優先）** | **1** | 資料庫密碼外洩，且指向等同正式環境的機器 |
| 🟡 中 | **4** | 權限沒檢查、金鑰外洩、連線沒加密 |
| ⚪ 低 | **1** | 程式碼寫錯一半，目前還打不到 |

**目前全部只是建卡歸檔，一件都還沒開始修**——依既有決定，由 PM 統一安排時程。

---

## 一、要修什麼（六件事）

| 卡號 | 這是什麼問題 | 出事會怎樣 | 緊急度 | 建議順序 |
|---|---|---|---|---|
| **CM-1629** | **資料庫管理員密碼（也是 Redis 密碼，同一組）被寫進 249 個文件裡**。其中一份還把主機、帳號、密碼湊成一條可直接使用的連線指令 | 拿得到程式碼的人可**直接連進資料庫**，這個帳號會**繞過所有客戶隔離**，所有客戶資料可讀可寫。而那份文件指的是 POC 機器（**規定上等同正式環境**） | 🔴 **高** | **1** |
| **CM-1630** | **「意見回饋」六個功能只有「匯出」會檢查權限**，其餘五個任何登入者都能用；而且修改、刪除**從不檢查那筆是不是你的** | 任何員工可以刪掉或竄改別人的回饋，連帶關閉 GitHub 上的問題單，**稽核紀錄還把他記成合法操作人** | 🟡 中 | **2** |
| **CM-1631** | **Google 雲端硬碟的應用程式密鑰與加密金鑰外洩**（22 個檔） | 配合上一條拿到資料庫後，可**解密所有客戶存的雲端硬碟權杖**，讀取客戶檔案 | 🟡 中 | **3** |
| **CM-1632** | 連 GitHub 時**沒檢查對方是不是真的 GitHub**（兩處） | 網路中間人可假冒 GitHub，**攔走公司的 GitHub 存取權杖** | 🟡 中 | 4 |
| **CM-1634** | **公司內部套件倉庫走沒加密的連線**，而且是主要來源（**26 個專案全部一樣**） | 內網有心人可在安裝套件時**掉包**，等於在打包機上執行他的程式碼——**而打包機產出的是客戶拿到的安裝檔** | 🟡 中 | 5 |
| **CM-1633** | 刪除附件時**安全檢查只做了一半**（算了過濾結果卻沒拿去用） | 目前打不到；未來若做批次刪除功能，會**靜默刪錯檔案**且不報錯 | ⚪ 低 | 6 |

另有 3 件（登入憑證金鑰、驗證碼金鑰、舊套件設定檔）**併入既有的 CM-1607** 一起清理，不另開卡。

---

## 二、預計怎麼修

### 🔴 CM-1629：資料庫密碼外洩（優先做）

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

**第一步：先堵住再發生的路**（不做這步，清了還會再長出來）

| 做什麼 | 為什麼 |
|---|---|
| 改用 `~/.pgpass` 或 `PGPASSFILE` 存密碼 | psql 原生支援。指令裡就不必打密碼，**對話紀錄自然錄不到** |
| 對話紀錄歸檔前先遮罩 | 在既有的歸檔流程加一步，把 `PGPASSWORD='...'` 換成 `***` |
| 評估加自動秘密掃描 | 在提交前或 CI 擋下來。**新增基礎設施要決策者裁，本卡只做評估回報** |

**第二步：清掉已經寫進去的**

- 249 個檔裡的密碼字串換成「請查 `.env`」
- **網頁版不用手改**——它們是文件產生的，改完文件重跑產生指令即可
- 那份有完整連線指令的文件（`2026-05-28-poc-db-migration-plan.md`）**優先處理**
- **不重寫版控歷史**（決策者 2026-09-08 已裁定：重寫會改動所有提交編號、影響每個下載過的人，而對已散出去的內容毫無補救效果）

**第三步：換密碼 ⚠️ 這步要決策者明確指示**

- **DEV 和基線庫**：屬開發環境，可以規劃
- **STG／POC**：**屬環境異動，依專案鐵律要決策者當次明確指示才可以動**
- 換的時候要同步：設定檔、機器上的環境變數、Redis 設定、所有連線腳本
- **建議把 Redis 和資料庫的密碼分開**（目前是同一組）

**做完怎麼驗**：搜尋新密碼在版控中的出現次數應為 0（**驗證時不要把密碼印在畫面或提交訊息裡**）；`~/.pgpass` 的用法要實際跑一次 migration 確認可用。

---

### CM-1630：意見回饋沒檢查權限

**分兩層改，兩層都要。**

**第一層：加上「你有沒有權限做這件事」的檢查**

- 用現有的統一守門機制（`common/authz/`），**不要另外寫一套**
- **先查資料庫的權限清單**：`feedback.export` 已經存在，要確認「讀、新增、修改、刪除」有沒有也定義了
  - **有定義** → 直接掛上去
  - **沒定義** → 新增權限項目屬產品決策，**先回報問決策者再動手**
- ⚠️ 注意 FR-079 的教訓：權限項目可能**有定義、有綁在頁面上、還寫進設計文件，卻沒有任何程式碼在檢查它**。查的時候要看資料庫，不能只看程式碼

**第二層：加上「這筆資料是不是你的」的檢查**

放在業務邏輯層（依專案規範，這種檢查不能放在最外層，因為要先把資料撈出來才知道要判誰）：

```python
@transaction
def delete_feedback(self, uid, login_user_name):
    fb = self.feedback_issue_domain_service.get_by_uid(uid)
    if fb is None:
        raise NotFound(...)
    if fb.created_user != login_user_name and not <有管理權限>:
        raise ForbiddenError(...)
    ...
```

**還要決定一件事**：列表功能應該是「只看自己的」還是「同公司都看得到」？**這是產品決策，動手前先問決策者。**

**要寫測試**（權限邏輯屬專案測試政策的例外，必須寫）：把歸屬檢查那行拿掉，測試要變紅。

**手動測試**：用帳號 A 建一筆 → 用帳號 B 嘗試改／刪 → 應該被擋（403）；帳號 A 改自己那筆 → 應該成功。

---

### CM-1631：Google 金鑰外洩

**⚠️ 重設金鑰要決策者明確指示。**

| 金鑰 | 怎麼換 | 注意事項 |
|---|---|---|
| **Google 應用程式密鑰** | 到 Google Cloud 後台重設，新值分發到各環境 | **STG／POC 屬環境異動要放行**。重設後檢查後台的授權紀錄有沒有異常活動 |
| **雲端硬碟加密金鑰** | 產新的（**每個環境一把，不要共用**） | 🔴 **要先寫好「把既有權杖重新加密」的程式再換**，否則客戶既有的雲端硬碟整合會全部失效。或者直接作廢，讓客戶重新授權 |

**清理**可以先做，不用等重設：22 個檔裡的金鑰換成「請查 `.env`」，文件改完重跑產生指令。

**做完怎麼驗**：在 DEV 環境實際跑一次 Google Drive 授權與同步流程。

---

### CM-1632：連 GitHub 沒檢查對方身分

**改動很小，但兩處要一起改。**

```diff
- Github(auth=auth, per_page=100, timeout=10, verify=False)
+ Github(auth=auth, per_page=100, timeout=10)
```

兩處：`infra/github.py:22` 和 `github_issue_adapter.py:43`。**只改一處沒用**，權杖還是會從另一邊漏出去。

**如果真的有「自己架的 GitHub 企業版」需求**（**要先查證有沒有，不要假設**）：改成從設定讀憑證路徑，不要關掉檢查。設定要放哪裡照 CM-1560 的教訓——**客戶會依自家環境調整的走設定頁，部署階段定死的才走環境變數**。

**順手做兩件**：確認 GitLab 那側的預設行為（首腦已確認沒有同款問題，但預設值要查一次）；`gitlab.py` 沒有設連線逾時，補上避免卡死。

**要寫測試**（加密／連線安全屬測試政策的例外）：驗證建出來的連線物件其憑證檢查沒有被關掉。

---

### CM-1634：內部套件倉庫走明文連線

**⚠️ 這條不是改程式碼，是基礎設施要先動，要決策者裁。**

**正解**：
1. **讓 Nexus 改走 HTTPS**（用公司內部憑證也可以，前提是憑證要發到所有開發機與打包機並被信任）
2. 把 26 個專案的網址從 `http://` 改成 `https://`——**這步機械性，但要等 Nexus 那頭先就緒**，否則全部裝不了套件
3. 版本鎖定檔會在下次更新時自動改，**不要手改**

**過渡期的緩解**（HTTPS 就緒前）：
- **把 `poetry update` 當成特權操作**，只在可信任的網段執行
- 更新後**檢查版本鎖定檔的雜湊變動**，出現非預期變化就停下來查

**順便要確認的事**：Nexus 上 `jedi_issue` 0.0.14／0.0.15 兩個舊套件檔**是否已下架**（那兩版把含密碼的設定檔打包進去了，是 CM-1573 沒收乾淨的尾巴。**程式碼查不到，要連 Nexus 確認**）。

---

### CM-1633：附件刪除的檢查只做一半

**改一行。**

```diff
- file_is_deleted = self.file_upload_service.delete_files(file_uids)
+ file_is_deleted = self.file_upload_service.delete_files(list(matched_file_uids))
```

順便看一下 GitLab／GitHub 兩個附件處理檔有沒有同樣問題。

**要寫測試**：傳入「一個合法編號 ＋ 一個別張單的編號」，確認**只有合法那個的檔案被刪掉**。

---

## 三、先修哪三件、為什麼

### ① CM-1629（資料庫密碼）— 唯一的「高」

三個理由疊在一起：**那個密碼是活的**、它指向的 **POC 依規定等同正式環境**、而且有一份文件把連線需要的五樣資訊**湊在相鄰兩行**——不需要任何拼湊就能直接使用。

**這件事有一半是治本。** 249 個檔的來源已經查清楚：九成是查資料庫時打的指令被寫進對話紀錄（112 個檔）與需求文件。**不是有人故意寫的，是「工作過程被完整記錄」的副作用**——而這個機制**已經是第三次了**（`.env.test` 事件、登入憑證金鑰、這次）。所以要先改工作方式，再清檔案。

### ② CM-1630（權限沒檢查）— 唯一「現在就能被員工利用」的

前面幾件都需要特殊條件（要在內網做中間人、要讀得到程式碼倉庫）。**這件只要有一個普通帳號就能做。**

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

### ③ CM-1631（Google 金鑰）— 因為它跟前面兩件性質不同

登入憑證金鑰、資料庫密碼**每套安裝都會各自產生一把新的**，所以外流的只是我們自己環境那把。**但 Google 的應用程式密鑰是在 Google 後台建的一組，所有環境共用同一把**——客戶端不會各自產生。**它的「外洩」比另外兩件更實在。**

### 可以緩的三件

- **CM-1632（GitHub 連線）**：出貨時這個功能預設是關的、權杖是空的，要打到得先有管理員主動開啟，再加上攻擊者剛好在連線路徑上
- **CM-1634（套件倉庫）**：修法是基礎設施，要先有人讓 Nexus 上 HTTPS。過渡期先把 `poetry update` 當特權操作
- **CM-1633（附件刪除）**：目前無法觸發，純預防性修正

---

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

**要分兩件事回答，不能籠統說「檢查過了」。**

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

**六次檢查的驗證投票全部完整跑完**——這是本專案六個檢查案以來第一次做到：

| 次別 | 檢查什麼 | 檔數 | 提出問題 | 投票數 | 漏投 | 中斷 | 耗時 |
|---|---|---:|---:|---:|---:|---:|---:|
| I1 | 跟 GitHub／GitLab 連線的部分 | 25 | 5→3 | 9 | 0 | 0 | 47 分 |
| I2 | 主專案的意見回饋功能 | 31 | 13→10 | **30** | 0 | 0 | 136 分 |
| I3 | 附件上傳下載 | 14 | 4→3 | 9 | 0 | 0 | 91 分 |
| I4 | 對外 API 與插件裝配 | 20 | 4→4 | 12 | 0 | 0 | 95 分 |
| I5 | 業務邏輯層 | **40** | 7→5 | 15 | 0 | 0 | 87 分 |
| I6 | 資料存取層 | 28 | 3→3 | 9 | 0 | 0 | 105 分 |

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

**而且首腦沒有只採信工具**：那條「高」的 249 個檔是自己數出來的（與報告吻合）、六個功能的權限檢查是自己打開檔案看的、六張表沒有客戶隔離是自己查資料庫結構查出來的。

### 第二層：「是不是只有這些」→ ❌ **不可信**

六次都是**快篩模式**，不是徹底審查：沒有全面盤點、沒有威脅建模、沒有廣度掃蕩。

**三個具體的缺口**：

1. **「範圍外」的發現不代表那些地方被檢查過。** 好幾條金鑰外洩是密鑰專項掃描順手撈到的，`docs/`、`scripts/` 這兩個目錄**從來不是檢查目標**。
2. **工具在小範圍的次別幾乎沒有產出。** I3（14 檔）、I4（20 檔）、I6（28 檔）三次，工具在範圍內**都是 0 個新問題**，反覆撈同一個範圍外的舊問題。**這三次真正的產出全是首腦補查的**——包括 I6 那條「六張表完全沒有客戶隔離」。
3. **沒有逐檔閱讀紀錄。** 六次的閱讀帳本都是空的，無法證明每一個檔案都被讀到結論。

**結論：這六件事都是真的；但「這個套件只有這六個問題」這句話不成立。**

---

## 五、這次換到的三條經驗

### ① 一次檢查 40 個檔可行——前提是分開跑

之前的規矩是「一次不超過 30 個檔」，因為 FR-077 那次檢查 42 個檔時，**105 個檢查員全部因額度用盡掛掉、21 張票一張都沒投出來**。

這次 I5 故意放到 40 個檔，結果**15 票全投、零漏投、零中斷、一輪跑完**。

**差別不在檔數，在有沒有和別的工作搶額度。** 這次六次全部分開跑，**一次都沒撞到額度**，對照 FR-079 那次撞了兩次、跑了 14 小時、還踩到工具的併檔陷阱。

**規矩可以改成：「40 個檔可行，前提是一次只跑一次。」這是決策者堅持「分開來跑」直接換到的。**

### ② 工具會被程式碼註解說服，這次抓到現行犯

套件的說明文件自稱「GitLab／GitHub 整合目前產品沒有在用，約佔套件 58%」——**開卡前查證發現這是錯的**（主專案有六個地方實際在呼叫）。

**如果照說明文件跳過，就會漏掉整個第三方整合的部分，而那正是唯一會帶著密碼對外連線的地方。** 六張卡都帶了這個警告。

### ③ 工具說「打不到」可信，但它不會告訴你程式碼是不是寫錯的

I3 那條被三票否決的候選，首腦核對後發現**檢查員的推理三點全對**——但他們漏看了同一個函式裡**第 92 行和第 99 行用了不同變數**（算了過濾結果卻只用一半）。

**所以：卡片點名要查的重點，就算被工具否決，首腦仍要自己看一眼。**

---

## 🔴 六、三件要決策者裁的事

### ① CM-1629 我判「高」不降，要不要換資料庫／Redis 密碼？

依 FR-079 換來的判準查過安裝程式：**每套安裝確實各自產生密碼，客戶端是安全的。**

**但這條不能因此降級**——這把密碼**同時用在 DEV、出貨基線庫、以及我們自己的 STG／POC**。POC 依規定是「等同正式環境、對外 demo」，基線庫是**出貨映像檔的來源**。這與登入憑證金鑰那條「只影響開發機」不同。

**若要換**：DEV 與基線庫可以規劃；**STG／POC 屬環境異動，依鐵律要您當次明示才可以動。** 另建議把 Redis 與資料庫密碼**分成兩組**（目前是同一組）。

### ② Google 的應用程式密鑰要不要現在轉？

這把跟前面幾條不同——**不是每套安裝各自產生的**，是 Google 後台的一組、所有環境共用。轉了要重新發到各環境。

另外加密金鑰若要換，**得先寫好「把既有權杖重新加密」的程式**，否則客戶既有的雲端硬碟整合會全部失效。

### ③ Nexus 上 jedi_issue 0.0.14／0.0.15 兩個舊套件檔下架了沒？

這是 CM-1573 沒收乾淨的尾巴——那兩版把含密碼的設定檔打包進去發布了。**程式碼查不到，要連 Nexus 才能確認。**

---

## 七、六份詳細報告

| 報告 | 檢查什麼 | 範圍內新問題 |
|---|---|---|
| [I1 跟 GitHub／GitLab 連線的部分](scan-I1-third-party-integration.md) | 25 檔 | 2（連線沒檢查身分） |
| [I2 主專案的意見回饋功能](scan-I2-host-wiring.md) | 31 檔 | **5（含唯一的「高」）** |
| [I3 附件上傳下載](scan-I3-attachments.md) | 14 檔 | 0（工具）＋1（首腦補查） |
| [I4 對外 API 與插件裝配](scan-I4-plugin-contract.md) | 20 檔 | 0（兩項查證首腦補做） |
| [I5 業務邏輯層](scan-I5-app-domain.md) | 40 檔 | **1（套件倉庫沒加密，全新）** |
| [I6 資料存取層](scan-I6-persistence.md) | 28 檔 | 0（工具）＋**1（首腦查出六張表沒隔離）** |
