這份文件給工程師頭頭掌握現況、跟 PM 會報用。本文是 2026-09-06 晚間快照,實況以
handoff/security-scan-STATE.md為準。2026-10-01 現況:本檔下列「修正中/盤點中/暫緩/待重跑」皆已過時——S6、S7 已重跑完成;CM-1579 已修(方案 A);CM-1559 已修(FR-094 CM-1788,1.21.0 出貨);CM-1582 裁定記錄不修(總表 M18-7,交付首位客戶前再處理);其餘修正卡 1.21.0 出貨(早期卡 09-21 作廢,逐條狀態見
docs/security-report/SUMMARY.md)。
我們用 Claude Code 官方的 claude-security plugin(一種 AI 深度審查工具,會派出多個子代理去讀程式碼、互相驗證彼此的發現)掃了三大塊:
這個工具的價值是補上 Sonar/Semgrep 這類靜態掃描工具看不到的東西——它們抓得到「這段程式碼有 SQL injection」,但抓不到「這支 API 的業務邏輯設計錯了,讓任何人都能接管別人的帳號」。這次找到的最嚴重問題(忘記密碼洩漏憑證)就是後者。
掃到哪:共規劃 11 棒,9 棒完成、S6 換大記憶體模型重跑中、S7 決策者裁定排今晚深夜重跑。 找到多少:累計 24 張問題卡(原 21 張+S5 新回報衍生 2 張+CM-1572③ 衍生 1 張全站上傳限制),其中 1 張最高等級(CRITICAL)、8 張高風險(HIGH)、13 張中風險(MEDIUM)、1 張低風險(LOW)、1 張機械修正。 修到哪:21 張已修完並驗收通過、1 張正在修(CM-1579,決策者已裁定現在做)、1 張正在盤點(CM-1559,修法待盤點結果與 S6/S7 掃完後裁定)、1 張暫緩(CM-1582)。排隊 0,序列組 A/B 全部做完。
| 棒 | 範圍 | 檔數 | 狀態 | 發現數 | 開卡數 |
|---|---|---|---|---|---|
| S1 | 認證與外部身分綁定 | 11 | ✅ 完成已驗 | 8(5 HIGH/3 MEDIUM) | 5 |
| S2 | 授權守門與提權路徑 | 8 | ✅ 完成已驗 | 0 | 首腦另加開 1 張(CM-1559) |
| S3 | 第二因子(MFA)與人機驗證 | 34 | ✅ 完成已驗 | 2(1 HIGH/1 MEDIUM) | 2 |
| S4 | 使用者本體與密碼變更 | 43 | ✅ 完成已驗 | 5(2 CRITICAL/1 HIGH/2 MEDIUM) | 4 |
| S5 | 角色與權限能力 | 49 | ✅ 完成已驗 | 2(2 MEDIUM)→ 開 CM-1585/1586 | 2 |
| S6 | 租戶與組織單位 | 47 | ✅ 重跑完成(原寫「Opus 重跑中」) | 1(MEDIUM) | CM-1588/1589(已修) |
| S7 | 登入態、UI 路由、middleware | 74 | ✅ 已收窄 29 檔重跑完成(原寫「排今晚深夜重跑」) | 首輪跑完但 8/9 條是跟 S1 重複,核心範圍其實沒真的掃到 | 1(Redis TLS,CM-1565) |
S6、S7 為什麼要重跑:這兩棒和 S4、S5 一樣,因為要看的檔案數超過 40 支,掃描工具用的模型「記性」(context 容量)不夠大,讀到一半就被系統判定當機、砍掉重來,最後什麼結果都拿不到(S6),或是讀到別的地方去、把已經抓過的問題當新發現重報一次(S7)。解法是換一顆記性更大的模型,S4、S5 用這個方法都成功了,S6 目前正用這個方法重跑,S7 由決策者裁定排到今晚深夜再跑。
| 棒 | 範圍 | repo | 檔數 | 狀態 | 發現數 | 開卡數 |
|---|---|---|---|---|---|---|
| L1 | License 驗章核心(密碼學層) | jedi-license-runtime | 10 | ✅ 完成已驗 | 1 HIGH(範圍內) | 1(另有 1 張越界處置卡) |
| L2 | License 授權狀態與 API | jedi-license-runtime | 33 | ✅ 完成已驗 | 1 LOW(範圍內)+人工復核多發現 1 HIGH | 2 |
| 棒 | 範圍 | repo | 檔數 | 狀態 | 發現數 | 開卡數 |
|---|---|---|---|---|---|---|
| L3 | 簽發核心與模組 | license_center | 32 | ✅ 完成已驗 | 2 條範圍內(併入 CM-1583)+3 條與 L4 重複(交叉驗證,不重開) | 併卡處理 |
| L4 | 網頁後台 | license_center | 43 | ✅ 完成已驗 | 4 MEDIUM | 2 |
11 棒中 9 棒完成、2 棒待重跑(S6、S7)。
兩個系統分開列表,各一張表、共用同樣欄位。處理狀態欄取代原本的分組,四種值: ✅ 已修完/🔵 修正中/🔍 盤點中(附卡住原因)/⏸ 暫緩(附理由)。
| 卡號 | 嚴重度 | 來源棒 | 問題是什麼 | 怎麼修 | 處理狀態 |
|---|---|---|---|---|---|
| CM-1575 | 🔴 CRITICAL | S4 | 使用者按「忘記密碼」時,系統把重設用的一次性憑證直接放進 API 回應裡吐回去——原本這個憑證應該只出現在寄給本人的信裡。攻擊者只要知道對方的 email,兩步就能改掉任何人(含系統管理員)的密碼 | 成功與失敗都回傳固定的空內容,讓憑證不再外洩,同時讓「信箱存在/不存在」也真正無法區分 | ✅ 已修完(決策者裁不做 nginx 臨時節流,與 CM-1583、LC 端修正一起排上版) |
| CM-1557 | HIGH | S3 | MFA(第二重驗證碼)沒有次數限制,密碼被偷的人還能靠猜驗證碼在時限內硬猜出正確答案 | 加上每人失敗次數限制,5 次即鎖 | ✅ 已修完(email 碼失效/TOTP 15 分鐘窗,沿用既有鎖定時間設定) |
| CM-1560 | HIGH | S1 | LDAP(企業內部帳號系統)登入有三個洞:非 AD 模式下從不驗證密碼、搜尋條件可被操控、連線不驗證憑證真偽 | 三個洞一起補:強制驗密碼、過濾特殊字元、開啟憑證驗證 | ✅ 已修完(TLS 項一度退回重做——原做成 .env 變數,決策者裁不符產品形狀,改成設定頁「驗證伺服器憑證」開關+「自訂 CA 憑證」欄位,對齊 GitLab/Grafana 慣例) |
| CM-1558 | MEDIUM | S3 | 人機驗證(防機器人)功能如果忘記設定金鑰,系統會悄悄改用「永遠通過」的測試金鑰,沒有任何警示 | 改成直接報錯,測試金鑰只在明確標示的開發模式下才給 | ✅ 已修完(沿用既有 DEBUG 旗標,不新開變數) |
| CM-1563 | MEDIUM | S1 | 登入失敗時,「帳號不存在」跟「密碼錯誤」回應不一樣,攻擊者可以拿名單逐一測試篩出哪些帳號真的存在 | 兩種情況統一回一樣的錯誤訊息與回應時間 | ✅ 已修完(帳號不存在時也跑一次假的密碼比對墊時間,堵掉時間差破綻) |
| CM-1565 | MEDIUM | S7 | 連接 Redis(快取/驗證碼暫存)的程式碼把「驗證伺服器憑證是否為真」寫死關閉,就算設定裡開啟加密連線,也擋不住有心人偽裝成 Redis 攔截資料 | 開啟加密連線時強制要求驗證憑證 | ✅ 已修完(新增選填的 CA 憑證路徑設定;DEV 環境沒有 TLS 版 Redis,只跑到單元測試) |
| CM-1562 | HIGH | S1 | 把外部帳號(如 AD)綁到自己帳號的功能,沒有檢查「你是不是本人」,任何登入者都能把自己的外部帳號綁去別人(含管理員)身上,接管對方 | 加上本人身分檢查 | ✅ 已修完(守門放在 LDAP 驗證之前) |
| CM-1561 | HIGH | S1 | Google 登入功能其實從未真正做完,是個空殼,任何人都能透過它偽造身分登入 | 原規劃直接拔除,決策者後來改裁:不拔除、改成關閉 | ✅ 已修完(登入與綁定白名單擋掉 google,程式碼保留並加註解;FE 本來就沒有這個入口) |
| CM-1564 | MEDIUM | S1 | LDAP 設定頁的「測試連線」功能會沿用已存的密碼去測,但連線位址是使用者自己填的,可以把位址改成自己的機器,藉此偷到系統真正的服務帳密 | 改位址時強制要求重新輸入密碼 | ✅ 已修完(沒改位址仍沿用已存密碼,不影響正常使用) |
| CM-1576 | HIGH | S4 | 使用者修改自己個人資料的功能沒有限制能改哪些欄位,理論上可以把自己升級成系統管理員 | 改成只允許修改指定的欄位,角色等敏感欄位不接受自助修改 | ✅ 已修完(runner 先實測攻擊確實成立——一般帳號真的能把自己改成 admin——才動手修) |
| CM-1577 | MEDIUM | S4 | 有兩條可以改密碼的路徑沒有檢查密碼強度規則,可以設成 1234 這種弱密碼 |
補上密碼強度檢查,與忘記密碼共用同一份強度政策 | ✅ 已修完(批次匯入使用者只驗使用者手填的密碼欄位) |
| CM-1578 | MEDIUM | S4 | 匯入使用者名單用 Excel 上傳時,暫存檔案的資料夾名稱直接用上傳者帳號拼出來,理論上帳號名稱帶特殊符號可以讓檔案跑到預期以外的資料夾(實際觸發門檻高,目前環境沒有踩到) | 補上格式驗證、改用固定的內部代號組路徑 | ✅ 已修完(帳號建立時就用格式檢查擋在源頭,上傳目錄再加一層檔名淨化當縱深防護) |
| CM-1585 | MEDIUM | S5 | 角色清單、單筆角色、角色選單這些「讀」的端點完全沒有權限檢查,任何登入者都能拿到整份「誰是管理員、誰有哪些能力」的對照表,還附帶每個角色成員的 email、電話、職稱 | 三支讀端點補上讀取用的權限檢查點,列表回應不再附帶成員名單、只留人數 | ✅ 已修完(查過既有 seed 資料不需要補) |
| CM-1586 | MEDIUM | S5 | 系統在計算「這個人有哪些權限」時,把已停用、已刪除、已過期、甚至別的租戶的角色權限也一起算了進去。管理員停用某個角色,畫面上看不到了,但 API 權限沒收回 | 補上過濾條件,API 端與側邊選單改用同一套「有效角色」判讀邏輯(是否在有效期內、是否同租戶、是否停用/已刪除) | ✅ 已修完(DEV 環境目前 0 筆資料受這條規則影響) |
| CM-1587 | MEDIUM | CM-1572③ 盤點衍生 | 主產品整站沒有請求大小上限,任何登入者可以送一個超大請求把伺服器記憶體塞爆 | 全站請求本體上限設 50MB,環境變數 MAX_REQUEST_BODY_MB 可調,超過回 413 並給明確錯誤訊息 |
✅ 已修完(已補登進凍結 error code 基準表) |
| CM-1559 | MEDIUM(影響面最廣) | 首腦復核加開 | 資料庫的租戶隔離機制在「沒有登入身分」或「租戶編號填 0」時,會直接給予最高權限,等於隔離形同虛設 | 補上更嚴謹的判定,去除這兩個會被利用的漏洞 | ✅ 已修(原寫「盤點中」;FR-094 CM-1788 無身分改 fail-closed,總表 M04-2,1.21.0 出貨) |
| 卡號 | 嚴重度 | 來源棒 | 問題是什麼 | 怎麼修 | 處理狀態 |
|---|---|---|---|---|---|
| CM-1572 | HIGH | L1 | 驗證授權碼簽章前,先把裡面的壓縮內容全部解開,攻擊者送一個「壓縮得極小、解開變超大」的假檔案,能讓後端把記憶體塞爆、整個服務打掛,且不需要登入 | 解壓縮設上限、先檢查長度再解,另外兩個套件裡的同款寫法也一併修掉 | ✅ 已修完(③ 全站上傳大小上限已由 CM-1587 定案 50MB) |
| CM-1580 | LOW | L2 | 兩個客戶同時上傳同一張授權碼時,因為檢查跟寫入不是同一個瞬間完成,理論上兩邊都能通過檢查、都寫進去,造成一張碼被兩個客戶共用 | 在資料庫加一條規則,同一張碼在「目前生效」的狀態下只能存在一筆 | ✅ 已修完(開發環境已建好這條規則,正式與展示環境待排程套用) |
| CM-1581 | 機械修正 | 首腦復核發現 | 出貨打包腳本裡有一道「檢查有沒有帶到正式簽發私鑰」的安全把關,但它檢查的檔案路徑早就搬家了,導致這道把關現在形同虛設 | 改成用程式動態找到正確路徑 | ✅ 已修完 |
| CM-1584 | MEDIUM | L4 | 簽發站登入頁的網址可以帶一個「登入完要導去哪」的參數,程式完全沒檢查就照導。攻擊者可以做一個看起來像真站的假登入頁連結,受害者登入成功後被導去假頁面,密碼因此被騙走 | 檢查這個導轉網址,只允許導回本站內部頁面 | ✅ 已修完 |
| CM-1573 | MEDIUM | L1 越界發現 | 另一個內部套件(jedi-issue)曾經把一份含有 GitHub/GitLab 存取密鑰的設定檔打包發布出去。原始碼雖然已經刪除,但已發布的舊版安裝包裡那份密鑰檔案還在 | 確認密鑰已作廢(決策者裁定跳過另撤銷)、清舊版安裝包、清本地殘留檔案、修改打包規則避免再次發生 | ✅ 已修完 |
| CM-1583 | MEDIUM→HIGH | L3+L4 | 簽發站的「開通授權碼」端點沒有登入保護(設計如此,因為開通前客戶還沒有帳號),但防止亂猜授權碼的機制形同虛設——鎖定條件用錯了 key,導致攻擊者怎麼猜都不會被鎖;而且記錄猜測次數的地方沒有上限、還會把授權碼整串印進系統日誌 | 比照同系統後台登入早就做對的方式:鎖定記錄改存資料庫、按來源 IP 計數、日誌印遮蔽過的內容、拉高授權碼的複雜度(七項合併修,含補發語意改採「產新序號、舊碼作廢」) | ✅ 已修完(DEV 環境 alembic 已套到最新;STG/POC 待排上版) |
| CM-1579 | HIGH(僅影響多租戶/SaaS 模式) | L2 人工發現 | 系統管理員停用某個租戶後,該租戶只要自己上傳一張舊的授權碼,停用狀態就會被自動解除,不需要管理員介入 | 決策者改裁:不等到有 SaaS 客戶才修,現在就做,方案 A(停權語意從照移到租戶層) | ✅ 已修(原寫「修正中」;方案 A,總表 M18-1) |
| CM-1582 | 待正式簽發環境建立才能定嚴重度 | L1 三環境硬編公鑰議題 | 系統目前同時認得開發、測試、展示三種環境的驗證金鑰,理論上用開發環境的私鑰簽的授權碼,正式出貨的產品也會認 | 出包按環境只編對應公鑰、客戶包不含 DEV 鑰 | 📋 裁定記錄不修(原寫「暫緩」;總表 M18-7,交付首位客戶前改成只認正式簽發站公鑰) |
| 嚴重度 | 總張數 | 已修完 | 修正中 | 盤點中 | 暫緩 |
|---|---|---|---|---|---|
| 🔴 CRITICAL | 1 | 1(CM-1575) | 0 | 0 | 0 |
| 🟠 HIGH | 8 | 7(CM-1572/1557/1560/1562/1561/1576/1583) | 1(CM-1579) | 0 | 0 |
| 🟡 MEDIUM | 13 | 11(CM-1573/1584/1558/1563/1565/1564/1577/1578/1585/1586/1587) | 0 | 1(CM-1559) | 1(CM-1582) |
| 🟢 LOW | 1 | 1(CM-1580) | 0 | 0 | 0 |
| 機械修正 | 1 | 1(CM-1581) | 0 | 0 | 0 |
| 合計 | 24 | 21 | 1 | 1 | 1 |
CM-1583 卡片嚴重度為 MEDIUM→HIGH(log 洩憑證追加後升級),此表計入 HIGH 列。 現況(2026-10-01):表中「修正中/盤點中」兩欄的 CM-1579、CM-1559 均已修;「暫緩」的 CM-1582 裁定記錄不修。
原本三條排隊規則(先修 FR-076 再動 jedi-iam/改同一支程式碼的卡片依序做/動核心連線邏輯的卡等掃描完),前兩條已隨修正完成解除——序列組 A(LDAP 四張)與序列組 B(使用者資料三張)全部做完。
目前只剩 CM-1559 受第三條限制。而且盤點後發現它不是單一修正,是「全站資料庫連線身分判斷」的重構等級改動,已改為等決策者裁方向(見七)。
projects 等三張表有 policy 但 RLS 未啟用(打開會直接壞)/users.tenant_id 可為 NULL(NULL 會靜默變成最高權限)修正都在各自 repo 完成,尚未推上 STG/POC:
PYTHONPATH=src → 重啟),2 支 alembic migration 要在 STG/POC 套用待裁(2 項,09-06 當時;現況:CM-1559 已修〔FR-094 CM-1788〕,jedi-iam 修正已隨 1.21.0 出貨)
X-Tenant-ID: 0 那條攻擊路徑已被 FR-069.16 擋死,本卡降為「縱深防禦少一層」;但直接翻成 fail-closed 會炸全站認證(JWT 解析、登入、忘記密碼、agent 心跳、Drive webhook 都靠無 context 讀 RLS 表才跑得動),且壞法是靜默不報錯。三個選項:
not tenant_id 那一行(爆炸面只有 signed_token 一處)+ fail-closed 分支加 WARNING log,第一刀另開 arc 綁 SaaS 前已裁(不重問)
本文是 2026-09-06 晚間快照,實況以 handoff/security-scan-STATE.md 為準。