檢查日期 2026-09-09|耗時 2 小時 16 分|對應卡片 CM-1614
六棒裡最嚴重的一棒。 找到兩件事:資料庫密碼被寫進 249 個文件裡(其中一份還附上完整的連線指令),以及意見回饋的六個功能有五個沒有檢查權限——任何員工都能刪掉別人的回饋。
「意見回饋」是使用者在系統裡回報問題的功能。送出後,系統會:
這一棒檢查主專案這邊的 31 個檔案——誰能用這些功能、輸入的東西怎麼流出去。
| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 修正卡 |
|---|---|---|---|---|---|---|
| 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:88feedback_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 作廢) |
白話:資料庫的管理員密碼,被寫進了 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 和資料庫用的是同一組密碼。
全部是文件,沒有一支是會跑的程式碼:
| 類型 | 數量 | 是什麼 |
|---|---|---|
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='<密碼>...
不是有人故意把密碼寫進文件,是「工作過程被完整記錄下來」的副作用——每次要查資料庫打的指令,連同密碼被寫進對話紀錄,對話紀錄再依規定進版控;實作計畫和交接文件也照抄同樣的指令當成「怎麼跑」的說明。
.env.test 事件——四把 API 金鑰進版控,已撤銷重發所以只清檔案是治標。 修正卡 CM-1629 要求先做治本:改用 ~/.pgpass(psql 原生支援,指令裡就不必出現密碼)、對話紀錄歸檔前先遮罩,再清檔案。否則下次還會再長出來。
怎麼修:詳見 CM-1629。要注意的是換密碼這件事——DEV 和基線庫可以規劃,但 STG/POC 屬於環境異動,依專案鐵律要決策者當次明確指示才可以動。
白話:意見回饋有六個功能。只有「匯出」那個會檢查你有沒有權限,其他五個任何登入者都能用。 而且修改和刪除從來不檢查那筆資料是不是你的。
首腦逐條打開檔案核對:
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。
三把都是被寫進對話紀錄的(和問題 1 同一個根因),但處置方式不一樣:
| 金鑰 | 每套安裝各自產生嗎? | 影響 | 怎麼辦 |
|---|---|---|---|
| Google 應用程式密鑰(問題 4) | ❌ 不是。是 Google 後台的一組,所有環境共用 | 外洩就是真的外洩 | 要到 Google 後台重設再發到各環境 |
| 雲端硬碟加密金鑰(問題 5) | ❌ 不是,是設定檔裡的固定值 | 配合問題 1 可解密客戶權杖 | 換新金鑰,但要先寫好「把既有權杖重新加密」的程式,否則客戶的整合會全部失效 |
| 登入憑證簽章金鑰(問題 6) | ✅ 是。每套安裝跑 openssl rand 各自產生 |
外流的只是我們開發機那把 | 降為清理工作,併入 CM-1607 |
這個對比很重要:同樣是「金鑰外洩」,問題 6 只影響我們自己的開發機,問題 4 卻影響所有客戶。判斷嚴重度時,第一個要問的是「客戶用的是不是同一把」。
這是六棒裡唯一拿到完整 verified 標記的一棒。 十條發現每一條都經過三個獨立檢查員投票、三票全過,30 票全數投出、沒有漏投、沒有中斷。
而且檢查員主動把四條降級了(原本研究員判「高」的被降成「中」)——這代表他們真的在做對抗性判斷,不是橡皮圖章。
首腦也沒有只採信工具:
第一層:這是快篩模式,沒有全面盤點、沒有威脅建模。
第二層(這棒特有的):五條「範圍外」的發現,不代表那些地方被檢查過。 那五條是「密鑰專項掃描」順手撈到的,而 docs/、scripts/ 這兩個目錄從來不是這次的檢查目標。也就是說,那兩處等於沒檢查過。
| 項目 | 數字 |
|---|---|
| 檢查範圍 | 31 個檔案 |
| 派出/回報的研究員 | 2 / 2 |
| 候選問題 → 去除重複 | 13 → 10 |
| 投票數 | 30(10 條 × 3 個檢查員) |
| 沒投到票的 | 0 |
| 投票中斷的 | 0 |
| 被檢查員降低嚴重度的 | 4 |
| 耗時 | 2 小時 16 分 |