檢查日期 2026-09-17|對應卡片 CM-1873|檢查範圍 36 個檔案
找到 4 條發現、其中 1 條高風險。 高風險那條是「測試連線」這個按鈕背後的端點忘了檢查權限——客戶公司裡任何一個登得進系統的人,都能叫系統把存好的掃描工具帳號密碼解密,送到他自己指定的一台電腦去,等於把客戶整批機器的維運帳密拱手送人。另外三條中風險分別是:那個端點的目標主機完全照使用者填的走(把客戶內網的代理程式變成任意連線跳板)、資料庫的客戶隔離規則寫反了方向(子公司反而看得到母公司的資料)、以及掃描歷史紀錄少了專案參與者檢查。高風險那條修法很小,就是補一行權限宣告,建議優先處理。
客戶要用系統做弱點掃描之前,得先在設定頁填好掃描工具的帳號密碼(例如 SonarQube 的權杖、連 Linux 主機的 SSH 帳密、連 Windows 的管理員密碼),系統會加密存起來。這一棒看的就是這批帳密的整條路:存進去、讀出來、送到畫面上、寫進系統日誌,每一段有沒有漏;「測試連線」這個功能會不會被人拿來當跳板;誰能改別家客戶的設定;以及整支套件的守門機制與資料庫隔離規則。
掃描目標 /Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-detection,revision 6119da51d488(branch feature/review,工作區乾淨),mode scan,scope 36 個檔案(檢測工具字典、租戶工具設定與憑證、任務層敏感參數、守門殼與路由表、五支資料庫變更腳本、插件組裝層),effort low,focus attack-surface。
low 強度+設定了 focus:派出兩位研究員(一位通盤讀完 36 檔,一位專做密鑰專項),未做元件盤點、未做威脅建模,completenessCheckOutcome 為 not-applicable(低強度本就不跑盤點)。兩位研究員共提報 8 個候選,去重後 6 個,三位檢查員各自獨立投票共 18 票、全數投出,沒有候選漏投、沒有候選遺失、沒有嚴重度被降低。6 個候選中 5 個通過、1 個被三位檢查員一致駁回(詳見下方「被駁回的候選」)。通過的 5 條中兩條(F4/F5)是同一個問題的兩個角度,已依決策者指示併為一條,故本報告呈現 4 條。
工具沒有實際執行任何程式碼:沒跑測試、沒發請求、沒示範攻擊,所有發現都是讀原始碼推出來的。
工具只碰到卡片列的八個重點裡的三個(①③⑤)。 ②④⑥⑦⑧ 五個重點工具完全沒有提出任何候選,全部由首腦回頭開檔查證補上,詳見下方「卡片重點逐項人工查證」段。
🔴 兩點必須先講明白(本報告的打折處):
CLAUDE-SECURITY-20260917-014935/,連帶帶走工具自己產的報告檔與驗證章。這不是平行掃描造成的(同時段另一支 D3 掃描有自己獨立的目錄,互不干擾)。本報告的發現內容與票數取自工作流實際回傳的結果,完整且未經改寫,但沒有工具蓋的驗證章可供對照。6119da5,掃描結束時已是 82d78bc(中間三筆是分類容器的 commit,屬別線工作、不在本次掃描範圍)。本報告描述的是 6119da5 這個版本,行號請以該版本為準。現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。
- F1(測試連線無權限檢查)=M03 第 2 條,✅ 已修(CM-2042)。
- F2(測試連線目標主機照單全收)=M03 第 6 條,✅ 已修(CM-2058)。
- F3(客戶隔離規則方向寫反)=M03 第 7 條,✅ 已修(CM-2271,BE
6c74ff0e3/套件6e0d0d4b,1.21.0 出貨)。- F4(掃描歷史缺參與者檢查)=M03 第 12 條,✅ 已修(CM-2040)。
這是什麼問題。 產品的檢測工具設定頁有個「測試連線」按鈕,用來確認填的帳密能不能連上。這個按鈕背後的 API 只檢查「有沒有登入」,沒有檢查「有沒有管理檢測工具的權限」——而同一個檔案裡的新增、修改、重置三個功能都有檢查。更麻煩的是,這個 API 允許呼叫的人自己指定要連到哪一台主機,系統就會把加密存著的帳密解開、交給客戶內網的代理程式,由它拿著這組帳密去連那台主機。
出事會怎樣。 客戶公司裡任何一個登得進系統的人(包含權限被刻意限制到最小的唯讀稽核員、約聘人員),可以:先打清單 API 拿到所有工具設定的識別碼(那支也沒有權限檢查),再打測試連線 API 並把目標主機填成自己控制的一台機器。系統會解密這家客戶的掃描帳密並送過去——包含 SSH 密碼或私鑰、Windows 管理員密碼、OpenVAS/SonarQube/ZAP 的 API 權杖。攻擊者在自己那台機器上架個服務就能把明文密碼記下來,而那組帳密正是用來掃描這家公司整批機器的,等於一次拿到全公司維運通行證。此外還能任意覆寫別人設定上的「上次測試結果」欄位。
要先有什麼才打得到。
plugin 模組在哪裡。 jedi_detection/api/routes/detection_tool_route.py:126(TenantDetectionToolConfigTestConnectionRoute.post)只掛了 @require_license("plugin")(這是「這家客戶有沒有買這個模組」的授權檢查,不是「這個人有沒有這個權限」);對照同檔的 :67 新增、:88 修改、:105 重置,三支都多掛了一行 @capability_required(lambda rt: rt.config.plugin_update_capability)。解密與外送的實際位置在 jedi_detection/app/service/detection_tool_service.py:158 起的 test_connection()。同檔 :53 的清單端點也沒有權限檢查,是攻擊者取得識別碼的那一步。
怎麼修。 在 TenantDetectionToolConfigTestConnectionRoute.post 補上與三個兄弟端點一模一樣的那行 @capability_required(lambda rt: rt.config.plugin_update_capability)。清單端點 TenantDetectionToolConfigsRoute.get(會吐出設定識別碼與 field_values)建議一併補,但那屬產品決策(讀取端要不要守門),可一起裁。
驗證。 3/3 三位檢查員(可達性/影響/既有防護)一致確認成立,嚴重度維持 HIGH。
首腦核對註記。 屬實,逐檔開過行號全部對得上。 這正是卡片重點①預判的那一點,而且坐實了它會解密憑證往外送,比卡片原先「回應會不會洩漏憑證」的疑慮更嚴重——洩漏不是透過回應,是透過連線行為本身。
這是什麼問題。 上一條說的那個 API,目標主機是呼叫者在請求裡直接填的,系統完全不檢查這台主機是不是這家客戶登記過的掃描目標,就把它和解密後的帳密一起交給代理程式去連。而代理程式依設計就裝在客戶的內部網路裡。
出事會怎樣。 代理程式變成一支「代打任意連線」的工具:外面的人可以指使它去連內網任何位址。更細的問題是,回傳的訊息會分辨「連線被拒」「找不到路由」「認證失敗」「工具沒安裝」這幾種不同結果——這等於一台內網掃描器,攻擊者靠回應內容就能摸清內網有哪些主機、開了哪些服務。而且主機欄位可以一次填多台(用逗號分隔),一個請求就能探測一整段。
要先有什麼才打得到。
在哪裡。 jedi_detection/app/service/detection_tool_service.py:224(DetectionToolService.test_connection 裡逐台發送 probe 的那段);主機字串來自 route :132 的 payload.get("host"),中間只做了用逗號/空白/分號切開,沒有任何比對或過濾。
怎麼修。 把目標主機限制住,而不是照單全收:逐台比對這家客戶已登記的掃描目標(或明確的白名單),拒絕不合理的內外網跨越,限制一次最多幾台,並且把逐台的細部結果收斂成單一的成功/失敗,讓回應不再能當內網探測器用。
驗證。 3/3 三位檢查員一致確認成立,嚴重度 MEDIUM。
首腦核對註記。 屬實。與 F1 是同一個端點上的兩個獨立問題:F1 是「誰可以按」,F2 是「按下去可以打到哪」。補 F1 的權限檢查並不會讓 F2 消失——有管理權限的人依然能拿代理程式當跳板,只是門檻從「任何登入者」升到「工具管理員」。兩條要分開修。
這是什麼問題。 系統用資料庫層的規則來隔離不同客戶的資料,判斷依據是「這個人屬於哪個組織路徑」。但這五張表的規則把路徑(例如 /1/102/)從中間切開,得到 [1, 102] 這串編號當作可存取清單——於是屬於 102 的人,連編號 1(也就是它的母公司/平台根)的資料也一起拿到了。正確的做法應該是「我自己和我底下的單位」,同一個檔案裡的基準庫相關規則用的就是正確寫法(app_tenant_allowed_for_session()),只有這幾張表用了切路徑的寫法。
出事會怎樣。 子單位的一般成員,對母單位(或平台根)的檢測工具設定與掃描執行紀錄,可以查、可以改、可以刪。搭配 F1 那條,還能把母單位的帳密解密後從自己這邊的代理程式送出去。
要先有什麼才打得到。
TenantDetectionToolConfigDomainService.verify_config_is_exist() 只用識別碼查、完全依賴資料庫規則把關,應用層沒有另一道客戶檢查在哪裡。 jedi_detection/migrations/002-detection-rls-grants.sql::34(detection_execution_groups)、:39(detection_executions)、:86(job_execution_detection_tool_agents)、:91(job_execution_detection_tools)、:96(tenant_detection_tool_configs)——五條都是 FOR ALL(查改刪一次全包)且都用 string_to_array(...) 切路徑,且都沒有 WITH CHECK。同檔 :44-:82 的基準庫八條規則則是正確寫法,對照明顯。
🔴 範圍註記(決策者指定):同樣寫法的規則在出貨基線裡共 9 條,除本套件這 5 條外,另含 agent_tasks 與三張授權相關表。 這不是本套件獨有的寫法,修法要一次看完 9 條、統一改成正確的子樹語意,不能只改這五條——否則同一個缺口會留在另外四張表上。
怎麼修。 把這幾條規則的判斷式換成 app_tenant_allowed_for_session(tenant_id)(即「我自己和我底下的單位」,與檔案其餘部分一致),並在 verify_config_is_exist() 補一個明確的客戶條件,讓應用層不再單靠資料庫規則把關。動到出貨基線的規則屬決策者裁示範圍,runner 不自行處理。
驗證。 2/3 通過:可達性與影響兩位確認成立,既有防護那位持保留。異議在「實際部署有沒有這種母子結構」,不在規則寫法本身——寫法方向寫反是三位都同意的事實。
首腦核對註記。 規則寫法屬實,五條全部開檔核對過,一字不差。
🔵 首腦已補做 DEV 唯讀實查(2026-09-17 13:30),前提成立:
/1/102/、/1/102/152/153/ 這種「母客戶底下掛子單位」的路徑,不是假設。verify_config_is_exist 只用設定編號查,完全沒有客戶條件)。急迫度不必再等修正棒查,已經確定:方向寫反是事實,實際部署也踩得到。
本條由工具的 F4/F5 兩個候選合併而成(決策者指示)。兩者是同一個缺口的兩個角度:F4 從 API 入口看,F5 從服務層看,指向同一條缺失的檢查。
這是什麼問題。 查詢某個任務的掃描執行紀錄時,系統只確認「你跟這個任務同一家客戶」,沒有確認「你是不是這個專案的參與者」。而同一個服務裡其他八個吃任務識別碼的功能(啟動、取消、重跑、刪除⋯⋯)每一個都做了這道檢查,只有查詢這支沒做。
出事會怎樣。 同一家客戶裡、沒被指派到某專案的人,可以讀到那個專案的完整掃描歷史:被掃描的主機位址與網段範圍、由哪台代理程式執行、錯誤訊息、是誰發動的、以及產出的弱點報告檔識別碼。而報告下載端點是「知道識別碼就能下載」的設計,所以把識別碼交出去,實質上等於把尚未修補的弱點清單交出去。限制是要先有一個有效的任務識別碼(UUID,無法從這支 API 枚舉)。
要先有什麼才打得到。
plugin 模組在哪裡。 API 入口 jedi_detection/api/routes/detection_tool_route.py:340(DetectionToolJobExecutionsRoute.get);實際缺失在 jedi_detection/app/service/detection_orchestration_service.py:1667(list_executions)。對照同檔有做檢查的八處::196/:871/:925/:1007/:1032/:1097/:1127/:1470,每一支都先呼叫 self._wf_svc.assert_project_participant(job.workflow_execution_id)。
怎麼修。 在 list_executions 開頭用 self._task_lookup.get_task(job_uid) 取出任務,再呼叫 assert_project_participant(job.workflow_execution_id),形狀與那八支完全一致;任務不存在時回 404,避免這支變成「這個識別碼存不存在」的探測管道。
驗證。 兩個候選各 3/3,三位檢查員一致確認成立,嚴重度均為 MEDIUM。
首腦核對註記。 屬實。⚠️ 注意修法落點跨棒:缺失的那行在 detection_orchestration_service.py,該檔屬 D3 範圍、不在本棒的 36 檔內(本棒只掃到 API 入口那一半)。開修正卡時範圍要涵蓋 D3 那個檔,或與 D3 的發現一起收。
有一個候選被三位檢查員一致駁回,記在這裡以免之後重複提報:
「解密後的掃描帳密會走未加密的 HTTP 送給代理程式」——研究員指出 agent_probe_client.py:83 確實是明文 POST,而加密只在「代理程式認證有啟用」時才開。但三位檢查員各自查證後都確認兩條觸發路徑都是關的:① 唯一的使用端(主專案 core/plugins/detection.py:318)是無條件接線的,「沒接線」這個分支不存在;② 安裝腳本 install.sh:1666 會寫入認證模式的預設值,「環境變數沒設所以退成不認證」這個分支也被出貨預設關掉了。判定為誤報,不列入發現。
🔴 低強度掃描只跑兩位研究員,卡片列的八個重點只碰到三個(①③⑤)。其餘五項由首腦逐項開檔查證如下。
工具報的兩條完整涵蓋。首腦另核對了卡片提到的「引用計數端點」(:144):確認同樣沒有權限檢查,但它只回一個數字(有幾個任務在用這個設定),資訊量低,不另列一條,建議與 F1 一起補守門。
設計自陳「加密存取/畫面顯示剝除/日誌剝除,漏一處等於零」,逐處核對:
detection_tool_service.py:83(新增)與 :104(修改)確實都走 self._crypto.encrypt(json.dumps(creds))。修改時是合併而非整包覆蓋(先解開舊的再蓋上新的),這是刻意的——前端對已設定的密碼欄位留空代表「沿用原值」,整包覆蓋會把沒重打的密碼洗掉。寫法正確。api/serializers/detection_tool.py:29-46 的回應格式完全沒有 credentials 欄位,只回一個 has_credentials 布林值。卡片擔心的 field_values 欄位混進帳密:核對 _resolve_credential_group_params()(:122)確認它只從資料庫的宣告組出參數、不吃前端傳來的參數鍵值,且 field_values 是非機敏設定值(服務網址、連接埠這類),機敏欄位一律走 credentials 那條獨立加密欄位。沒有混入。"剝除敏感參數 key=%s")。沒有任何一行日誌會印出帳密、請求內容或設定內容。三處防護都到位。任務層敏感參數(common/detection_secret_params.py)另外查證兩件卡片點名的事:
encrypt_secret_params() 的邏輯是「值本身已是加密信封 → 原樣保留不二次加密;值是空的 → 從舊資料沿用既有密文;值是新明文 → 加密」,三條路都不會產出明文。另外 crypto 沒注入時會記警告後原樣回傳而不是靜默落明文,設計上留了痕跡。strip_secret_params 只有一個呼叫點(detection_orchestration_service.py:2298),加密則在任務綁定處理器兩處(:155/:428)。decrypt_secret_params 在本套件(jedi-detection)內找不到呼叫者,但這是正常的、不是缺口——呼叫它的是主專案(BE)那一側的接線程式 infra/remote_agent/adapter/detection_task_payload_provider.py:60,在組派工單時把加密信封解開。套件只負責提供能力,由誰呼叫是宿主(主專案)決定,所以只在套件 repo 裡搜尋當然搜不到。「加密會做、解密沒人呼叫」這個結論不成立,這條路是通的。卡片最擔心的是「宿主不給 auth_required 時 35 條路由全裸」。查證結果:有攔截,這個擔心不成立。
plugin/assembly.py:66 在掛載路由之前先跑 _assert_wiring(adapters),而 auth_required 列在 plugin/contract.py:175 的 REQUIRED_WIRING 必填名單裡(同名單還有三軸守門、加解密、任務查詢共六項)。任一項缺了直接 RuntimeError 拒絕掛載,錯誤訊息明講「掛上去會讓檢測工具設定失去認證,故拒絕掛載」。mount_routes(bp, auth_required=None) 預設值是 None——確有其事(api/routing.py:6),且 _guarded_factory(None) 回傳的確實是不做任何事的 lambda cls: cls(:172-173)。但走正規組裝路徑永遠到不了那裡,因為 assembly.py:79 傳進去的值已經被必填檢查擋過一輪。這是「函式自己的預設值寬鬆、但唯一的呼叫者先把關」的形狀,不是漏洞。common/guard.py 的 _guard() 缺接線直接 RuntimeError ——如卡片所述是正面案例,確認做到,不列為漏洞。require_license 的兩顆資源字串(plugin/detection-profile)在 api/guards.py 有明確註記「字串值凍結,是對外契約」。確認無誤。DetectionToolRepoImpl 與 DetectionToolParamSchemaRepoImpl 兩個檔案都只有建構子、沒有任何自訂方法,領域服務那邊也只有 get_tool_by_uid/get_tool_by_id/get_tools/verify_tool_is_exist/get_current_schema 五個純讀取方法,沒有 create/update/delete,沒有任何 session.add/merge/flush。secret: true 標記」(這是決定哪些欄位要加密的唯一依據):只有能跑資料庫變更腳本的人,也就是部署者。產品裡沒有任何 API 改得動。這是安全的形狀。detection_tool/param_schema/job_execution_detection_tool/tenant_detection_tool_config)四個全部是每欄 Optional[X] = None+to_dict() 只回有值的條件,且每個檔案的註解都明寫「防幽靈 WHERE」。寫法一致、沒有例外。tool_params 這個欄位混雜明文與加密信封:確認設計如此,且判斷「要不要解密」是看值本身長不長得像加密信封,不是查參數格式表。這個設計讓格式改版後舊任務仍然解得開,是正確的取捨。plugin/contract.py:87-90 宣告四顆能力點設定槽:plugin_update、profile_create、profile_update、profile_delete。plugin.update 掛在檢測工具的新增/修改/重置三支;三顆 profile 能力點在 detection_profile_route.py:54-56 都有被使用(該檔屬 D2 範圍,本棒只核對「有沒有被用」不深入)。detection-profile.* 一顆沒掛」——查證後不成立,三顆都掛了。卡片這一點的斷言有誤,以本查證為準。plugin.update 沒掛滿:同一個資源上,新增/修改/重置有掛,測試連線(F1)、清單、引用計數三支沒掛。這與 FR-098 第 80 項「讀取端點不守門」是同款產品決策題,但測試連線不是讀取端點——它會解密憑證並發起對外連線,不該比照讀取端點放行,這是 F1 判 HIGH 的理由。002 的隔離規則:五條 FOR ALL 且都沒有 WITH CHECK——這點卡片要求「確認 PostgreSQL 語意不憑印象」:FOR ALL 若只寫 USING 沒寫 WITH CHECK,PostgreSQL 會把 USING 的條件同時當作 WITH CHECK 用,所以新增/修改不會因此變成無限制。缺 WITH CHECK 本身不是漏洞,真正的問題是 USING 的判斷式方向寫反(即 F3)。003/004 的寫入身分與冪等:兩支 seed 都不用 ON CONFLICT,而是用 INSERT ... SELECT ... WHERE NOT EXISTS 做冪等(工具用 code 反查、參數格式用「工具代碼+版本」反查),外鍵一律用代碼子查詢反查、不寫死任何 id。重複執行不會產生重複資料。寫入身分欄位(created_user/updated_user)一律填 NULL,代表系統寫入、非特定使用者假冒。寫法正確。005(選單路由):用 ON CONFLICT (route_id, capability_id) DO NOTHING 做冪等。正確。plugin/migrations.py 只提供不執行:確認該檔只負責把腳本檔案清單交出去,沒有任何實際執行 SQL 的程式碼,套用的決定權在宿主。這是正確的形狀(套件不該自己動客戶的資料庫)。detection_tools、detection_tool_param_schemas)依④的查證確認是碼表、無應用層寫入路徑、零隔離正確。其餘七張中 detection_profile_taxonomies/detection_profile_controls 屬 D2 範圍、job_execution_comments 等四張屬 flow-control 不在本套件,本棒不判定。DetectionAdapters 有沒有靜默降級:contract.py:119-126 明列「可省略(缺了優雅降級)」七項,逐項核對其降級後果——crypto 不在可省略名單裡,它在必填名單(REQUIRED_WIRING),所以卡片擔心的「crypto=None → 派工帶空憑證」不成立,缺了會直接拒絕掛載。可省略的七項降級後果都有明文記載且都是功能性降級(少個暱稱、少個下載識別碼、不產佐證),沒有任何一項降級會導致守門失效。18 票全投出、沒有漏投:F1/F2/F4(兩個候選)都是 3:0,F3 是 2:1 而異議在「實際部署有沒有母子結構」不在「規則寫法對不對」。另有一個候選被 3:0 一致駁回,顯示檢查員確實有在擋誤報而非照單全收。首腦逐條開檔核對(route 的守門宣告、服務層的解密位置、五條隔離規則的判斷式、八處對照的參與者檢查)全部對得上。
⚠️ 但沒有工具蓋的驗證章可對照(產物目錄被前一支 session 誤刪,見 Coverage 段)。上述票數取自工作流回傳的結果,內容完整未經改寫,但無法出示工具自己產的那份簽章檔。
最低強度快篩、兩位研究員、不做元件盤點不做威脅建模。卡片八個重點工具只碰到三個,其餘五個由首腦回頭開檔補查——人工查證的結論是五項都沒有新的資安漏洞(且推翻了卡片原先兩個斷言:profile 能力點沒掛、crypto 缺了會帶空憑證,兩者都不成立)。這些是人工補的結論,不是工具給的。
另外,本套件的程式碼註解極為詳盡且處處自我辯護(每個可疑設計都附一段說明為什麼安全)——這正是卡片預警「工具會被說服」的情境。首腦查證時一律以程式碼實際行為為準、不採信註解自陳,例如④是實際列出所有方法確認沒有寫入路徑,而非相信註解說它是碼表。
| 項目 | 數字 |
|---|---|
| 檢查範圍 | 36 個檔案(工具字典+租戶憑證+守門殼+五支資料庫腳本+插件組裝) |
| 檢查強度 | 最低(low),focus 設為 attack-surface |
| 候選問題 → 去除重複 | 8 → 6 |
| 投票數 | 18(6 條候選 × 3 位檢查員),全數投出 |
| 通過 / 駁回 | 5 / 1 |
| F1 / F2 / F3 / F4(原 F4+F5) | 3:0(HIGH)/ 3:0(MEDIUM)/ 2:1(MEDIUM)/ 3:0 與 3:0(MEDIUM) |
| 沒投到票的 / 投票中斷的 / 被降低嚴重度的 | 0 / 0 / 0 |
| 研究員派出 / 回收 | 2 / 2 |
| 驗證章狀態 | 無法出示(產物目錄被前一支 D1 session 誤刪,非平行掃描所致) |
| 掃描耗時 | 2541 秒(約 42 分),另含中斷後續跑 |
| 掃描當下的程式碼版本 | commit 6119da51d488,branch feature/review,工作區乾淨 |
| 掃描期間的版本漂移 | 結束時 HEAD 已為 82d78bc(三筆別線 commit,不在掃描範圍) |
| scan_id / workflow run | 9ea230c3 系列 / wf_f074881c-329 |
| 首腦人工補查項目 | 卡片八個重點逐項查證,②④⑥⑦⑧ 為純人工補查 |
| 中斷與續跑 | 面板投票階段遭 session 中止一次,以 resumeFromRunId 續跑補完,研究員結果走快取未重跑 |
agent_tasks 與三張授權表),要一次看完統一改,屬決策者裁示,runner 不自行處理。急迫度已由首腦 09-17 DEV 實查確認:母子單位結構存在、資料全在子單位,資安與功能兩面都已經在發作。decrypt_secret_params 無呼叫者infra/remote_agent/adapter/detection_task_payload_provider.py:60,不是功能缺口。修正卡本棒不開(決策者指示);後續統一派工,四條均已修(1.21.0 出貨)。