檢查日期 2026-09-18|對應卡片 CM-1875(D3 第 2 小棒)|檢查範圍 5 個檔案 1,328 行
找到 1 條中風險:換工具時「掃描範圍」不會重新檢查,可以讓系統把一整個超大網段逐台展開,把後端伺服器的記憶體吃爆、服務倒掉。 要先是某個專案的管理者,送兩次修改再按執行才打得到,所以不急;修法很小——換工具時拿「實際會生效的值」重驗一次,再幫展開那支加一道硬上限。另外查證了卡片重點⑥的另外三項確認沒問題:密碼加密的時機是對的(綁定與 BPMN 兩份同源、不會留明文副本)、留空沿用不會把密碼寫到別人那列、掃描範圍只有專案管理者能改。
客戶在一張稽核任務上指定「要用哪個檢測工具、掃哪些機器、用什麼帳密登入」。這份設定存下來之後,按「執行」時會被拆成工單送給裝在客戶機房的代理程式。
這一棒看的是存設定的那一段,兩件事:
192.168.50.1-50 或 10.0.0.0/8 這種寫法,系統要把它展開成一台一台的位址。填得太大就是一顆炸彈——問題是檢查有沒有真的擋在每一條路上。掃描目標 /Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-detection,revision 955e40964baa(branch feature/review,工作區乾淨),mode scan,effort low,範圍 5 個檔案:綁定處理器 detection_job_binding_handler.py(659 行)、任務層敏感參數加解密 detection_secret_params.py(154 行)、掃描目標語法與展開 scan_target_spec.py(307 行)、分派參數工具 detection_assignment_params.py(171 行)、綁定表 model job_execution_detection_tool.py(37 行),共 1,328 行。
low 強度:一位研究員讀完這 5 個檔案就提報候選,未做元件盤點、未做威脅建模、未跑額外密鑰專項掃描,completenessCheckOutcome 為 not-applicable(低強度本來就不跑盤點)。驗證跑了 1 輪,2 個候選去重後仍是 2 個,沒有候選遺失、沒有嚴重度被降低、沒有候選被略過。研究員為了追資料流另外讀了範圍外的幾處(主專案的任務序列化器與 job service、BaseRepositoryImpl 的更新語意、編排服務的展開與派工、jedi-file-upload 的下載守門與 RLS),那些只當佐證、沒有納入稽核。
研究員沒有回報「哪些檔案沒讀完」的自述(coverage.research 為 null),所以工具端沒有做讀取完整度的交叉檢查。
工具沒有實際執行任何程式碼:沒跑測試、沒發請求、沒示範攻擊,所有判斷都是讀原始碼推出來的。唯一的例外是首腦核對時直接呼叫 scan_target_spec 這支純函式確認數字(count_spec('10.0.0.0/8') 回 16,777,214),那是讀取性質的驗算、不改任何狀態。
工具碰到了卡片重點⑥的「掃描目標驗證」那一半,「加密時機」那一半完全沒提出候選,由首腦回頭開檔查證補上,詳見下方「卡片重點逐項人工查證」段。
現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。
- F1(換工具時掃描範圍不重驗)=M03 第 11 條,✅ 已修(CM-2058)。
| 編號 | 這是什麼問題 | 出事會怎樣 | 要先有什麼 | 在哪裡 | 嚴重度 |
|---|---|---|---|---|---|
| F1 | 換工具時掃描範圍不重驗,超大網段留在原地被逐台展開 | 後端工人記憶體被吃爆,API 服務變慢或倒掉,過程中還握著一個沒關的資料庫交易 | 專案管理者身分 + 兩次修改 + 按執行 | detection_job_binding_handler.py:202 |
MEDIUM |
被駁回 1 條(內部欄位沒過濾就寫進派工參數),三位檢查員一致認為拿不到好處,理由見下方。
這是什麼問題。 「掃哪些機器」這一欄有台數上限(預設 256 台),但上限只檢查「這次送上來的值」。如果這次的修改沒有帶這一欄,程式就當作沒東西要檢查、直接跳過;而底層更新又有一條規則是「沒送的欄位不覆蓋」,於是上次存進去的那個超大網段原封不動留著,工具卻已經換成受上限管的那一支了。等到按下執行,派工端才真的去展開這個網段——那裡沒有上限(那支程式的註解自己就寫著「這裡刻意不擋」)。
還有一個前提讓這件事打得成:不是每個工具都受台數上限管。OpenVAS 這類工具自己就吃網段寫法、不需要逐台登入,所以刻意豁免;OpenSCAP、Nmap、CINC Auditor 這幾支要逐台 SSH 登入的才受管。攻擊者就是利用這個豁免把大網段先存進去。
出事會怎樣。 一個 /8 網段展開後是 16,777,214 台(首腦實跑 count_spec 確認的數字)。展開的動作會在記憶體裡生出一千六百多萬個字串、一個去重用的集合,最後再把它們串成一條逗號分隔的長字串——而且整段過程都在一個請求的資料庫交易裡面。結果是那個 API 工人的記憶體被吃光、行程變慢或直接被系統砍掉,同時那個沒結束的交易還一直佔著資料庫連線。其他使用者這段期間會覺得系統卡住或連不上。
要先有什麼才打得到。
compliance-manager-be/app/flow_control/service/job_service.py:235 的 _require_manager。10.0.0.0/8;第二次只換工具(換成 OpenSCAP)、不帶參數。三個條件都是「一個內部人員刻意做」才成立,不是外面的人打一個網址就中——所以是中風險不是高風險。
在哪裡。
jedi-detection/jedi_detection/app/service/detection_job_binding_handler.py:201-204 —— 台數檢查只吃 tool.get("params"),也就是這次送來的值:331-333 —— values = [v for v in raw_values if v],全是 None 就直接 return,後面的上限檢查一行都沒跑jedi-common/jedi_common/session/database/repository/base_repository_impl.py:424 —— value is not None 這個條件讓 tool_params=None 被跳過,舊值留著compliance-manager-be/api/flow_control/serializers/job.py:256 —— params = fields.Raw(allow_none=True, load_default=None),沒帶就是 Nonejedi-detection/jedi_detection/common/scan_target_spec.py:60-67 —— SSH_ITERATED_TARGET_FIELDS,openvas 不在裡面jedi-detection/jedi_detection/app/service/detection_orchestration_service.py:555 —— expand_spec(raw),上游函式的 docstring(:535-537)明說「真的走到這裡還超量仍照展開」另一個落點(首腦補查,工具沒提)。 同一個缺口在分派列那條路也成立,而且更直接:_replace_detection_tool_agent_assignments(:492)開頭的 if assignments is None ... return(:536)表示「沒帶分派清單=不動既有設定」,於是 :570 那道同名的台數檢查根本不會跑,既有列裡的 hosts 就這樣帶著舊值跟到新工具底下。修法要兩條路一起補,只補其中一條會留下另一半。
怎麼修。 兩層,都要做:
detection_job_binding_handler.py:201 附近,把餵給檢查的值從「這次送來的」改成「實際會生效的」——tool.get("params") 取不到時回頭拿 existing.tool_params。判斷條件是工具有沒有換(detection_tool_id 與 existing.detection_tool_id 不同),換了就一定要用生效值重驗一次。分派列那條(:570)同樣處理:工具換了就把既有列的 hosts 一併拉出來重驗。detection_orchestration_service._expand_scan_target_fields(:551 的迴圈裡)呼叫 expand_spec 之前先呼叫 scan_target_spec.count_spec(raw),超過一個硬上限就記 error 並跳過該欄位(或直接讓這張工單失敗),不要展開。count_spec 是純算術、不產生清單,成本幾乎是零(這正是它當初被寫出來的理由,見 scan_target_spec.py:168-170 的 docstring)。這一層是給存量資料的保險——就算寫入端補好了,資料庫裡已經存著的超大網段還是打得到。⚠️ 注意展開端的現行設計是刻意不擋的(docstring 寫「寧可跑一張大工單,也不要讓一個已經存在的任務突然變成執行不了」)。所以那道硬上限要訂得比使用上限寬很多(例如 65,536),只用來擋「明顯是炸彈」的輸入,不要讓正常的大範圍掃描被誤擋——否則會推翻原本的設計判斷。
驗證。 3/3 三位檢查員確認成立(可達性、影響、既有防護三個角度),嚴重度維持 MEDIUM。三位各自獨立把鏈子追完:序列化器的預設值 → 檢查被跳過 → 底層不覆蓋 → 展開無上限,四個環節都對得上,且都確認中間沒有任何一處做過補救。
為什麼是 MEDIUM 不是 HIGH: 要先是專案管理者,還要分兩次送修改、再按執行;打不到資料(不會外洩也不會竄改),只會讓服務變慢或倒掉。
首腦核對註記。 屬實,逐環開檔核對過::201-204 確實只吃這次的值、:331-333 確實提早 return、base_repository_impl.py:424 的 value is not None 確實會跳過 None、SSH_ITERATED_TARGET_FIELDS 確實不含 openvas、_expand_scan_target_fields 確實無上限。另外實跑 scan_target_spec 確認 10.0.0.0/8 語法檢查會通過、展開台數是 16,777,214、openvas 回空 tuple(不受管)、openscap 回 ('hosts',)(受管)——攻擊路徑的每一環都對得上。與 D1/D3-1 不重複:D1 查的是憑證存放與守門,D3-1 查的是結果回收信不信代理程式,都沒碰到綁定寫入這一段。淨新增 1 條,登記跨 arc 總表。
卡片重點⑥有兩半:「任務綁定的加密時機」與「掃描目標驗證」。工具只碰到後者(就是 F1),前者完全沒提候選,首腦開檔補查。
設計本身是對的。 密碼不是整欄加密,而是包成一個「自我描述的信封」:{"__enc__": "fernet", "value": "<密文>"}。這樣做的理由寫在 detection_secret_params.py 的模組說明裡,而且理由站得住——密文存進 JSON 之後跟明文一樣都是字串,光看值分不出有沒有加密;如果靠「查工具定義哪些欄位是機敏的」來決定要不要解密,那工具定義一改版,舊任務就會解錯(把明文當密文解會炸、把密文當明文送出去會讓代理程式拿密文去登入)。改成信封之後,值自己會說明自己加密與否,工具定義怎麼改都不影響既有資料。
兩條寫入路徑都有加密,沒有漏的。 任務層一條(:147-159)、分派列一條(:412-433),兩處都呼叫同一支 encrypt_secret_params。
加密的時機擋在對的位置——:139 的 _encrypt_tool_secret_params 是在 tool dict 剛進來、還沒分岔去寫 BPMN XML 之前就做的。這點很關鍵,該函式的 docstring(:128-133)自己點名了:同一個 tool dict 會同時被拿去寫綁定表和寫進 BPMN 流程定義的 XML,如果只在綁定那端加密,BPMN XML 裡就會留一份明文密碼副本,而那份還會被規劃頁與 BPMN 編輯器讀出來——等於加密了卻照樣外洩。現行寫法是在分岔前就加密,兩份拿到的都是密文。判定:時機正確。
「留空=沿用原值」不會把密碼寫到別人那列。 前端對已設定的密碼欄位是留空的(不會把密文送回來),所以後端要從既有資料沿用。危險在於「沿用誰的」——分派列有很多列,拿錯列就會把 A 列的密碼寫進 B 列。核對 :415-417:沿用來源是用 uid 逐列對位建的表(previous_by_uid),沒有 uid 的列視為新增、本來就沒有前值。判定:對位正確。
三處剝除都在。 加密只解決資料庫,畫面與稽核記錄要另外處理:strip_secret_params 的做法是整個欄位不出現而不是遮成 ****——理由(:120-124)也站得住:遮罩仍要把值取出來走過序列化,任何一處忘了套就是明文外洩,而且遮罩後的密文照樣會落進 api_logs.response。判定依據是「值是不是信封」而不是查工具定義,所以連拿不到定義的讀取路徑(control-tree 的 raw SQL)也剝得掉。判定:沒問題。
兩個容錯行為的方向是對的(出錯時偏向安全):crypto 沒注入時會記 warning 而不是靜默落明文(:79-83);解密失敗時剝除該欄位而不是把密文丟給代理程式(:130-135),因為送密文過去會讓它拿密文當密碼去登入,症狀是「掃描跑完但沒進到登入後頁面」,比「缺欄位」難查得多——缺欄位至少會被連接器明確回報。
語法檢查本身寫得不錯,三個細節值得記。
scan_target_spec.py:93-99)。只有長得像「四段數字」的東西才套 IPv4 規則。這是必要的,因為掃描目標欄一直都允許填主機名,不能因為新增了 IP 語法就把它們擋掉。:107-137)。192.168.1.1-192.168.2.5(跨網段範圍,目前不支援)長得像一個含連字號的主機名,如果讓它走放行那條路,會被當成一台叫這個名字的主機送出去,DNS 解析失敗變成一筆「主機不存在」——使用者以為掃了一整段、其實零台。現在會明確報錯告訴他要改寫法。這是靜默失敗的反面教材,寫得對。:127-129)。192.168.50.100-50 這種會報錯而不是幫他調過來,理由是「自動對調等於猜使用者的意思,猜錯就掃了一整段不該掃的機器」。誰能改掃描目標:只有專案管理者。 三個入口(建立任務 :134、更新任務 :179、改分派 :235)都先呼叫 _require_manager(compliance-manager-be/app/flow_control/service/job_service.py:94)。這是 FR-048 軸③的資源域守門,走 app service 層、不是 route 層 decorator,符合規範。判定:權限這一層沒問題。
前端送來的內部欄位會被剝掉。 scan_targets 裡 _ 開頭的欄位是後端自己維護的狀態(例如源碼包的參照),前端看不到也不該送。:556-561 會先把前端送來的內部欄位一律丟棄,再從既有資料逐列保留原本的——剝除與保留是一組動作,少了後者的話使用者每次按儲存都會把源碼包參照清掉。判定:處理正確(這也正是下面被駁回那條候選的爭點,見後)。
缺口就是台數上限那一項 —— 見 F1。
研究員提報:綁定寫入那條路(detection_job_binding_handler.py:213)沒有做上面說的「剝掉 _ 開頭內部欄位」那一步(分派列那條路 :558 有做),所以前端可以塞 _source_file、_profile 這種內部欄位進去,一路流到代理程式讀取檔案時用的允許清單。
三位檢查員一致否決,理由一致且首腦複核同意:
:213 確實少了那道剝除,注入的欄位也確實會存活到派工參數裡。/api/1.0/file/download/<uid> 的守門與這個欄位無關。agent_file_access_service.py:127-130 會從代理程式那筆資料推出租戶身分,讀取時被資料庫的租戶隔離(RLS,就是「每個客戶只能看自己資料」的機制)擋住,跨租戶拿不到東西。判定:不是漏洞,不計為發現。 不過既然兩條寫入路徑對同一件事的處理不一致(分派列剝、綁定不剝),建議在修 F1 時順手把 :213 也補上那道剝除——一致性本身有價值,而且「兩條路不一樣」這件事本身就是下一個人踩坑的來源。列為體質改善建議。
分兩層講:
「報出來的這條存在嗎」——可信度高。 F1 的每一環首腦都開檔核對過,而且實跑純函式確認了數字(/8 是 16,777,214 台、openvas 不受管、openscap 受管)。三位檢查員從三個不同角度獨立追同一條鏈,三票全過。另外首腦補查時還找到同一缺口的第二個落點(分派列那條路),這增加而非削弱了對這條發現的信心——但也代表修法不能只補一處。
「只有這條嗎」——可信度中等,有三個已知限制。
low,只有一位研究員讀一遍,不是多位分工交叉讀。低強度的設計目的是快速篩,不是窮盡。verification.status 為 verified:三位檢查員對 2 條候選各投一票,6 票全數投出,沒有漏投,票數由工具自己的程式碼統計、不是任何 agent 自報。
| 項目 | 數字 |
|---|---|
| run ID | wf_e2650a07-12c |
| 掃描 commit | 955e40964baa3cfc0e8766078db7d65075d3c50b(工作區乾淨) |
| 範圍 | 5 檔 1,328 行 |
| 強度 | low(一位研究員 + 三票面板) |
| 研究員 | 派 1 位、回 1 位、零重試 |
| 面板 | 2 條候選 × 3 位檢查員 = 6 票,全數投出 |
| 總耗時 | 約 140 分鐘(21:29 啟動 → 23:49 報告產出) |
| 候選 → 成立 | 2 → 1 |
| 驗證輪數 | 1 |
| 驗證章 | verified |
| 工具原始產物 | jedi-detection/CLAUDE-SECURITY-20260918-132920/(不入版控) |
這一棒是「每棒總行數 ≤ 2,000」判準的第二次實證。 1,328 行、零重試跑完。另外這棒是刻意與另一支掃描(jedi-evidence-classification E1)平行跑的——決策者要驗「並行」是不是死因之一。結果是平行沒有害死它,這與第 6 次診斷的結論一致:死因是研究員單次思考超過 180 秒被判停滯,跟並行無關。不過樣本只有一次,「一次只跑一棒」的紀律沒有因此解除,後續仍照舊。