84ea07ef9fe286d6b3bc33569539cdc38dd2ccde(branch feature/FR-075)claude-security plugin v0.11.0,effort low、focus attack-surfaceverified(三個獨立檢查員對每條發現各投一票,30 票全數投出,沒有漏投)掃完了,範圍內找到 3 個真問題,最嚴重的是「任何登入者只要知道一個任務編號,就能讀走別家客戶的稽核證明清單」——而資料庫層完全沒有第二道防線。 另外工具在範圍外順手撈到 6 條密碼/金鑰被寫進版控的問題,全部都是已經開過卡的舊案,不算本棒的新發現。
現況(以 docs/security-report/M06*.md 為準):F1/F8(證明清單讀取,M06-1)→ ✅ 已修(CM-2059);F9(流程留言讀寫,M06-2)→ ✅ 已修(CM-2035);建議後續第 3 點的流程定義外洩併 P1(M06-8)已修。範圍外的憑證項(CM-1607/1608/1629/1631 系列):檔案殘留已清(CM-2048/2049/2051),密碼換發排在正式環境上版前。
稽核任務跑起來之後,使用者會在畫面上做四件事:把任務標成「完成」、把完成的任務「退回」、在流程上「留言」、替任務上傳「證明文件」。
這一棒檢查的就是:當這四個動作從網址送進後端時,後端有沒有問一句「你是不是這個專案的人」。 因為這些網址只帶一個編號(例如某個任務的編號),沒有帶「哪個專案」,所以如果後端不主動反查並比對,任何一個登入者都能把編號換成別人的,去動別人的資料。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 |
|---|---|---|---|---|---|
| F1 🔴 HIGH |
「查某個任務的證明清單」這支 API 沒有檢查你是不是這個專案的人,只檢查你有沒有登入 | 讀走別家客戶的稽核證明清單:檔名、描述、參考網址、Google 雲端硬碟連結、上傳者帳號、檔案指紋。拿到檔案編號後還能再去下載檔案本體 | ① 任一個有效登入帳號(不需任何權限) ② 知道一個任務編號(是亂數 UUID,通常要從被轉寄的連結、截圖、log 或匯出檔取得,猜不出來) |
app/flow_engine/service/job_evidence_service.py:48 |
在第 48 行取到 job_execution 之後、第 55 行查證明之前,補一行 self._workflow_execution_service.assert_project_participant(job_execution.workflow_execution_id)——同一個檔案的新增(:81)、更新(:145)、刪除(:168)都已經有這行了,只有「讀」漏掉 |
| F8 🟡 MEDIUM |
同一支服務裡「查單一筆證明」(get_job_evidences_uid)也沒有檢查歸屬 |
同 F1,但目前打不到——回傳前程式會先自己壞掉(見下方詳述),所以是「守門缺口」不是「資料外洩」 | 同 F1 | app/flow_engine/service/job_evidence_service.py:56(缺口本體在 :67 的 get_job_evidences_uid) |
在 get_job_evidences_uid 取到 job_evidence 之後補 assert_project_participant(job_evidence.workflow_execution_id) |
| F9 🟡 MEDIUM |
「流程留言」的讀與寫都沒有檢查你是不是這個專案的人 | 任何登入者可以對別家客戶的稽核流程塞留言、也能讀走整串既有討論。留言還會透過即時推播丟給該專案的正當成員,可以拿來假冒同事做釣魚(例如「請把稽核報告寄到 xxx@evil.com」)。留言存成 JSON 陣列且沒有筆數上限,也能被灌爆 | ① 任一個有效登入帳號 ② 知道一個流程編號 |
寫:api/flow_engine/routes/flow_engine_route.py:105(整支 put 從 :66 起)讀:同檔 :44-64 的 get |
在 app service 層(不是在 route)解出 workflow_execution 之後呼叫 assert_project_participant(workflow_execution.id),和同模組的「完成任務」(workflow_execution_service.py:655)、「退回任務」(:833)同一套做法。讀寫都要加 |
工具設了 focus 就會額外跑一次「找密碼金鑰」的專項掃描,它會掃全 repo、不受本棒 23 檔範圍限制。以下六條都不在本棒範圍內,且經比對全部落在既有卡片上:
| # | 內容 | 對應既有卡 |
|---|---|---|
| F2(HIGH) | GitLab 個人存取權杖被寫進對話紀錄檔(9 個檔) | CM-1607(清版控殘留金鑰) |
| F3(MEDIUM) | POC 資料庫密碼寫死在文件產生腳本裡 docs/system-design/scripts/generate_db_schema_docx.py:34 |
CM-1608/CM-1629 |
| F4(MEDIUM) | JWT 簽章金鑰被寫進對話紀錄檔(78 個檔含此字串) | CM-1607(FR-079 F12 已併入) |
| F5(MEDIUM) | OpenAI/Anthropic/Google 三家 API 金鑰被寫進對話紀錄檔 | CM-1607 系列(FR-079 B2 F2 已記) |
| F6(MEDIUM) | Google 雲端硬碟應用程式密鑰 + 保護權杖用的加密金鑰一起外洩 | CM-1631 |
| F7(MEDIUM) | Nexus 私有套件庫的 admin 帳密用 base64 寫死在前端打包腳本 scripts/build/build_fe_image.sh:51 |
CM-1634 系列(FR-083 D2 F2 已記同一組帳密) |
結論:範圍外這 6 條不需要開新卡。 但它們再次佐證同一件事——「把 .env 原樣貼進對話紀錄」這個習慣,讓同一批金鑰散進了幾十到幾百個檔案。
這是什麼問題(白話) 畫面上點開一個稽核任務,會看到「這個任務附了哪些證明文件」。後端提供這個清單的 API,只確認「你有登入」,沒有確認「這個任務是不是你們專案的」。
出事會怎樣 換一個任務編號就能看到別家客戶的證明清單,內容包含:檔名、描述、參考網址、Google 雲端硬碟的檢視連結、上傳者的帳號、檔案的 MD5 指紋、檔案編號。拿到檔案編號之後,還可以再去換簽章憑證把檔案本體下載下來。稽核證明常常裝的是內部組態、掃描報告、人員名單。
要先有什麼才打得到
3f2a…),猜不出來,實務上要從被轉寄的畫面連結、截圖、log 摘錄、匯出報表,或「曾經是這個專案成員但被移除」取得在哪裡
api/flow_engine/routes/job_evidence_route.py:25-30(GET /api/1.0/job-evidences?job_execution_uid=<編號>,只掛了 @jwt_required())app/flow_engine/service/job_evidence_service.py:42-59,第 48 行只用編號查出任務就往下走:81(新增)、:145(更新)、:168(刪除)都呼叫了 assert_project_participant怎麼修 在 get_job_evidences_by_job_execution_uid 第 48 行取到 job_execution、第 55 行查清單之前,插一行:
self._workflow_execution_service.assert_project_participant(job_execution.workflow_execution_id)守門政策在 common/authz/workflow.py,任何角色的專案參與者都放行、只擋非參與者,查不到專案就預設擋下來。
首腦核對註記(我自己開檔+查 DEV 資料庫確認)
job_evidence_route.py:28-29 確實直接用 request.args.get('job_execution_uid');job_evidence_service.py:42-59 確實沒有任何授權檢查,而 :81/:145/:168 三處確實都有,註解還寫著「擋跨專案 / 跨租戶猜 uid」——是漏掉不是刻意。compliance.job_evidences(848 筆)與 compliance.job_executions(11,065 筆)的「每個客戶只能看自己資料」隔離機制(RLS)開關是關的、規則 0 條。出貨基線 schema(scripts/init/02-schema.sql)全庫只有 31 張表開了這個機制,這兩張都不在裡面。這是什麼問題(白話) 和 F1 同一支服務裡,還有另一支「用證明編號查單一筆」的方法,一樣沒有檢查歸屬。
為什麼是 MEDIUM 不是 HIGH 因為目前打不到。三個檢查員都指出:api/flow_engine/routes/job_evidence_route.py:71 把「一筆」資料餵進了一個宣告成「多筆」的序列化器,而那個資料物件是純 dataclass、不能被逐項迭代,所以在資料送出去之前程式就會自己壞掉(工具原本另外報的 F10 就是因此被 3 票全數否決)。守門缺口是真的,資料外洩目前不成立。 但這是「靠一個 bug 擋住」,那個 bug 一被修好,缺口就變成真的外洩。
在哪裡 app/flow_engine/service/job_evidence_service.py:61-70(get_job_evidences_uid),route 在同檔 api/flow_engine/routes/job_evidence_route.py:59-72
怎麼修 和 F1 一起補:
self._workflow_execution_service.assert_project_participant(job_evidence.workflow_execution_id)首腦核對註記:✅ 開檔核對成立,get_job_evidences_uid 確實只查 uid 就回傳。序列化器不匹配這件事我讀了 route 第 70-71 行確認形狀對不上(單筆丟進 many=True),與檢查員說法一致。
這是什麼問題(白話) 稽核流程頁面上有一個討論區可以留言。留言的「寫」和「讀」兩支 API,都只確認你有登入,沒有確認這個流程是不是你們專案的。
出事會怎樣
要先有什麼才打得到 ① 任一個有效登入帳號 ② 知道一個流程編號(同樣是 UUID,要取得而非猜出)
為什麼是 MEDIUM 不是 HIGH 影響的是討論留言而不是證明文件本體,且同樣需要先取得編號。但如果把「假冒同事推播釣魚」算進來,實務衝擊不見得比 F1 低——這點我保留意見,交決策者判斷。
在哪裡
api/flow_engine/routes/flow_engine_route.py:66-121(PUT /api/1.0/flow-engine/process/comments/<id>),守門缺口在 :105 附近實際寫入處:44-64(GET 同一條路徑)app/flow_engine/service/workflow_execution_service.py:655、「退回任務」:833,兩支都有 assert_project_participant怎麼修 把守門加在 app service 層(不是 route,route 只負責 HTTP 邊界):解出 workflow_execution 之後呼叫 assert_project_participant(workflow_execution.id),讀寫兩條路徑都要。另建議替留言內容加長度與筆數上限。
首腦核對註記
flow_engine_route.py 的 WorkflowExecutionCommentRoute 的 get(:44)與 put(:66)都只有 @jwt_required(),route 內直接查 workflow_execution 再讀寫留言,全程沒有任何歸屬檢查。complete_job(:655)與 revert_job(:833)真的都有守門,所以「留言漏掉」是這個模組自己的不一致,不是設計上刻意不守。compliance.element_variables(3,236 筆)隔離開關關、規則 0 條;compliance.workflow_executions 有 3 條規則但開關是關的,等於規則寫好了沒生效。卡片列了 8 項思考起點,逐項交代(證偽和證實一樣重要):
| 卡片上的疑點 | 結果 |
|---|---|
| ① 流程留言 GET/PUT 零守門 | ✅ 成立 → F9。讀寫都沒守門,與「完成/退回」不一致 |
| ② 任務證明「讀」零守門、「寫」有守門 | ✅ 成立 → F1(清單)、F8(單筆)。寫的三處確實都有 |
③ job_evidences/element_variables 隔離關、零規則 |
✅ 成立,我用唯讀 SQL 在 DEV 實查確認(見上表)。job_executions 也是關的、零規則 |
| ④ 守門政策三個「不守」分支 | ⚠️ 部分推翻卡片的假設,需要決策者看一眼。common/authz/workflow.py:38 的註解寫「tenant admin 放行(RLS 仍限租戶)」,但我追到源頭 jedi-iam/jedi_iam/middleware/context.py:134 是 is_admin=user.is_super_admin——這個旗標實際上是「平台超級管理員」不是「租戶管理員」。所以卡片擔心的「租戶 A 的管理員能動租戶 B」不成立(因為根本不是租戶管理員)。但註解是錯的,而且它宣稱的兜底「RLS 仍限租戶」在這三張表上是空話(隔離根本沒開)。另兩個分支:「沒有登入身分放行」屬背景任務路徑(同 CM-1559 同源,已有卡);「 role_service 沒注入放行」我查了 DI 接線(di_containers/flow_engine/workflow_excution_containers.py:151),正式路徑有注入,不會走到這個放行分支 |
| ⑤ 反查鏈對「輪次主流程」恆回 None → 100% 誤擋 | ⚠️ 狀況屬實但沒有引發繞過。workflow_execution_service.py:137-153 確實在查不到時回 None,而守門對 None 是預設擋下來。我 grep 過全部呼叫端:只有 FR-050 的階段回退傳了 known_project_id 繞過反查鏈,而且它是傳「已解析出的正確專案編號」、不是可被外部操控的值,其餘呼叫端都沒有為了繞過而拿掉守門。沒有發現「因為必被擋所以有人把守門刪掉」的情況 |
| ⑥ 完成/退回端點的 body 沒有 schema | ✅ 讀過了,不構成安全問題。守門在 service 層(:655/:833)確實有;revert_to_job_id 指到別的流程的 job 時,workflow_execution_service.py:857 會因為在 BPMN 圖裡找不到節點而回 404(GRC_JOB_NOT_FOUND),不會亂跑;request.get_json() 遇到非 JSON 在 Flask 3.1 是回 400/415 而不是 500 |
| ⑦ 流程定義 GET 回整份範本 XML 給任何登入者 | ⚠️ 人工核對成立,但工具沒有報,故未經三人投票。flow_engine_route.py:29-37 只有 @jwt_required();套件端 jedi-flow-engine 的 get_workflow_template_by_uid(workflow_template_service.py:46-56)確實只用 uid 查、沒有租戶檢查;而 compliance.workflow_templates 在 DEV 實查是規則 4 條但開關關的(=規則沒生效)。結論:知道範本編號就能讀到別家客戶的流程圖(含自訂階段、角色、任務名稱)。 這條的責任層在套件(屬 P1 範圍),本棒只記錄、建議併入 P1 那一棒處理 |
| ⑧ 我的任務清單 GET | ✅ 這條沒問題。flow_engine_route.py:175-186 用的是登入者自己的 user.id,而 infra/readmodel/tasks/my_grc_jobs_query.py:74-77 的查詢確實是 VwUserJobQueue.user_id == user_id,使用者無法指定別人的 id。不是 FR-087 說的那 4 支「用擁有者身分繞過隔離」的 view 之一(那個問題的形狀是「view 沒有帶 security_invoker,導致查詢用 view 擁有者的身分跑」,這裡因為過濾條件綁死自己的 user_id,繞不過去) |
分兩層講,不要混在一起:
low,代表只派了一位研究員通讀全範圍(實際派 2、回 2,含密鑰專項那支),不是逐檔窮舉。coverage.research 是空的),所以「哪幾個檔真的被讀完了」無從查證。白話總結:報出來的問題是真的;但「掃過了」不等於「這 23 個檔乾淨了」。
照 stamp 的 verification 欄實抄:
| 項目 | 數值 |
|---|---|
status |
verified |
candidates |
11(去重後 10) |
panel_votes |
30 |
panel_quorum_findings |
9 |
researchers_dispatched / returned |
2 / 2 |
unreviewed_candidate_sites |
0 |
verificationRun |
1 |
lostCandidates |
無 |
severityLowered |
F3(HIGH→MEDIUM)、F4(HIGH→MEDIUM),皆由檢查員投票結果決定 |
| 掃描範圍 | 23 支檔(scopeFileCount: 23,與 git ls-files 對上) |
| effort / focus | low / attack-surface |
| 完整度檢查 | not-applicable(範圍掃描不做全樹盤點) |
| Run ID | wf_bd76357a-f9d |
| 耗時 | 約 3 小時 10 分(11,366 秒),32 個 agent 全數完成、0 失敗 |
沒有執行任何程式碼:所有發現都是讀程式碼推導出來的,沒有實際打過 API、沒有驗證過任何攻擊手法。對資料庫只做了唯讀查詢(確認隔離開關與筆數)。
jedi-flow-engine 套件,建議併入 P1 那一棒,不在本棒開卡。common/authz/workflow.py:38 把平台超管寫成 tenant admin)——衛生類,建議併入前述任一張修正卡順手改掉,因為這行註解會誤導下一個讀它的人以為有租戶兜底。job_evidences / job_executions / element_variables)——這是治本方向,但屬跨模組決策,且 workflow_executions 已經出現「規則寫好卻沒開開關」的狀況(FR-087 已記),建議與 FR-087 一起收口,不在本棒決定。