檢查日期 2026-09-25|對應卡片 CM-2166(母卡 CM-2152)|檢查範圍 10 個檔案 1,844 行|工具 run ID
wf_5b4ba820-2db
這十支檔案都有把零件接上,但有兩處接上的守門問錯了問題。 這兩處都不是全新的洞,是舊洞裡先前沒有人說清楚的一角:
ai-dashboard.read,選單也照它決定要不要顯示。但生成端點本身只檢查「有登入」和「公司有買這個模組」。所以角色沒有這個權限點的人(DEV 上的「稽核人員」「IT執行人員」就是)選單看不到,直接打網址照樣能用,每用一次公司就付兩次 AI 費用。這正是 U4 那條教訓:「問你有沒有買」不等於「問這是不是你的」。工具這一棒有 4 條候選,三人面板 3:0 通過 3 條、3:0 否決 1 條。通過的 3 條全是舊帳(第 34/37/40 項、第 11 項、第 36 項),不另計。
卡片要我回答的三件事:
| 卡片問的 | 答案 |
|---|---|
| 交給套件的守門零件,問的問題對不對 | 九支裡七支問對了。問錯的兩處是上面兩條:AI 儀表板用「公司有沒有買」代替「人能不能用」;檔案端點用「有沒有登入或有沒有通行證」代替「這是不是你的」(後者是第 34 項舊帳) |
| 套件拿到空值時是拒絕還是放行 | 登入、權限點、商務授權這類「門」一律拒絕掛載,六支套件都寫死了。會放行的只有兩種:①公告拿不到「你是哪個部門」時看全部(第 1 項舊帳);②修正線的檔案套件拿不到「歸屬名冊」時只記一行警告、照樣掛上(主專案修正線已經接上名冊,現況不會觸發) |
| DI 模組清單的載入順序,會不會讓守門在被用到時還沒接上 | 不會。所有守門零件都是「被呼叫的那一刻」才去找 DI 容器,而容器在迴圈之前就放好了。di_modules.py 也沒有漏列任何用到注入的模組(逐支比對過) |
系統啟動時,主專案要把 21 支外部套件一支一支「接上」:交給它們資料服務、登入檢查、權限檢查、商務授權檢查等零件。這十支檔案就是這份接線表:
| 檔案 | 行數 | 接的是什麼 |
|---|---|---|
core/plugins/identity.py |
679 | 登入、帳號、角色、租戶(jedi-iam)。全產品的守門零件從這裡交出去 |
config/di_modules.py |
225 | 決定哪些程式要「自動拿到零件」。漏列一支,那支的注入全部落空而且不報錯 |
core/plugins/bulletin.py |
220 | 公告 |
core/plugins/file_upload.py |
145 | 檔案上傳、下載、預覽、刪除 |
core/plugins/ai_bot.py |
135 | AI 聊天小幫手 |
core/plugins/ai_dashboard.py |
98 | AI 動態儀表板 |
core/plugins/integrity.py |
93 | 防竄改(出貨程式有沒有被改過) |
core/plugins/__init__.py |
83 | 21 支套件的清單與掛載順序 |
core/plugins/flow_engine.py |
83 | 稽核流程引擎(只當函式庫用,不掛網址) |
core/plugins/notification.py |
83 | 寄信、測試信 |
DI 比對腳本(di-wiring-audit.md)已經確認這十支「零缺接」,所以這一棒不找「沒接」,專找「接錯」。
| # | 嚴重度 | 一句話 | 來源 | 算不算新的 |
|---|---|---|---|---|
| U13a-1 | 🟡 中 | 共用原廠檔案可被任何公司刪掉實體檔,全體客戶一起壞 | 工具 F1 的一段(面板 3:0)+runner 開檔與 DEV 唯讀查證 | 淨新增(待首腦判是否併第 37 項) |
| U13a-2 | 🟢 低 | AI 儀表板生成端點不檢查 ai-dashboard.read |
工具 F2 的一句(面板 3:0 提到)+runner 手做實測 | 淨新增 |
| 工具 F1 其餘 | 中 | 檔案下載/預覽/刪除不問「是不是你的」 | 面板 3:0 | 舊帳:第 34/37/40 項,不另計 |
| 工具 F2 其餘 | 中 | AI 儀表板 26 支查詢沒有逐支權限檢查 | 面板 3:0 | 舊帳:第 11 項(修正分支 CM-2038 已補),不另計 |
| 工具 F3 | 中 | 上傳網頁檔在預覽時被當程式執行(儲存型 XSS) | 面板 3:0 | 舊帳:第 36 項(總表記高),不另計 |
| 工具候選 C4 | — | 測試信把存好的 SMTP 密碼送到呼叫者指定的主機 | 面板 0:3 否決 | 舊帳:CM-1605(FR-078 當時記 HIGH),見下方「面板與前次判定不同」 |
現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。
- U13a-1(共用原廠檔被刪實體檔)=總表第 217 項,✅ 已修(CM-2228,套件
5ec9b617:原廠共用檔禁刪、刪除順序改先刪紀錄再刪檔,1.21.0 出貨)。- U13a-2(AI 儀表板生成缺能力點)=總表第 218 項,✅ 已修(CM-2220,commit
80dedfbb5/套件fb271590,1.21.0 出貨)。- 候選 C4=CM-1605(測試信送存好的 SMTP 密碼):🚫 裁定不修(權限面由客戶自行決定);程式面已加強(位址或連接埠不同就不沿用存著的密碼,CM-2111,M16 第 1 條)。
app/upload_file/service/managed_file_upload_service.py:385(delete_file)。選儲存空間的那段在 :177-182(_adapter_for_uid)。接線落點在 core/plugins/file_upload.py:92(沒交歸屬名冊)。實際刪檔的是套件 jedi_file_upload/infra/adapter/minio/minio_adapter.py:134(先刪實體檔)→ :137(再刪紀錄),版本是出貨釘住的 1.2.0。storage_scope='system',例如法規框架的 PDF),所有客戶都讀得到,DEV 上有 18 支。資料庫的規則是:這類紀錄每家公司都能讀,但只有平台管理員能刪。刪檔 API 的流程是:先用檔案編號讀出紀錄(讀得到,因為共用)→ 到原廠儲存空間把實體檔刪掉 → 再刪資料庫紀錄。第三步被資料庫規則擋下,但它擋的方式是「當作沒有這筆」、不報錯,所以 API 照樣回成功。delete_file 對 storage_scope='system' 的檔案,非平台管理員一律拒絕,而且要在刪實體檔之前拒絕;②刪除順序改成「先刪紀錄、刪成功(影響筆數 = 1)才刪實體檔」,這樣任何被資料庫擋下的刪除都不會留下半套結果。建議跟第 37 項同一張卡,驗收時加一條「一般帳號刪原廠檔要失敗、實體檔要還在」。BEGIN READ ONLY … ROLLBACK)。另外,修正分支 fix/security-b1 已經接上歸屬名冊(core/plugins/file_upload.py:355-356),但原廠檔的「刪除」會不會被名冊擋下,我沒有驗。修的人請在修正線上補測這一條。jedi_ai_dashboard/api/routes/ai_dashboard_route.py:99(生成端點只掛 @require_license)。主專案接線 core/plugins/ai_dashboard.py:52-61(交出去的只有 auth_required 與 license_guard,沒有權限點守門)。ai-dashboard.read,選單也靠它決定顯不顯示。但生成端點只過第一道門。這不是搬家時弄丟的:搬進套件前,主專案自己的端點也只掛了登入+商務授權(git 查過 1b27db20e 之前的版本)。修正分支 fix/security-b1 的套件端點也一樣沒有。ai-dashboard.read,其餘角色都有。register() 掛上端點,模擬「公司有買、角色沒有權限點」的使用者送出請求。商務授權檢查被呼叫了(參數 ai-dashboard),請求接著直接進到生成服務,途中沒有任何權限點檢查。ai-dashboard,角色沒有 ai-dashboard.read。AiDashboardAdapters 開一個 capability_required 欄位,主專案交 require_capability,生成端點加上 ai-dashboard.read。跟第 11 項同一個套件、同一個修正線,建議併在 CM-2038 的後續一起做。| 卡片點名 | 答案 |
|---|---|
🔴 identity.py:每個需要權限檢查的服務,這裡有沒有把守門零件接上 |
接上了,也問對了。交給套件三道門:登入(jwt_required())、權限點(require_capability,查「現在有效的角色指派」,停用、過期、跨租戶的角色都不算)、換發權杖(jwt_required(refresh=True))。三道缺一個,套件會拒絕掛載(jedi_iam/plugin/contract.py:42)。其他幾件:人機驗證可以用 TURNSTILE_ENABLED 關掉(落地版內網連不到 Cloudflare,屬部署政策);原廠帳號的密碼永不過期豁免,在判斷不出來時走一般政策(不放寬,:252-269);新租戶開通時「無照=不扣能力點」是刻意的放寬,真正的執法在每次請求(:288-316,理由寫在註解裡,與第 181 項的總開關同一條軸)。綁定 LDAP 帳號那支的包裝(:496-516)照單轉交 user_uid、不核是不是本人,這是 CM-1562 舊帳,不另計 |
di_modules.py:有沒有漏列某個模組 |
沒有漏列。用程式比對全 repo:用到 @inject/Provide[...] 的正式程式碼全部在清單內,清單外的命中都是註解、文件產生器或 common/authz 的說明文字。清單裡的 77 個模組都真的存在。另外核了兩件:①21 支套件的網址全部不用 @inject(改成請求當下從 app.extensions 取),所以套件不需要進清單;②本機的靜態清單 config/di_modules_static.py 比動態掃描少了兩支、多了一支,但這是本機舊產物:該檔不入版控,出貨建置時先重產再用 --check 斷言零差異(scripts/build/build_release.sh:308-311),不會帶著舊清單出貨 |
bulletin.py/file_upload.py:09-12 搬家後的新位置有沒有接對 |
公告接對了:登入、權限點、登入帳號三件缺一個就拒絕掛載(套件 assembly.py:69-82),讀寫三個端點掛的權限點與 config 一致。「拿不到部門就看全部」是第 1 項舊帳。檔案接的東西都在,但守門問錯了問題:file_access_guard=signed_token_or_jwt 問的是「有沒有登入,或有沒有通行證」,不是「這是不是你的」,這是第 34 項舊帳;新找到的一角是 U13a-1 |
ai_bot.py/ai_dashboard.py:之前沒被任何一棒碰過 |
AI 小幫手:只有登入一道門(auth_required 是必填欄位,漏了建構就炸)。套件宣告的權限點是空的,產品授權表裡也沒有這個模組,所以「只驗登入」符合設計。金鑰每次對話才依「目前這家公司」解出來,公司沒設就用原廠那把(app/system_config/service/ai_provider_key_resolver.py:145-151)。這是刻意的後備順序,金鑰本身不會回給使用者。次數上限是 CM-1638 舊帳。AI 儀表板:見 U13a-2 |
integrity.py:雜湊鏈驗證的零件有沒有接上 |
接上了,而且啟動閘門和後續抽查用的是同一顆 context(main.py:123 建一次,傳進 create_app(),integrity.py:84 原樣交給套件)。沒傳進來時(測試或腳本)才自己補建。竄改事件寫資料庫那支沒註冊時是「放行、留缺口」,這是套件刻意的設計,已記在第 14 項(兩道鎖只做一道)。附帶觀察:integrity_tamper_events 表沒開資料庫隔離,一般應用帳號 cm_app 可以刪它(DEV 唯讀查權限)。這張表存的是整台機器的事件、不分公司,不另計,已補在第 14 項的脈絡裡提給首腦 |
| 帶著 DI 腳本比對結果去看:標紅的缺漏逐一回答 | 標紅 3 處都在 di_containers/(U13b 範圍)。我先開檔答了,給 U13b 參考:①TenantProvisioningService.default_role_name 不成立:預設值是 "System Manager" 這個角色名字串,不是守門零件,沒給就用預設名稱建預設角色;②③MetadataCloneService/OscalIoService.role_repo 不成立:這個 role 是 OSCAL 文件裡的「職責角色」資料表,不是權限角色,沒給時建構子自己補一支真的資料存取物件(role_repo or RoleRepoImpl(),套件 metadata_clone_service.py:54、oscal_io_service.py:199),不是空的 |
這張表回答派工說的「U1 核過 DataAPIService 缺 checker 時是拒絕,其他 plugin 逐支核」:
| 套件 | 必填的門(缺了就拒絕掛載) | 選填、缺了會怎樣 |
|---|---|---|
| jedi-iam | 登入、權限點、換發權杖 | 通知信沒接只產碼不寄;原廠帳號判斷不出來走一般政策(不放寬) |
| jedi-bulletin | 登入、權限點、登入帳號 | 拿不到部門 → 看全部(放寬,第 1 項) |
| jedi-file-upload | 登入、檔案守門、通行證簽發 | 修正線:沒接歸屬名冊 → 只記警告、照樣掛上(放寬);出貨的 1.2.0 根本沒有這個欄位 |
| jedi-notification | 登入、權限點 | — |
| jedi-ai-dashboard | 登入、商務授權 | 修正線:沒接逐支權限檢查 → 全部拒絕;1.2.0 沒有這個欄位;端點本身沒有權限點這道門(U13a-2) |
| jedi-ai-bot | 登入(必填欄位,漏了建構時就炸) | — |
| jedi-integrity | 三張驗證零件 | 資料庫落點沒接 → 留缺口、不擋關機(第 14 項) |
| jedi-flow-engine | 無 | 兩張 port 目前沒人讀 |
core/plugins/__init__.py 的紅線只有兩條:identity 要早於 file_upload,api_log 要晚於 logging 設定。兩條都照做。require_capability 在請求當下才從 app.extensions['di_container'] 找權限查詢服務(jedi_iam/authz/capability.py:69-77);AI 儀表板的商務授權是請求當下才 import;公告、通知的守門是請求當下才去 runtime() 拿。app.extensions['di_container'] 在掛載迴圈之前就放好了(core/app_factory.py:313)。工具候選 C4 就是 CM-1605(FR-078 N2 F1,當時面板判 HIGH、決策者開過卡):測試信 API 在「沒改密碼」時,把資料庫裡的真 SMTP 密碼配上呼叫者送來的主機位址去登入。這一棒三位檢查員 0:3 否決,理由一致:能打這支的人必須持有平台專屬的 smtp-config.update,他本來就能直接改掉 SMTP 設定,所以這支沒有給他新的能力。我沒有再查「存設定」那條路是否也會沿用舊密碼,所以無法判斷哪一次對。兩次面板結論相反,請首腦決定 CM-1605 要維持還是降級。
第一層:工具正式報告(經三人面板驗證)
verified(stamp CLAUDE-SECURITY-REVISION-256994ee2a34-dirty.json)di_containers/。這符合 DI 專掃的性質:接線檔本身幾乎沒有邏輯,問題都在「交出去的東西」。第二層:runner 自行開檔核對(未經三人面板投票)
plugin/contract.py 與 assembly.py(主線與修正線兩版);jedi_iam/authz/capability.py、middleware/context.py;common/authz/license.py;common/iam_ports.py;app/notification/service/*;app/system_config/service/ai_provider_key_resolver.py;core/app_factory.py 的接線段;main.py 的防竄改 context;file-upload 1.2.0 wheel 的刪除順序;upload_files 的 migration 004;FE 的 AI 儀表板 API 常數。register() 掛端點、Flask 測試客戶端送請求,確認權限點那道門不存在。di_modules.py 用程式比對全 repo 的注入使用點。ai-dashboard.read 對照(16:51)、upload_files 依儲存範圍計數(18:15)、integrity_tamper_events 隔離與權限。| 項目 | 值 |
|---|---|
| 掃描目標 | BE repo compliance-manager-be,branch feature/review |
| 版本 | 256994ee2(工作區有平行線未 commit 改動,範圍 10 支檔案本身無改動) |
| 工具 | Claude Code 官方 claude-security plugin 0.11.0 |
| 參數 | mode scan/effort low/focus attack-surface/scope 10 檔 |
| run ID | wf_5b4ba820-2db |
| 報告目錄 | CLAUDE-SECURITY-20260925-083957/(不入版控) |
| 耗時 | 約 92 分鐘(5,507 秒;研究員通讀約 64 分鐘,面板約 25 分鐘) |
| 研究員 | 2 派出/2 交回,failed 0 |
| 候選/面板票 | 4/12(3:0 通過 3、0:3 否決 1) |
| 驗證章 | verified |
| runner 淨新增 | 2 條(中 1、低 1),皆未經面板 |