W3 掃描報告 — 檢測的接線本體(CM-2092)

範圍:12 檔/1,457 行(core/plugins/detection.py、core/plugins/_host.py、檢測三支 DI 容器檔、infra/readmodel/detection/ 兩支查詢、生命週期接縫、任務型別申報、三支轉手檔)。common/code/detection_tools_error_code.py 依首腦裁定不列入。 掃描工具:Claude Code 官方 claude-security plugin,effort low,focus 生產程式碼。 掃描基準 commit:aa23a63554ce(工作區有其他 session 的未提交改動)。 驗證章:verified(0 候選,面板 0 票)。只掃不修。


1. 一句話結論

卡片說的「上游防線」——detection.py 第 337~340 行那組解壓上限——其實是沒接上的死設定:套件收下之後一次都沒讀過。 真正擋住上傳檔案的,是 DI 容器另外直接讀 Config 組出來的那一份;而網址那條路(總表第 116 項)在修正分支上補的上限,用的是套件寫死的數字,也不讀宿主設定。所以同一件事現在有三套數字,運維調環境變數只會改到其中一條路。這不是可以直接打的洞,但它就是「守了一半」會長出來的樣子(見 W3-1)。

工具這一棒 0 條候選。我開檔追卡片重點時另外查到一條:母公司刪除自己分享給子公司的檢測基準時,系統判斷「還有沒有人在用」會漏看子公司的引用,於是子公司正在用的基準可以被刪掉。根因是總表已登記的「資料庫隔離規則方向寫反」,這裡是它的一個新後果,不另計新項,但要列進那張修正卡的驗收項(W3-2)。

卡片其餘四點都查過了,都不成立,第 5 節逐條寫了理由。


2. 這一棒在檢查什麼

弱點檢測功能本身寫在 jedi-detection 套件裡,FR-108 十四棒已經掃完。這一棒看的是主專案把套件接上產品的那一層:套件需要「登入檢查、權限檢查、加解密、查任務、寄通知、寫證據」這些東西,都由主專案遞給它。要問的不是「哪裡沒有權限檢查」,而是:主專案遞過去的守門零件對不對、齊不齊;套件以為主專案會守的地方,主專案真的有守嗎。

core/plugins/detection.py 被 FR-108 五棒引用過,但都只是查某一點時打開看一行。這一棒是第一次從頭讀到尾。


3. 掃到什麼:總覽

# 這是什麼問題 出事會怎樣 要先有什麼才會發生 在哪裡 嚴重度 來源
W3-1 壓縮檔上限有三套數字,卡片點名的那一套根本沒接上 運維以為調了上限、實際只改到上傳那一條路;網址那條路永遠是寫死的數字。調緊的時候,會變成一條路緊、一條路鬆 有人去改 DETECTION_PROFILE_* 環境變數 core/plugins/detection.py:322(build_config,死設定)
di_containers/detection_tools/detection_tools_containers.py:152(實際生效的那一份)
不是漏洞:設定分岔,會長成漏洞的地方 runner 開檔,未經投票
W3-2 母公司刪自己分享出去的基準時,看不到子公司正在用 子公司任務綁的基準版本被刪掉,下次派工失敗,稽核軌跡裡的那份基準也跟著消失 ① 母公司把基準設成「分享給子樹」② 子公司的任務綁了它 ③ 母公司管理員按刪除 infra/readmodel/detection/detection_profile_usage_query.py:128(find_refs);根因在資料庫隔離規則 低:沒有洩漏,是資料完整性;而且刪的人本來就是基準的擁有者 runner 開檔+DEV 唯讀查證,未經投票

4. 每條發現的詳述

4.1 W3-1 壓縮檔上限有三套數字,卡片點名的那一套沒接上

白話說明

客戶可以上傳一包檢測規則(壓縮檔),也可以填一個網址讓系統去抓。為了防止有人丟「解壓炸彈」(很小的壓縮檔,解開後幾十 GB,把伺服器記憶體吃光),系統對壓縮檔設了三道上限:最多幾個檔、解開後最多多大、壓縮比最多幾倍。

我追了這組數字從哪裡來、被誰用,結果有三套:

哪一套 數字(檔數/解開後大小/壓縮比) 誰在用
① 主專案設定檔 config/config.py:194-200(讀環境變數) 10,000/500MB/200 倍 上傳檔案那條路(DI 容器 detection_tools_containers.py:152-158 直接讀它,組出驗證器交給套件)
② core/plugins/detection.py:337-340 遞給套件的 DetectionConfig 讀①,讀不到才用 20,000/500MB/100 倍 沒有人。套件全部原始碼只有宣告這四個欄位的那一處(plugin/contract.py:82-85),沒有任何讀取點
③ 套件內建預設值 common/archive_limits.py:23-25(修正分支) 10,000/500MB/200 倍,寫死 網址那條路(修正分支 profile_extractor/inspec.py:404 的 ArchiveBombGuard(archive_size=len(raw)) 沒傳上限,直接吃預設值)

卡片問的三件事,答案如下:

  • 「Config 沒設時用預設值,預設值合不合理?」 ②的後備數字永遠用不到:Config 是類別屬性,就算環境變數沒設,getattr 也一定拿得到值(os.getenv 自己就帶預設值)。真正會生效的預設值是①的 10,000/500MB/200 倍,設定檔註解說明了取值理由(現有規則包約 123KB、解開 876KB),這組數字是合理的。只是②的後備值(20,000、100 倍)跟①③都不一樣,同一件事就有三組數字。
  • 「是 detection 專用還是全域?」 是 detection 專用。變數名是 DETECTION_PROFILE_*,設定檔註解寫明它刻意跟「源碼包上傳」那組上限(DETECTION_UPLOAD_*,100MB/五萬檔)分開,因為兩者大小差兩個數量級。
  • FR-113 O3b 建議 Excel 那條路「直接沿用 detection.py:338-340」:這個指路指錯了地方,那幾行是死設定。要沿用的話,來源是 config/config.py:194-200。但這組數字是照「一份檢測規則包」的大小訂的,拿去管 Excel 合不合適,要由開 Excel 修正卡的人自己判斷,不是可以直接照抄的全域常數。

為什麼算「會長成漏洞的地方」

現在三套的數值剛好一致(①跟③都是 10,000/500MB/200),所以目前兩條路擋得一樣緊。問題出在有人調整的時候:

  • 運維把 DETECTION_PROFILE_MAX_ENTRIES 調緊成 2,000 → 上傳那條路變 2,000,網址那條路還是 10,000。調緊的人會以為全擋住了,其實只擋了一半。
  • 開發者看到 detection.py 的 build_config(),以為改這裡就能調上限 → 改了沒有任何效果,也不會有錯誤訊息。

這跟總表第 116 項(網址那條路原本完全沒有上限)是同一個病的延續:第 116 項的修法是把上限補到網址那條路上,但補的是寫死的數字,沒有接到宿主的設定。

⚠️ 兩份套件副本現況不同:feature/review 這邊的套件副本(jedi-python-package/jedi-detection),網址那條路還是完全沒有上限(inspec.py:361-369 沒有任何防炸彈檢查),也就是第 116 項在這個分支還沒修。有接上限的是修正分支 fix/security-b1 的副本。本機 venv 目前以 editable 模式指向修正分支的那一份。

建議修法(只建議、不動手)

  1. 套件把 DetectionConfig 的四個上限真的用起來:register() 用它組出 ArchiveLimits,上傳驗證器跟網址解析器都吃同一份,不再各讀各的。
  2. 主專案刪掉 detection_tools_containers.py:152-158 另外組驗證器的那段,也刪掉 detection.py:337-340 的 getattr 後備值(直接讀 Config.DETECTION_PROFILE_*),整件事只剩一份數字。
  3. 驗收時兩條路各測一次:調緊環境變數後,上傳跟網址都要被擋。

4.2 W3-2 母公司刪自己分享的基準時,看不到子公司正在用

場景

母公司(例如集團總部)的管理員在「檢測基準庫」建了一份基準,設成「分享給子樹」(FR-094 的 SHARED),底下的子公司就能拿來用。某家子公司的顧問建了一個檢測任務,綁上這份基準。之後母公司管理員覺得這份基準過時了,在基準庫按「刪除」。

系統刪除前會先查「這份基準還有沒有人在用」,有人用就回 409、不給刪。但這次它回「沒人在用」,就刪下去了。 子公司任務綁的那一版連同控制項一起消失,子公司下一次按「開始掃描」會拿到「找不到基準」;如果這個任務已經掃過,稽核軌跡裡指向的那份基準也不在了。

為什麼會查不到

「還有沒有人在用」這件事,是 detection_profile_usage_query.py 用一支 SQL 查三張表:任務綁定表 config.job_execution_detection_tools、派工單 compliance.agent_tasks、執行紀錄 compliance.detection_executions。這支查詢用的是當前操作者的身分,所以受資料庫隔離規則(RLS)限制。

這三張表的隔離規則,正是總表已登記的那條「方向寫反」(FR-108 D1 F3,模組報告 M03 第 7 條):它把登入者的公司路徑 /1/102/ 切成 {1, 102},只認「路徑上的祖先」,不認「底下的子公司」。所以母公司(102)的 session 看不到子公司(152)的綁定紀錄。

我在 DEV 開了唯讀交易、切成 cm_app 身分核對(最後 ROLLBACK,沒有寫入任何東西):

  • 以母公司 102 的身分:任務綁定表的規則判斷「子公司 152 的列看不看得到」→ 看不到;同一時間,產品正規的子樹判斷函式 app_tenant_allowed_for_session(152) → 看得到。兩者方向相反。
  • 以子公司 152 的身分:看得到母公司 102 的 58 筆任務綁定(這就是 D1 F3 已經報過的那一半)。

卡片原本擔心的情境不成立:卡片擔心「平台管理員刪公版基準時,以為沒人在用」。平台管理員是最上層公司(路徑只有一段 /1/)的成員,資料庫會給他超級管理員旗標,三張表的規則第一個條件就直接放行,所以他看得到全站的引用。公版(SYSTEM)的刪除只有平台管理員做得到,因此公版這條路沒有問題。真正漏掉的是「母公司刪自己的 SHARED 基準」這條路,這條路在 FR-094 加入分享功能之後才出現。

為什麼判「低」、而不是「中」

  • 沒有任何資料外洩,是資料完整性問題:子公司的任務斷掉、稽核軌跡少了一份基準。
  • 按刪除的是基準的合法擁有者,不是攻擊者拿到了原本沒有的權限。
  • 根因已經在總表登記過,修好那條(三張表的規則改用 app_tenant_allowed_for_session),這條會跟著消失。

處理建議

  • 不另外登記新項次,併進「隔離規則方向寫反」那張修正卡,在驗收項加一條:「母公司刪除自己分享出去、且子公司任務已綁定的基準版本,要回 409 並列出子公司的專案名」。
  • 另外要注意:修完之後,母公司在刪除錯誤訊息裡會看到子公司的專案名稱(_in_use_message 會列出專案名)。母公司本來就看得到子樹的專案(projects 表用的是正規的子樹規則),所以這不是新的洩漏,但修的人應該知道會有這個變化。

沒有實際做的事:我沒有在 DEV 真的建一份 SHARED 基準、讓子公司綁定、再從母公司刪除。上面的推論是「讀程式+讀資料庫規則+唯讀驗證規則的判斷結果」得出的。手測步驟見第 8 節。


5. 卡片點名、查證後不成立的(排除也是結論)

卡片重點 結論 怎麼查的
platform_admin_check/scope_writable_check 走「模組級設定」,會不會在程序啟動時就被固定成某個人的身分 不會 啟動時交給套件的是函式本身(detection.py:315-316),套件存在模組變數裡(common/guard.py:26-30)。每次呼叫時,函式才去讀 get_user_context()(登入者身分存在「每個請求各自一份」的變數裡),viewer_is_platform_admin()(jedi_iam/authz/platform.py:15)與 assert_scope_writable()(common/authz/sharing.py:25)都是當下才讀身分。沒接上時套件會直接拋例外,不會預設放行(guard.py:39-45)
CryptoAdapter 沒有「誰能呼叫解密」的限制 設計如此,不重報第 85 項 這是一個薄轉接(detection.py:129-140),只負責把宿主的 Fernet 金鑰物件遞給套件。「誰能觸發解密」由呼叫它的 service 決定,第 85 項(測試連線沒有守門)的缺口在套件的測試連線那一支,不在這個零件
detection_profile_usage_query.py 會不會因為隔離規則只看得到自家引用,讓平台管理員刪公版時以為沒人在用 平台管理員這條不成立;但查出母公司刪 SHARED 那條=W3-2 見 4.2
detection_job_notify_query.py 會不會把別家公司的人拼進收件人 不會 這支查詢只在遠端代理程式回報結果時執行,那時的身分是用 tenant_context() 設成派工單所屬公司的機器身分(jedi_remote_agent/.../agent_task_service.py:113-120,公司編號由伺服器從派工單查出,不信任請求內容)。收件人來源三張表都有隔離規則:task_assignees/project_participants 都要求「所屬專案看得到」,projects 用的是正規的子樹規則。唯一沒有隔離規則的 workflow_execution_control_mapping 只拿來串出 project_id,接著用那個 id 去查 projects 時還是會被規則過濾。這條路拼不進別家的人
detection_lifecycle_listener.py 不重報 FR-077 R3 37 行純轉呼叫,沒有判斷邏輯
AI 儀表板那條路(④) 不適用 di_containers/dashboard_apis/ 下沒有任何檢測相關的申報

6. 「套件以為宿主守、宿主以為套件守」逐處追查

註解或契約說…… 追到另一側的結果
detection.py 檔頭:「認證與三軸授權守門由宿主給」 ✅ 給齊了。auth_required=jwt_required()(:302);授權、能力點、平台管理員三支 adapter(:303-305)都呼叫 common.authz 的正式守門(實際 import 驗證五支函式都存在)。套件 routing.py:166-180 把 auth_required 包在每一條路由上;缺少必要接線時 register() 會拒絕掛載
detection.py:131-134:「本 port 缺了就是明文落庫」 ✅ 有給(:306),而且這一項被列在「缺了拒絕掛載」的必要接線裡
detection_orchestration_containers.py:45-47:「task_lookup 缺了,派工三道守門全部無從進行」 ✅ 有給。TaskLookupAdapter.get_task 查的 job_executions 有隔離規則;查無時照契約回 None,由套件決定回 404
套件編排服務(修正分支):「任務被指派人或專案 manager 才可操作(assert_job_operator)」 ⚠️ 這個方法在本分支的宿主上不存在。修正分支的套件呼叫 self._wf_svc.assert_job_operator(...)(8 處),但 feature/review 的 app/flow_engine/service/workflow_execution_service.py 只有 assert_project_participant(:126),assert_job_operator 只存在於 BE 的修正分支 worktree。這不是破口,缺方法只會拋 AttributeError(回 500、不會放行)。但本機 venv 以 editable 指向修正分支的套件,feature/review 的 BE 在本機跑起來時,檢測的「開始掃描」等 8 支操作應該會全部 500。只要兩邊分支一起合併就沒事,這裡是提醒驗收環境要對齊
task_type_declaration.py 啟動掛鉤「對檢測任務自動派第一次掃描」 ✅ 守門在下一層。掛鉤呼叫 orchestration.start_execution(),那支方法一進去就先檢查任務操作權(detection_orchestration_service.py:196/修正分支 :202),權限檢查不會被掛鉤繞過

同一套接線五支並排、有沒有哪支少了(對照盤點檔 §3 ②的並排表):檢測是唯一「逐項手寫」、不吃 host_defaults() 的一支,但登入、能力點、授權、身分四道門都有給,而且多給了平台管理員與分享守門,沒有少給的。

守門次數對公開方法數:

檔 守門字眼 公開方法 說明
core/plugins/detection.py 10 20 守門字眼集中在四支 guard adapter;其餘是資料轉接,守門在套件的 route/service 層
core/plugins/_host.py 3 5 host_defaults() 給能力點守門;lazy()/di()/_HostServiceProxy 都是呼叫當下才解析,沒有「存下第一個實例」的問題
兩支 readmodel 查詢 0 各 2 查詢層本來就不該有守門,隔離靠資料庫規則(就是 W3-2 出事的那一層)
生命週期接縫、任務型別申報 0 3/1 見上表
三支轉手檔 0 0 純 re-export,沒有邏輯

7. 這份結果可信到什麼程度

工具那一層:「0 條」是真的 0 條,但只代表快速掃過一輪沒撿到東西。 研究員與密鑰專項各派 1 支、都回來了,兩支都回報空清單,面板因此 0 票,驗證章是 verified。但研究員沒有交代它讀了哪些檔(工具的 coverage.research 是空的),所以不能說 12 支檔都讀完了。

runner 那一層:W3-1 與 W3-2 沒有經過三人面板投票,是我自己開檔核對的。

  • W3-1:在兩份套件副本上 grep 過 profile_max_* 的所有出現處,確認只有宣告沒有讀取;三套數字都開檔逐行核對過。
  • W3-2:DEV 查過 pg_policies,用唯讀交易以 cm_app 身分驗證規則的判斷結果(最後 ROLLBACK);用 pg_has_role 確認 cmmgr 是 cm_app 的成員。沒有實際跑一次「建分享基準→子公司綁定→母公司刪除」的完整流程。
  • 引用的套件行號:feature/review 副本與修正分支副本的行號不同,報告裡都標了是哪一份。

不保證完整:用的是最快的掃描檔位(effort low,沒有威脅建模和廣度掃描)。套件本體不在範圍內,只在追接線時讀了相關段落。


8. 手測清單(給驗收用)

W3-2

  1. DEV 準備母公司 A、子公司 B(B 的路徑在 A 底下)。
  2. A 的管理員建一份檢測基準,改成「分享給子樹」。
  3. B 的顧問建一個檢測工具任務,綁上這份基準的目前版本(不用真的掃)。
  4. A 的管理員先打「查這份基準的使用狀況」API → 預期現況:回 in_use: false(應該要是 true)。
  5. A 的管理員刪除這份基準 → 預期現況:刪除成功(應該要 409)。B 的任務再按開始掃描 → 回「找不到基準」。

W3-1

  1. 把 DETECTION_PROFILE_MAX_ENTRIES 設成 50,重啟 BE。
  2. 上傳一份 60 個檔的規則包 → 被擋(上傳那條路有吃到設定)。
  3. 用網址方式建立同一包 → 預期現況:修正分支上會通過(網址那條路吃的是寫死的 10,000),feature/review 上完全沒有上限。

9. 執行概況(數字,給工程師看)

項目 數值
掃描範圍 12 檔/1,457 行
基準 commit aa23a63554ce(工作區 dirty,有平行 session 改動)
檔位 effort low,focus 生產程式碼
研究員 派 2 支(研究 1+密鑰專項 1),回 2 支,零失敗、零重跑
原始候選 0 條
投票 0 票(沒有候選)
驗證章 verified(CLAUDE-SECURITY-REVISION-aa23a63554ce-dirty.json)
工具 run ID wf_1783d39b-c1f
耗時 約 16 分鐘(2 個 agent,約 20.6 萬 token)
工具產出原始報告 CLAUDE-SECURITY-20260923-130633/(未入版控)
runner 自行查證 兩份套件副本逐檔 grep、DEV 唯讀查 pg_policies/tenants/pg_roles、唯讀交易驗規則判斷

10. 待首腦裁決

  1. W3-1 要不要登記:建議記進總表 §3.2(非資安程式錯誤,會長成漏洞的地方),修正卡跟第 116 項一起開,因為修法是同一件事:讓網址與上傳兩條路吃同一份、由宿主設定的上限。
  2. W3-2 併進「隔離規則方向寫反」那張修正卡:建議不另外登記項次,只在那張卡加一條驗收項(見 4.2)。
  3. FR-113 O3b 的指路要不要更正:它建議 Excel 修法「沿用 detection.py:338-340」,但那幾行是死設定。建議改指 config/config.py:194-200,並註明那組數字是照檢測規則包的大小訂的。
  4. 本機驗收環境對齊:feature/review 的 BE 搭修正分支的套件,檢測操作會因為缺 assert_job_operator 回 500(第 6 節)。驗收 FR-114 修正或手測本棒時,要注意兩邊分支要一致。