api/、domain/ports.py、plugin/ 五檔)ac9c152(branch feature/FR-075,工作目錄乾淨)claude-security plugin,effort low、focus attack-surfacewf_ca839a58-ac4掃完了,套件本體這 49 支檔裡沒有新的資安問題。唯一被三個檢查員一致確認的一條,是「流程留言不檢查你是不是這個專案的人」——但這條上一棒(H2)就已經查到並登記了,本棒只是從套件這一側再證實一次,不算新發現。
換句話說:這一棒的價值不在「又找到什麼」,而在把套件的每一支公開方法逐一對回主專案的守門,確認「宿主到底包了幾條、漏了幾條」。答案是:宿主只在「完成任務」「退回任務」兩條路上有守門,留言那條漏了(已登記),其餘公開方法目前沒有任何外部網址可以直接打到。
現況(以 docs/security-report/M06*.md 為準):P2-1(即第 51 條流程留言)→ ✅ 已修(CM-2035);未來陷阱的備用服務(M06-16)→ 🗑️ 已拆除(CM-2215,1.21.0)。
流程引擎這個套件,在系統裡的角色是「零件供應商」:它負責建立流程執行紀錄、把任務標成完成、算出下一個該辦的人是誰、讀寫流程上的留言與變數,然後存進資料庫。它自己沒有任何對外網址——所有請求都先打到主專案,主專案檢查完權限,才轉手叫這個套件。
所以這一棒問的是兩個問題:
另外,這次的範圍比卡片原訂多了 7 支檔——掃 P1 期間有平行工作把 plugin.py 拆成五個檔,還新長出了 api/ 目錄與 domain/ports.py。這些是全新程式碼、之前沒有任何一棒看過,本棒特別確認了它們。
| 編號 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 |
|---|---|---|---|---|---|
| P2-1 🟡 MEDIUM ⚠️ 重複,非本棒新發現 |
流程留言的讀與寫,只認呼叫者傳進來的流程編號,不檢查這個流程是不是他們專案的 | 任何登入帳號只要知道別家客戶的流程編號,就能讀走整串討論留言(含留言內容、留言者帳號與暱稱),也能用自己的名義往那個流程塞留言 | ① 任一個有效登入帳號(不需任何特殊權限) ② 知道目標流程的編號(UUID 猜不到,但會出現在網址與清單 API 回應裡,而且那張資料表沒開客戶隔離,本來就會外漏編號) |
套件側:jedi_flow_engine/app/service/element_variable_service.py:50(寫)、:26(讀)宿主側: api/flow_engine/routes/flow_engine_route.py:44-64(GET)、:66-121(PUT) |
在主專案的 app service 層解出 workflow_execution 之後呼叫 assert_project_participant(workflow_execution.id),讀寫都要加——與同模組「完成任務」(app/flow_engine/service/workflow_execution_service.py:611)、「退回任務」(:788)同一套做法 |
只有一條,而且是舊的。 這一棒沒有範圍外發現——工具因為設了 focus 會額外跑一次「找寫死的密碼金鑰」專項掃描(掃全 repo、不受 49 檔限制),這次在 jedi-flow-engine 目錄裡什麼都沒撈到。
白話說明
稽核流程的頁面上有一個討論區。使用者在上面留言時,瀏覽器會打一支網址,網址裡帶著「哪一個流程」的編號。後端拿到這個編號之後,直接照著它把留言讀出來或寫進去——中間沒有任何一步問「這個流程是不是你們專案的」。
在哪裡
套件側(本棒 scope 內)
jedi_flow_engine/app/service/element_variable_service.py:49-76 update_element_variable(寫)
jedi_flow_engine/app/service/element_variable_service.py:26-29 get_element_variable(讀)
——兩支都是拿 workflow_uid 直接查出 workflow_execution 就動手,零檢查
宿主側(本棒 scope 外,開檔核對用)
api/flow_engine/routes/flow_engine_route.py:44-64 GET(只掛 @jwt_required)
api/flow_engine/routes/flow_engine_route.py:66-121 PUT(只掛 @jwt_required)
di_containers/flow_engine/workflow_excution_containers.py:2 確認宿主接的就是這支套件 service
對照組(有守門的兩支)
app/flow_engine/service/workflow_execution_service.py:611 complete_job → assert_project_participant
app/flow_engine/service/workflow_execution_service.py:788 revert_job → assert_project_participant
怎麼修
守門加在主專案的 app service 層(不是 route 層——route 只負責處理網路請求的收發,查資料庫與判權限是 service 的事):解出 workflow_execution 之後呼叫既有的 assert_project_participant(workflow_execution.id),讀與寫兩條路徑都要加。不必新寫檢查函式,那支已經存在且同模組另外兩支正在用。
套件這一側也可以補第二道,但要小心形狀:如果加一張「權限檢查 port」(port =套件對外開的插槽,由主專案把實作插進來),沒插的時候必須直接拒絕、不可放行——插槽開著卻沒接,結果變成無聲放行,比不加還糟。
首腦核對註記(我自己開檔看過)
element_variable_service.py:49-76 整支從頭到尾只有「查流程 → 查變數 → 寫回」,沒有任何身分或歸屬判斷。讀的那支(:26)更短,連流程都不查,直接照 query 條件撈。flow_engine_route.py 的 WorkflowExecutionCommentRoute,get(:44)與 put(:66)都只掛 @jwt_required(),route 內直接查 workflow_execution 再讀寫留言,全程無守門。complete_job(:611)與 revert_job(:788)確實都呼叫了 assert_project_participant。所以「留言漏掉」是這個模組自己的不一致,不是刻意不守。scripts/init/02-schema.sql 裡,31 支「開啟客戶隔離」的指令中,workflow_executions/job_executions/workflow_templates/element_variables 一張都沒有。workflow_executions 雖然寫了三條隔離規則(:24160/:24167/:24174),但開關沒開,等於規則寫好了沒生效。卡片列了九項疑點(含首腦補充的兩項)。證偽與證實同樣有價值,所以不成立的也寫清楚是什麼擋住了。
| # | 卡片的疑點 | 結論 |
|---|---|---|
| 1 | 公開方法清單 vs 宿主守門對照表 | ✅ 做完了,見第 6 節。結論:套件三支 service 共 25 支公開方法,主專案真正接來用的只有 2 支 service(留言與任務查詢),且只有留言那 2 支方法有對外網址打得到。套件自己的 WorkflowExecutionService(含 complete_job/revert_job)主專案根本沒有 import——主專案有一支同名但完全獨立的自家 service,才是 route 真正呼叫的對象 |
| 2 | repo 注入可被無條件覆寫 | ✅ 確認機制存在,但無 request 期觸發路徑,不列為發現。詳見第 6.2 節 |
| 3 | 插件沒有 route 也沒有接線守衛 | ✅ 確認 blueprint 是空的。plugin/assembly.py:39 建的 blueprint 從頭到尾沒有 add_resource/add_url_rule,register() 也沒掛任何 resource。套件本身沒有對外網址 |
| 4 | 三張表的隔離狀態 | ✅ 屬實,用出貨基線 schema 核對(見上方核對註記)。四張表全部沒開客戶隔離。租戶欄是誰填的:套件四支 model 中只有 WorkflowTemplate 掛了租戶欄位的 mixin,其餘三張沒掛;element_variables 連欄位都沒有。有欄位的那幾張靠 jedi-common 在寫入前從登入身分自動填 |
| 5 | 流程變數是 JSONB 任意寫 | ⚠️ 機制屬實,但不另計為資安發現。element_variable_service.py:31-76 的 add/update 確實不驗 value 的形狀與大小,type 欄位也是呼叫端隨便填。但能打到這裡的唯一外部入口就是 P2-1 那支留言 API,所以「可以灌爆」是 P2-1 的一個後果,H2 報告已寫進第 51 條(「存 JSON 陣列無筆數與長度上限」)。delete_element_variable_by_main_workflow_execution_id(:82)全 codebase 零呼叫,目前沒有任何路徑叫得到 |
| 6 | 建立任務時把 BPMN 屬性當 JSON 解析 | ✅ 機制屬實,但是可用性問題不是資安問題。workflow_execution_service.py:118 的 json.loads(job.properties['devices']) 沒有 try,壞 JSON 會在啟動流程時丟例外。但寫得進 BPMN 範本的人本來就要有範本編輯權限,而且炸的是他自己啟動的那個流程,不擴散。on_job_created(:138-146)確實把整包資料交給主專案的 callback,但收的那一端是主專案自己的程式碼,不是外部輸入 |
| 7 | 完成任務的條件參數 condition_param |
✅ 不成立,有東西擋住。套件的 complete_job(:205)確實收這個參數,但主專案根本沒有 import 套件的這支 service(見第 6.1 節)。主專案 route 呼叫的是自家同名 service,那一支有 assert_project_participant 守門。所以「使用者可控嗎」的答案是:這條路目前沒有外部入口 |
| 8 | query entity 的 _gen_filters |
✅ 不成立。_gen_filters 不在套件裡,它在 jedi-common 的 base_repository_impl.py:459,邏輯是「欄位值不是 None 才加條件」——傳 None 確實等於不過濾。但用到它的兩支(get_non_completed_jobs/get_completed_jobs)套件內外都零呼叫,是死程式碼;實際在用的 get_job_executions 每次都帶著 workflow_execution_id。沒有 FR-079 F13 那種「宿主誤用成全表查詢」的情形 |
| 9 | plugin/ 五檔+api/+domain/ports.py(首腦補充) |
✅ 看過了,沒有發現。詳見第 6.3 節。api/guards.py 不是守門、domain/ports.py 的兩張插槽目前零使用且沒有「忘記接線就放行」的形狀 |
先講最重要的發現:套件裡有一支 WorkflowExecutionService(549 行,含 complete_job/revert_job),主專案裡也有一支同名的 WorkflowExecutionService(app/flow_engine/service/workflow_execution_service.py)。兩支是不同的東西,而主專案的 route 呼叫的是自己那一支。 全 codebase 搜尋確認:主專案從來沒有 import 過套件的 workflow_execution_service。
這件事直接決定了整張表的形狀——套件那 13 支「零授權檢查」的公開方法,目前沒有任何外部網址打得到。
| 套件的 service | 主專案有沒有接來用 | 有沒有對外網址打得到 | 守門狀況 |
|---|---|---|---|
ElementVariableService(留言/流程變數,7 支公開方法) |
✅ 有(di_containers/.../workflow_excution_containers.py:2) |
⚠️ 有,2 支:get_element_variable、update_element_variable 經留言 API |
🔴 零守門(= P2-1/第 51 條) |
JobExecutionService(任務查詢與增刪改,6 支公開方法) |
✅ 有宣告(同檔 :3、:113) |
✅ 無——容器裡建好了,但全 codebase 沒有任何地方取用這個 provider | 不適用(沒有入口) |
WorkflowExecutionService(流程執行,13 支公開方法含 complete_job/revert_job/start_workflow_execution) |
❌ 沒有——主專案用的是自家同名 service | ✅ 無 | 不適用(套件這支沒被接) |
WorkflowTemplateService(範本,P1 範圍) |
✅ 有(6 處 import) | — | P1 範圍,本棒不重審 |
這張表的三個含意:
complete_job/revert_job 零授權檢查是設計如此、宿主包一層」——實際情況比這更徹底:宿主不是包了一層,而是整支重寫了一份自己的。套件那份目前是沒人用的程式碼。JobExecutionService 是一個「接好了但沒人用」的插頭。今天無害,但如果哪天有人接上去用,update_job_execution/delete_job_execution 這些方法一樣是零守門、零租戶檢查。建議記一筆,未來要用時務必先補守門——這不是現在的漏洞,是未來的陷阱。app/wiring/__init__.py 的 set_repo_providers() 檔頭自陳「刻意採無條件覆寫,後注入者勝出」。核對結果:
global _providers = providers,沒有任何「已經設過就拒絕」的保護。程序內任何一次呼叫都能把四支資料庫存取元件整組換掉。plugin/wiring.py:24 的 bind_repo_providers(),由 plugin/__init__.py 在 import 期執行一次。全 codebase 沒有第二個呼叫點,也沒有任何網址或設定值能讓它在使用者請求的當下再跑一次。plugin/ 五檔、api/、domain/ports.py這 8 支檔之前沒有任何一棒掃過,逐一確認結果:
| 檔 | 是什麼 | 結論 |
|---|---|---|
plugin/__init__.py |
插件註冊入口(register())+對外 import 面 |
✅ 沒有長出 route。檔頭明寫「本套件目前沒有 api 層,這是刻意的」,且有測試焊死 blueprint 必須是空的 |
plugin/assembly.py |
建 blueprint | ✅ :39 建出來的 blueprint 之後沒有掛任何 resource。確認是空的 |
plugin/contract.py |
四個資料結構+常數 | ✅ 純宣告,無行為 |
plugin/runtime.py |
執行期組件的袋子 | ✅ 唯讀袋子,register() 期裝好 |
plugin/wiring.py |
把四支 repo 綁進 app 層 | ✅ 見 6.2,import 期執行一次 |
api/__init__.py |
HTTP 邊界的 import 面 | ✅ 只 re-export 一支 runtime()。沒有 route |
api/guards.py |
名字叫 guards,但它不是守門 | ⚠️ 命名容易誤導,但沒有安全問題。這支檔裡只有一支 runtime(),功能是「從 Flask 拿出啟動期裝好的組件袋」。檔頭自己寫清楚了「本套件沒有 route,故這裡也沒有守門 decorator」,並要求「日後長出 route 的那一棒要補守門,並同時在 plugin/assembly.py 補接線檢查——缺認證接線就拒絕掛載」。這個註記寫得對,方向也對(缺接線就拒絕=出錯時預設擋下來)。與主專案 common/authz/workflow.py 不是並存的兩套守門——因為這支根本不是守門 |
domain/ports.py |
兩張插槽(讀設定/發通知) | ✅ 目前零使用,且沒有「忘記接線就無聲放行」的形狀。兩張都是純功能性插槽(讀設定值、寄信),不是權限判斷——所以就算沒接,最壞情況是功能不動,不會變成放行。真正需要小心「沒接=放行」的是權限類 port,而這裡一張都沒有 |
分兩層問,答案不一樣。
被否決的那一條:有研究員報「流程變數這張表沒有掛租戶欄位的 mixin,而另外三張姊妹表有掛,所以它特別危險」。三個檢查員查了出貨基線 schema 之後一致否決:那三張姊妹表的客戶隔離開關其實也全是關的,所以根本不存在「別人有第二道防線、只有它沒有」這個反差。這個否決本身反而確認了一件事:四張流程相關的表在出貨基線裡,資料庫層一道隔離都沒有。
low 檔次:一輪研究員掃完 49 支檔,不做威脅建模、不跑第二輪廣度掃描。深度靠檢查員的三票補,廣度沒有第二次機會。coverage.research 是空的)。所以我不能宣稱 49 支檔每一支都被完整讀過——只能說範圍設定正確、研究員兩個都完整回報了。但有一個反向的佐證:卡片列的九項疑點,本棒每一項都有明確答案(成立/不成立/機制屬實但無入口),沒有一項是「沒讀到」。特別是第 7、8 項——那兩項的答案都是「這條路目前沒有外部入口」,而且是靠全 codebase 搜尋確認的,不是靠猜。這表示研究員確實把套件與宿主的接縫走過了一遍。
本棒沒有新發現,不需要開新卡。 唯一的 P2-1 已是總表 §3.1 第 51 條(決策者 2026-09-13 已裁升 HIGH、併入「程式層那批」一起開卡)。
補一句從套件側得到的證實:「套件層 element_variable_service.py:26/:50 同樣零檢查,所以不存在『套件有擋』的可能,守門必須加在主專案 app service 層。」 這句話會讓修正卡的工程師少查一輪。
JobExecutionService(套件的任務增刪改)已經在主專案的 DI 容器裡建好,但目前沒有任何地方取用。它的 6 支公開方法零守門、零租戶檢查。今天無害,但哪天有人把它接上去用,就是一組現成的無守門端點。建議登記在總表 §3.3(掃描補洞類)或 §3.2,內容:「接好了但沒人用的插頭,未來啟用前必須先補守門。」
照 stamp(CLAUDE-SECURITY-REVISION-ac9c1522c28b.json)的 verification 欄實抄:
| 項目 | 值 |
|---|---|
status |
verified(面板完整跑完) |
candidates |
3(研究員提出的原始候選) |
candidates_deduped |
2(去重後真正送審的) |
panel_votes |
6(2 條候選 × 3 個檢查員,全數投出,沒有漏投) |
panel_quorum_findings |
1(達到票數門檻、進報告的) |
panel_reviewed_findings |
1 |
unreviewed_candidate_sites |
0(沒有任何候選沒被審到) |
incomplete_panel_candidates |
0 |
researchers_dispatched / returned |
2 / 2(派出兩個研究員,兩個都完整回報) |
| 嚴重度降級 | 無(severityLowered 空) |
| 驗證輪數 | 1(一輪跑完,沒有續跑) |
| 耗時 | 3 小時 21 分(12,089 秒) |
| scope 檔數 | 49(git ls-files 事前對過) |
| 掃描版本 | ac9c1522c28b4d3514897a3ae8d6b47d53ef8209,工作目錄乾淨(dirty: false) |
投票明細:
工具產物落點(在 jedi monorepo 內,該目錄自帶 .gitignore 不入版控): /Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-flow-engine/CLAUDE-SECURITY-20260913-121441/
scripts/init/02-schema.sql 得到的,沒有連線到任何環境,更沒有任何寫入。feature/FR-075。