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

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

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


§1

🔴 一句話結論

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


§2

這一棒在檢查什麼

「意見回饋」是使用者在系統裡回報問題的功能。送出後,系統會:

  1. 存進自己的資料庫
  2. 如果管理員有開啟整合,還會拿公司的權杖去 GitHub/GitLab 開一張真的問題單
  3. 使用者上傳的附件也一起送過去

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


§3

找到什麼:10 個問題

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

§4

詳細說明

問題 1:資料庫密碼散在 249 個文件裡(唯一的「高」)

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

# 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 的公告功能(後已修,FR-114.1-5a)。修法可以直接抄 CM-1589。

問題 4、5、6:三把金鑰外洩,但性質不同

三把都是被寫進對話紀錄的(和問題 1 同一個根因),但處置方式不一樣:

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

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


§5

這份結果可信到什麼程度

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

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

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

首腦也沒有只採信工具:

  • 問題 1 的 249 個檔是自己數的(與報告吻合),還多發現「Redis 和資料庫共用同一組密碼」
  • 問題 2、3 的六條功能守門形狀是自己打開檔案看的
  • 249 個檔的來源分類是自己統計的

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

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

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


§6

執行概況(技術細節)

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