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

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

檢查期間 2026-09-09~10|分六次檢查完成|首腦已驗收


🔴 結論:要修 6 件事,其中 1 件緊急

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

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

現況:全數已修(1.21.0 出貨)——當時(09-10)只是建卡歸檔,09-21 起改照內化版問題總表(docs/security-report/M23-issue.md)派工,早期六張卡作廢;對照見 README 修正卡段。


§1

一、要修什麼(六件事)

卡號 這是什麼問題 出事會怎樣 緊急度 建議順序
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 一起清理,不另開卡。


§2

二、預計怎麼修

🔴 CM-1629:資料庫密碼外洩(優先做)

現況:已修(CM-2049,commit 5d4c14221,1.21.0 出貨;原卡 CM-1629 作廢)。249 檔殘留字串已清、再寫進去的路已堵;密碼換發決策者裁排在正式環境上版前。

分三步,順序不能顛倒。

第一步:先堵住再發生的路(不做這步,清了還會再長出來)

做什麼 為什麼
改用 ~/.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:意見回饋沒檢查權限

現況:已修(FR-114.1-7/CM-2030,1.21.0 出貨;原卡 CM-1630 作廢)。

分兩層改,兩層都要。

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

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

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

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

@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 金鑰外洩

現況:已修(CM-2051,1.21.0 出貨;原卡 CM-1631 作廢)。15 檔殘留已清;金鑰重發與既有通行證重新加密另案處理。

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

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

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

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


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

現況:已修(CM-2066,1.21.0 出貨;原卡 CM-1632 作廢)。兩處 verify=False 已拿掉。

改動很小,但兩處要一起改。

- 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.21.0 出貨;原卡 CM-1634 作廢)。連線維持 http,決策者裁倉庫與打包機都在內網。

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

正解:

  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:附件刪除的檢查只做一半

現況:已修(CM-2066,1.21.0 出貨;原卡 CM-1633 作廢)。

改一行。

- 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 兩個附件處理檔有沒有同樣問題。

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


§3

三、先修哪三件、為什麼

① CM-1629(資料庫密碼)— 唯一的「高」

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

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

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

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

而且它是本專案第三次犯同樣的錯(CM-1585、CM-1589 已修完,FR-079 的公告功能後來也已修,FR-114.1-5a),修法可以直接抄。

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

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

可以緩的三件

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

§4

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

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

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

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

次別 檢查什麼 檔數 提出問題 投票數 漏投 中斷 耗時
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. 沒有逐檔閱讀紀錄。 六次的閱讀帳本都是空的,無法證明每一個檔案都被讀到結論。

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


§5

五、這次換到的三條經驗

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

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

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

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

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

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

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

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

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

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

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


§6

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

① CM-1629 我判「高」不降,要不要換資料庫/Redis 密碼?

現況:殘留字串已清(CM-2049,1.21.0 出貨);密碼換發決策者裁排在正式環境上版前。

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

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

若要換:DEV 與基線庫可以規劃;STG/POC 屬環境異動,依鐵律要您當次明示才可以動。 另建議把 Redis 與資料庫密碼分成兩組(目前是同一組)。

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

現況:檔案殘留已清(CM-2051,1.21.0 出貨);Google 後台重發與既有通行證重新加密另案處理。

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

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

③ Nexus 上 jedi_issue 0.0.14/0.0.15 兩個舊套件檔下架了沒?

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


§7

七、六份詳細報告

報告 檢查什麼 範圍內新問題
I1 跟 GitHub/GitLab 連線的部分 25 檔 2(連線沒檢查身分)
I2 主專案的意見回饋功能 31 檔 5(含唯一的「高」)
I3 附件上傳下載 14 檔 0(工具)+1(首腦補查)
I4 對外 API 與插件裝配 20 檔 0(兩項查證首腦補做)
I5 業務邏輯層 40 檔 1(套件倉庫沒加密,全新)
I6 資料存取層 28 檔 0(工具)+1(首腦查出六張表沒隔離)