U13a 檢查結果:套件接線九支+DI 模組清單(找「接錯」不是找「沒接」)

U13a 檢查結果:套件接線九支+DI 模組清單

檢查日期 2026-09-25|對應卡片 CM-2166(母卡 CM-2152)|檢查範圍 10 個檔案 1,844 行|工具 run ID wf_5b4ba820-2db

§1

🔴 一句話結論

這十支檔案都有把零件接上,但有兩處接上的守門問錯了問題。 這兩處都不是全新的洞,是舊洞裡先前沒有人說清楚的一角:

  1. AI 儀表板只問「這家公司有沒有買」,不問「這個人能不能用」(淨新增,低)。產品替 AI 儀表板定了一個權限點 ai-dashboard.read,選單也照它決定要不要顯示。但生成端點本身只檢查「有登入」和「公司有買這個模組」。所以角色沒有這個權限點的人(DEV 上的「稽核人員」「IT執行人員」就是)選單看不到,直接打網址照樣能用,每用一次公司就付兩次 AI 費用。這正是 U4 那條教訓:「問你有沒有買」不等於「問這是不是你的」。
  2. 共用的原廠檔案(例如法規框架 PDF),任何一家公司的登入者都能把實體檔刪掉,全體客戶一起壞(淨新增,中,建議首腦判斷是否併入第 37 項)。資料庫那道牆(第 34/47 項的修正)讓大家以為跨公司刪除已經擋住了。實際上刪除的順序是「先刪儲存空間裡的實體檔、再刪資料庫紀錄」,資料庫只擋住了第二步,而且擋的時候不會報錯。結果是紀錄還在、檔案沒了,API 還回「成功」。

工具這一棒有 4 條候選,三人面板 3:0 通過 3 條、3:0 否決 1 條。通過的 3 條全是舊帳(第 34/37/40 項、第 11 項、第 36 項),不另計。

卡片要我回答的三件事:

卡片問的 答案
交給套件的守門零件,問的問題對不對 九支裡七支問對了。問錯的兩處是上面兩條:AI 儀表板用「公司有沒有買」代替「人能不能用」;檔案端點用「有沒有登入或有沒有通行證」代替「這是不是你的」(後者是第 34 項舊帳)
套件拿到空值時是拒絕還是放行 登入、權限點、商務授權這類「門」一律拒絕掛載,六支套件都寫死了。會放行的只有兩種:①公告拿不到「你是哪個部門」時看全部(第 1 項舊帳);②修正線的檔案套件拿不到「歸屬名冊」時只記一行警告、照樣掛上(主專案修正線已經接上名冊,現況不會觸發)
DI 模組清單的載入順序,會不會讓守門在被用到時還沒接上 不會。所有守門零件都是「被呼叫的那一刻」才去找 DI 容器,而容器在迴圈之前就放好了。di_modules.py 也沒有漏列任何用到注入的模組(逐支比對過)
§2

這一棒在檢查什麼

系統啟動時,主專案要把 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)已經確認這十支「零缺接」,所以這一棒不找「沒接」,專找「接錯」。

§3

掃到什麼(總覽)

# 嚴重度 一句話 來源 算不算新的
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 條)。

U13a-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 照樣回成功。
  • 出事會怎樣:這份法規框架 PDF 對所有客戶都打不開了(紀錄還在、實體檔不見)。被刪的人不會收到任何通知,要等有人開檔時才發現。
  • 觸發前提:任何一家公司的任何登入帳號,加上知道一支原廠檔案的編號(原廠檔案在資源庫頁面上每家都看得到)。
  • 為什麼說是新的:第 37 項當初寫的是「別家客戶的證據會被刪」。後來第 34/47 項補了資料庫那道牆,看起來像是跨公司刪除已經擋住。這條說明對原廠共用檔案,牆只擋住了紀錄,實體檔照樣被刪。修第 37 項時如果只驗「刪別家客戶的檔會失敗」,這條不會被測到。
  • 建議修法:①delete_file 對 storage_scope='system' 的檔案,非平台管理員一律拒絕,而且要在刪實體檔之前拒絕;②刪除順序改成「先刪紀錄、刪成功(影響筆數 = 1)才刪實體檔」,這樣任何被資料庫擋下的刪除都不會留下半套結果。建議跟第 37 項同一張卡,驗收時加一條「一般帳號刪原廠檔要失敗、實體檔要還在」。
  • 打折處:沒有對正在運行的服務實刪(會真的弄壞 DEV 的共用檔)。證據是開檔讀 1.2.0 wheel 的刪除順序+讀 migration 的刪除規則+DEV 唯讀查到 18 支原廠檔(2026-09-25 18:15 +08,BEGIN READ ONLY … ROLLBACK)。另外,修正分支 fix/security-b1 已經接上歸屬名冊(core/plugins/file_upload.py:355-356),但原廠檔的「刪除」會不會被名冊擋下,我沒有驗。修的人請在修正線上補測這一條。

U13a-2 — AI 儀表板只問「公司有沒有買」,不問「這個人能不能用」

  • 嚴重度:🟢 低
  • 位置(該補檢查的地方):套件 jedi_ai_dashboard/api/routes/ai_dashboard_route.py:99(生成端點只掛 @require_license)。主專案接線 core/plugins/ai_dashboard.py:52-61(交出去的只有 auth_required 與 license_guard,沒有權限點守門)。
  • 白話說明:產品替每個功能訂兩道門:「公司買了這個模組沒」(商務授權)和「這個人的角色有沒有這個功能的權限點」。AI 儀表板在套件裡宣告了權限點 ai-dashboard.read,選單也靠它決定顯不顯示。但生成端點只過第一道門。這不是搬家時弄丟的:搬進套件前,主專案自己的端點也只掛了登入+商務授權(git 查過 1b27db20e 之前的版本)。修正分支 fix/security-b1 的套件端點也一樣沒有。
  • DEV 實況(2026-09-25 16:51 +08 唯讀):公司 102 的「稽核人員」「IT執行人員」兩個角色沒有 ai-dashboard.read,其餘角色都有。
  • 實測:本機用套件的 register() 掛上端點,模擬「公司有買、角色沒有權限點」的使用者送出請求。商務授權檢查被呼叫了(參數 ai-dashboard),請求接著直接進到生成服務,途中沒有任何權限點檢查。
  • 出事會怎樣:①管理員以為把 AI 儀表板從某個角色拿掉了,實際上那個角色照樣能用,只是看不到選單;②每用一次公司就付兩次 AI 費用,而這個功能本來就沒有次數上限(第 11 項已記)。修正分支補了 26 支查詢各自的權限白名單(CM-2038)以後,這條不會再多看到資料,剩下的是「權限設定不生效」和費用。
  • 觸發前提:一個登入帳號,所屬公司有買 ai-dashboard,角色沒有 ai-dashboard.read。
  • 建議修法:在 AiDashboardAdapters 開一個 capability_required 欄位,主專案交 require_capability,生成端點加上 ai-dashboard.read。跟第 11 項同一個套件、同一個修正線,建議併在 CM-2038 的後續一起做。
§4

卡片「重點看什麼」逐條回答

卡片點名 答案
🔴 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),不是空的
§5

「套件拿到空值時」逐支對照

這張表回答派工說的「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 目前沒人讀
§6

載入順序查了什麼

  • 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)。
  • 掛載當下就建實例的只有三處:公告的部門名冊與暱稱搜尋、身分的通知服務、全站唯一的身分名冊。它們內部的資料存取都是「用到才開連線」,不會把第一個請求的連線綁死,也都不是守門零件。
§7

面板與前次判定不同(請首腦裁)

工具候選 C4 就是 CM-1605(FR-078 N2 F1,當時面板判 HIGH、決策者開過卡):測試信 API 在「沒改密碼」時,把資料庫裡的真 SMTP 密碼配上呼叫者送來的主機位址去登入。這一棒三位檢查員 0:3 否決,理由一致:能打這支的人必須持有平台專屬的 smtp-config.update,他本來就能直接改掉 SMTP 設定,所以這支沒有給他新的能力。我沒有再查「存設定」那條路是否也會沿用舊密碼,所以無法判斷哪一次對。兩次面板結論相反,請首腦決定 CM-1605 要維持還是降級。

§8

這份結果可信到什麼程度

第一層:工具正式報告(經三人面板驗證)

  • 驗證章:verified(stamp CLAUDE-SECURITY-REVISION-256994ee2a34-dirty.json)
  • 候選 4 條、面板 12 票零漏投:3 條 3:0 通過、1 條 0:3 否決。研究員派出 2 位、交回 2 位(通讀全範圍 1 位、找寫死密碼專項 1 位,後者零發現),失敗 0,續跑 0。
  • 通過的 3 條都錨在接線或釘版本的位置,病灶在套件或 di_containers/。這符合 DI 專掃的性質:接線檔本身幾乎沒有邏輯,問題都在「交出去的東西」。

第二層:runner 自行開檔核對(未經三人面板投票)

  • 範圍 10 支逐支通讀;卡片「重點看什麼」六條、派工三個問題逐條答完。
  • 跨讀(只讀不報):六支套件的 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 常數。
  • 實測:U13a-2 用套件真的 register() 掛端點、Flask 測試客戶端送請求,確認權限點那道門不存在。di_modules.py 用程式比對全 repo 的注入使用點。
  • DEV 唯讀查證:角色與 ai-dashboard.read 對照(16:51)、upload_files 依儲存範圍計數(18:15)、integrity_tamper_events 隔離與權限。
  • 打折處:①U13a-1 沒有實刪(會弄壞 DEV 共用檔),是讀碼+讀 migration 推論;修正線的歸屬名冊擋不擋得住,也沒驗。②本機 venv 有四支套件(file-upload/ai-dashboard/iam/ai-bot)指向修正線原始碼,不是出貨的釘住版本。U13a-2 的實測跑在修正線上,但我另外開檔確認主線 1.2.0 與搬家前的主專案端點也沒有權限點這道門,兩邊結論一致。③CM-1605 的面板分歧沒有追到底。
§9

執行概況

項目 值
掃描目標 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),皆未經面板