FR-088.H4 掃描報告:流程執行宿主服務與接線(主專案 flow_engine)

  • 卡片:CM-1674
  • 掃描範圍:主專案 app/flow_engine/service/workflow_execution_service.py(1,559 行)+ DTO/enums + DI 接線 + domain/infra 的 ext_workflow_execution 與 control_mapping,共 21 檔
  • 版本:commit 8fc8c1c1(BE repo,branch feature/FR-075,工作目錄有未提交異動故 stamp 標 -dirty)
  • 日期:2026-09-13
  • 工具:claude-security plugin,low effort,focus attack-surface
  • 面板:9 條候選 × 3 位檢查員 = 27 票全數投出,8 條成立、1 條被三票否決,stamp verified
  • run ID:wf_ba3ef1cb-e96

1. 🔴 一句話結論

流程討論區的門沒鎖:任何一個能登入的帳號,只要知道某個流程的編號,就能把別家客戶(或自己已經被移出的專案)的稽核討論整串讀走,還能用自己的名字往裡面貼話,貼完立刻推播給該專案所有線上的人。

第二件事比較細但同樣真實:專案裡「只給看、不給動」的角色,其實可以把別人的稽核任務標成完成、或把流程退回上一關——系統只檢查「你是不是這個專案的人」,沒檢查「這件事是不是你的」。走批次那條路會被擋,走單筆這條路不會,兩條路的規矩不一樣。

另外掃出六條密碼與金鑰被寫進版控檔案的問題。這六條不在這一棒的範圍內(在 docs/ 與 scripts/ 下),是工具的密鑰專項掃出來的,本報告照實列出但不計入本棒範圍發現,請與既有的憑證卡比對是否重複。其中我逐把比對過本機 .env:Google API key 與 Google OAuth client secret 至今未換、仍是活的。


現況(以 docs/security-report/M06*.md 為準):H4-1(討論區,M06-2)→ ✅ 已修(CM-2035);H4-2(完成/退回,M06-7)→ ✅ 已修(FR-114.1-1/FR-114.1-2,CM-2119);範本 XML 寫回母版(M06-15)→ 🚫 裁定不修;H4-3(資料庫隔離開關)→ ✅ 已修:projects/workflow_executions 隔離已於 2026-09-14 開啟(FR-094 CM-1768/1772,內化版 M20-1);AI 儀表板列專案把呼叫者當管理員那一半另由 M15-3(SUMMARY #61)修好,均隨 1.21.0 出貨。範圍外的憑證項(CM-1607/1608/1629/1631 系列):檔案殘留已清(CM-2048/2049/2051),密碼換發排在正式環境上版前。

2. 這一棒在檢查什麼(白話)

稽核流程「跑起來」的所有事情,都在主專案這支 1,559 行的程式裡:開始一個稽核流程、幫每個關卡建任務、任務完成後決定下一個換誰、寄通知、把任務跟控制項對起來。套件只提供零件,真正的規則寫在這裡。

這一棒要回答三個問題:

  • 守門有沒有每條路都包住——會不會有某個入口忘了檢查「你是不是這個專案的人」
  • 給 AI 儀表板用的三支查詢,會不會漏掉「只能看自己租戶」的過濾
  • 通知信裡會不會帶出不該帶的內容、會不會寄給別家客戶的人

只找問題、不修問題——底下所有修法都只是建議,這一棒沒有改任何一行程式碼,也沒有連任何資料庫、沒有真的打任何一支 API 去測,全部是讀程式碼推論出來的。


3. 掃到什麼:總覽表

3.1 本棒範圍內(21 檔)

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 嚴重度 來源
H4-1 **流程討論區完全沒有檢查「你是不是這個專案的人」 任何登入者能讀走整串稽核討論、並用自己名字貼假訊息,貼完即時推播給該專案所有線上成員 任何有效帳號 + 知道一個流程編號 api/flow_engine/routes/flow_engine_route.py:105(GET 在 :55 同樣沒守門) MEDIUM** 工具 3/3 票 + 本棒開檔核對
H4-2 **完成/退回單筆任務只檢查「是不是專案成員」,沒檢查「是不是你的任務」 定義為「只能看」的 viewer 也能把別人的任務標完成、把流程退回上一關,稽核紀錄上的經手人變成他 是該專案的參與者(任何角色)+ 知道流程編號與任務編號(一般列表 API 就拿得到) app/flow_engine/service/workflow_execution_service.py:655(complete_job)、:805(revert_job) MEDIUM** 工具 3/3 票 + 本棒開檔核對
H4-3 資料庫沒開「每個客戶只能看自己資料」的隔離,但程式有一條路刻意信任它 AI 儀表板列專案清單那條路會撈到別家客戶的專案 多租戶部署下的任何登入者 + 走到那條路 scripts/sql/2026-06-25-align-poc-to-stg-view-eventtrigger-projects-rls.sql:124;配套 app/flow_control/service/project_service.py:224 MEDIUM 工具 3/3 票 + 本棒開檔核對(僅核對版控內的 SQL,沒查線上資料庫)

3.2 範圍外(密鑰專項掃出來的,不計入本棒發現)

工具設了 focus 就會多跑一次密鑰專項,它讀的是整棵樹,所以以下六條都落在 docs/ 與 scripts/,這一棒的 21 檔沒有涵蓋這些區域。列出來是因為是活的憑證,請先與既有憑證卡(CM-1607/1608/1629/1631)比對是否重複。

# 這是什麼問題 出事會怎樣 在哪裡 嚴重度
外-1 OpenAI/Anthropic/Google 的 API 金鑰、Google 登入用的密鑰被整份 .env 抄進對話紀錄檔 別人可以拿我們的帳號去用 AI 服務(帳單算我們的)、冒用我們的 Google 登入身分 docs/conversation-history/2026-04-28-to-04-30-survey-answer-arc/part-verbatim-01-of-03.md:3651 HIGH
外-2 GitLab 權杖、Gmail 寄信密碼、MinIO 金鑰、公司網域帳號密碼被整張 DB 表抄進對話紀錄檔 別人能讀我們的原始碼倉庫、用我們的名義寄信(拿去釣魚很像真的)、開我們的檔案儲存、查公司通訊錄 docs/conversation-history/2026-04-28-to-04-30-survey-answer-arc/part-verbatim-03-of-03.md:5933 HIGH
外-3 登入用的簽章金鑰被抄進對話紀錄檔 知道這把金鑰就能自己做一張假的登入通行證,冒充任何人(含管理員),等於完全繞過登入 docs/conversation-history/2026-04-21-google-drive-integration/2026-04-21-google-drive-integration-part-03-of-14.md:14232 HIGH
外-4 資料庫密碼寫死在 249 個 版控檔案裡 拿到 repo+連得到內網的人,能用「可以看所有客戶資料」的那個帳號直接連資料庫 docs/claude/memory/project_drive_sync_test_data.md:18 等 249 檔 MEDIUM
外-5 管理員密碼寫死成 seed 腳本的預設值 沒改過密碼的環境會被用這組直接登入成平台管理員 scripts/seed_2026-08-01_fr059_detection_profiles.py:65 等 5 檔 LOW

(外-1~外-3 是同一類問題的三個不同檔案,都是「把 .env 或 DB 查詢結果整段貼進對話紀錄」造成的,根因相同。)


4. 每條發現的詳述(本棒範圍內)

H4-1(MEDIUM)流程討論區沒有任何守門

這是什麼問題(白話)

每個稽核流程底下有一個討論區,成員在裡面留言討論。讀留言與寫留言走同一條網址、同一個程式檔。

問題是——這兩個動作都只檢查「你有沒有登入」,從來沒檢查「你是不是這個專案的人」。

你拿自己的帳號,報上別人流程的編號,它就把整串討論給你;你想貼話,它也讓你貼,而且貼完會立刻透過即時推播(Socket.IO)送到該專案所有正在線上的成員畫面上。

出事會怎樣

  • 稽核討論內容通常是最敏感的——誰質疑了什麼、哪個控制項有問題、內部怎麼喬——整串被外人讀走
  • 攻擊者能以自己的真實姓名貼話進去(留言會帶 user_nickname 與 user_name),對該專案成員來說看起來就是「有個不認識的人突然在我們的討論區發言」,或更糟:貼假的稽核結論混淆判斷
  • 因為 compliance.workflow_executions 這張表也沒開客戶隔離(見 H4-3),資料庫層面連「跨租戶」都擋不住

要先有什麼才打得到

  • 一組有效的登入帳號(任何已登入帳號都行,不需要是該專案的人;已經被移出專案的前成員也算)
  • 知道目標流程的編號(uid)。取得方式:曾經是該專案成員時記下來、從分享的網址或截圖、或從其他 API 回應外洩

在哪裡

api/flow_engine/routes/flow_engine_route.py:55    ← GET(讀留言),只有 @jwt_required()
api/flow_engine/routes/flow_engine_route.py:105   ← PUT(寫留言),只有 @jwt_required()
app/flow_engine/service/workflow_execution_service.py:1181  ← get_workflow_execution(),本身不做任何授權

本棒開檔核對結果:屬實。 我逐行讀過 flow_engine_route.py:55-122 這整個類別,兩個 verb 都是「拿網址上的 id → get_workflow_execution() → 直接讀寫留言」,中間沒有任何一行呼叫 assert_project_participant。對照同一支檔案的 complete_job(:123 起)與 service 層的 :655,那條路有這道守門且註解寫明「C16:非該專案參與者不可 complete job(擋跨專案猜 id)」——同一個風險已經被想過、被擋過,只是留言這條路漏了。

怎麼修

在 GET 與 PUT 兩個 verb 解出 workflow_execution 之後、動留言之前,補上與 complete_job 相同的那一行:

workflow_execution_service.assert_project_participant(workflow_execution.id)

(assert_project_participant 是既有的 public method,在 app/flow_engine/service/workflow_execution_service.py:122,docstring 明寫「public — 供其他 service 重用同一守門」,不需要另外寫新的。)非參與者會被既有的 GRC_NOT_PROJECT_PARTICIPANT 擋成 403。

比較乾淨的作法是把留言讀寫下沉到 app service 裡再守門,這樣 route 層也不用碰 session;但若要最小改動,補這一行就能關掉這個洞。


H4-2(MEDIUM)viewer 也能完成或退回別人的任務

這是什麼問題(白話)

專案裡的人有不同角色,其中 viewer 的定義是「只能看,沒有待辦」。

但完成任務這個動作,只檢查「你是不是這個專案的成員」,沒有再檢查「這件任務是不是指派給你的」。所以 viewer 可以把別人還沒做的稽核任務標成「已完成」,流程就往下一關跑了;退回任務(revert_job)是同一道守門,一樣可以。

出事會怎樣

  • 稽核流程的可歸責性壞掉:任務完成紀錄上的經手人(updated_user)寫的是攻擊者,但這個人本來沒有執行該任務的資格。事後追查會看到一筆「某某人完成了不該他做的事」,而系統當時是允許的
  • 連帶改寫 job 狀態、問卷狀態(TaskSurveyStatusCode.EDITING)與整個 workflow 狀態
  • 可以把已完成的關卡退回,干擾正在進行的稽核

要先有什麼才打得到

  • 攻擊者是該專案的參與者,角色不拘(viewer/auditor/reviewer 都行,不必是 manager,也不必是該任務的指派人)
  • 知道流程編號與 BPMN 任務編號——同專案成員從一般的列表 API 就正常取得

為什麼是 MEDIUM 不是 HIGH:攻擊者必須已經是這個專案的成員(外人打不到),影響限於該專案內部的流程完整性,不是資料外洩。

在哪裡

app/flow_engine/service/workflow_execution_service.py:655   ← complete_job,守門只有 assert_project_participant
app/flow_engine/service/workflow_execution_service.py:805   ← revert_job,同一道守門
common/authz/workflow.py:28-49   ← 該守門的政策:任一角色放行、只擋非參與者
api/flow_engine/routes/flow_engine_route.py:136-146  ← 參數來自 client payload

本棒開檔核對結果:屬實。 common/authz/workflow.py:46-49 的邏輯是 role = role_service.get_user_role(...),然後 if role is None: raise——只要查得到任何角色就放行,完全不看角色是什麼。complete_job 在 :655 呼叫它之後,下一段(:669-673)就直接把狀態寫成 COMPLETED,中間沒有第二道檢查。

工具提到「走批次端點 /grc/jobs/batch-complete 做同一件事會被 NO_PERMISSION 擋下」,這一點我沒有開檔核對(批次端點不在本棒 21 檔內)。若屬實,代表同一件事兩條路徑規矩不一致,那本身就是該收斂的設計問題;請在開修正卡時一併查證。

怎麼修

在 complete_job 與 revert_job 的 assert_project_participant 之後,加一道「是不是你的任務」的檢查:比對呼叫者是該 job 的 task_assignee,或是該專案的 manager,都不是就 raise ForbiddenError。可重用既有的 participant_role_service.get_user_role(已注入)與 task_assignee_domain_service(已注入,_resolve_project_id_from_workflow 就在用),不需要新增 service。

如果產品真的要讓所有參與者都能代為完成,那應該寫成明確的角色白名單(例如「manager/auditor 可代完成,viewer 不行」),而不是現在這種「任一參與者皆可」的隱性放行——後者讓「viewer 是唯讀」這個產品承諾變成假的。這一條要先問決策者是哪一種意圖,再決定怎麼改。


H4-3(MEDIUM)資料庫的客戶隔離沒開,但程式有一條路信任它

這是什麼問題(白話)

資料庫有一個機制叫 RLS(就是「每個客戶只能看自己資料」的資料庫層隔離)。它要兩件事同時成立才有效:規則要寫好,而且開關要打開。

現在的狀況是:規則寫好了,開關沒開,甚至有一支 migration 明確把它關掉。而程式裡有一條路(AI 儀表板列專案清單)刻意跳過應用層的可見性過濾,註解直接寫「tenant 隔離由 RLS 負責」——它把責任交給一個沒打開的機制。

出事會怎樣

B 客戶的使用者呼叫 AI 儀表板的專案列表,拿到 A 客戶的專案清單(名稱、狀態、metadata)。

要先有什麼才打得到

  • 多租戶部署(單客戶自帶環境不受影響)
  • 一個登入帳號,走到那條 is_admin=True 的路
  • 線上資料庫的狀態與版控內的 SQL 一致

在哪裡

scripts/sql/2026-06-25-align-poc-to-stg-view-eventtrigger-projects-rls.sql:124
    ALTER TABLE compliance.projects DISABLE ROW LEVEL SECURITY;
scripts/init/02-schema.sql   ← workflow_executions_* 政策寫了 46 處,但整份檔案沒有一行對 projects/workflow_executions 下 ENABLE ROW LEVEL SECURITY
app/flow_control/service/project_service.py:218-225   ← 註解「tenant 隔離由 RLS(session_scope)負責」,並傳 is_admin=True

本棒開檔核對結果:版控內的 SQL 屬實。 我做了三件核對:① sed -n '118,130p' 看到那句 DISABLE ROW LEVEL SECURITY 就在第 124 行,註解寫「compliance.projects RLS 以 STG 為主 → 關閉」;② grep 'ENABLE ROW LEVEL SECURITY' scripts/init/02-schema.sql | grep -iE 'projects|workflow' 零筆,同檔卻有 46 處 workflow_executions_ 政策定義——政策寫了但開關沒開,等於那些規則現在是空轉的;③ project_service.py:218-225 的註解與 is_admin=True 都在,與工具說的一致。

🔴 這條的限制要講清楚:我沒有查任何一個線上資料庫(唯讀查詢是允許的,但本棒定位是讀程式碼,且線上狀態可能已被後來的操作改過)。所以這一條成立的是「版控裡的 SQL 是這個狀態」,不是「DEV/STG/POC 現在確實沒開」。開修正卡前應先跑一次 SELECT relname, relrowsecurity FROM pg_class WHERE relname IN ('projects','workflow_executions') 確認三個環境的實況——這屬唯讀,不算環境異動。

怎麼修

兩件事要分開做,都做才算修完:

  1. 把開關打開:對 compliance.projects 重新啟用、對 compliance.workflow_executions 與 compliance.workflow_templates 首次啟用(它們的政策已經寫好、現在是空轉的),然後用 pg_class.relrowsecurity 驗三個環境。⚠️ 打開隔離可能讓某些現在「看得到」的查詢突然看不到,這是行為變更,要先在 DEV 驗過。
  2. 不要把信任全押在資料庫開關上:get_projects_for_dashboard 應該在查詢裡自己加租戶條件,而不是傳 is_admin=True 然後期待資料庫兜底。一支 migration 就能把兜底關掉——這次就是這樣發生的。

5. 卡片「重點看什麼」的逐項回應

卡片列了八個重點,以下逐項給結論。證偽與證實同樣重要,不成立的我寫清楚是什麼擋住了。

卡片項目 結論 說明
① 向 AI 儀表板申報的三支查詢有沒有漏租戶過濾 ⚠️ 部分成立,但破口不在 </strong>kwargs 開檔核對:di_containers/dashboard_apis/flow_engine.py 申報的三支是 get_main_workflow_executions / get_sub_workflow_executions / get_workflow_executions(:1157/:1162/:1176),三支都是 query_entity 形狀、不是 **kwargs——卡片擔心的「申報單沒宣告就被丟掉」(FR-079 F13 同款)在這三支身上不成立**。真正的 <strong>kwargs 兩支(:1123/:1140,帶 _and_pager)並未申報給儀表板**。但這三支確實沒有任何租戶或專案過濾,而 workflow_executions 表的隔離也沒開(見 H4-3),所以破口是真的、成因不同:不是參數被丟掉,是從頭到尾就沒有過濾條件,全靠資料庫兜底而兜底沒開。這一點併入 H4-3 理解,不另計一條。
② 守門與反查鏈:known_project_id 是否都由伺服器端解析 ✅ 沒問題 grep 全 repo,非測試的呼叫者只有 app/flow_engine/service/stage_rollback_service.py:160 一處。開檔看 :172-180:project_id 來自 _get_user_role(),它拿 project_uid 去 project_domain_service.get_one() 解出來的,是伺服器端解析,不是請求可控。bypass_round_frozen_gate=True 同樣只有這一個呼叫者。
③ 輪次凍結檢查 fail-open ⚠️ 屬實,但影響低於預期 :155-176 確實三種情況全放行(沒注入 round service/解析不到 project/無輪次),註解自陳「主 gate 在顯示層」。但這道 gate 擋的是「規劃期才可退回」的流程時序,不是身分授權——assert_project_participant 那道是獨立的、且不受 bypass 影響(revert_job docstring 明寫這點,我核對 :834-839 屬實)。所以 fail-open 的後果是「本該凍結的輪次仍可退回」,而能做這件事的人已經被 H4-2 的守門缺陷涵蓋。單獨看不值得另開卡,修 H4-2 時一併把這裡收緊。
④ 啟動流程時改寫範本 XML:改的是凍結副本還是母版 🔴 改的是母版 :426 呼叫 _patch_template_xml_job_uids(workflow_template, ...),而 :436 直接 workflow_template_domain_service.update_workflow_template(workflow_template)——寫回的是 workflow_template 本身,不是流程實例的副本。意思是每啟動一次流程,就把 job uid 蓋回共用的範本上。這不是資安漏洞(寫進去的是系統自己產的 uid,不是使用者輸入),但是設計上的疑慮:多個流程共用同一範本時會互相覆蓋(_inject_job_execution_uid 會先移除舊值再寫新值,:463)。建議當作功能面 bug 記錄,不列資安發現。
⑤ params: dict 怎麼進到流程變數 ✅ 讀過,沒報 start_workflow_execution(:179)的 params 進到 element_variable 存放,型別是結構化的 dict/JSON,沒有被拿去拼字串或當程式執行。沒發現問題。
⑥ 通知內容會不會外洩、會不會寄給別租戶的人 ✅ 沒問題 逐支開檔:notify_users_batch_assigned(:1265)只帶暱稱與任務「數量」,不帶任務內容;notify_control_reviewers_on_task_complete(:1380)帶暱稱、任務名稱、系統網址,不帶留言原文。收件人來源是 get_control_participants_with_inherits(project_id=...) 再過濾 manager/reviewer,是從專案參與者往下查,不會撈到別專案的人。⚠️ 附註:notify_control_reviewers_on_task_complete 的 docstring 自陳「目前無呼叫點」(CM-1504 退役死分支後成孤兒,刻意保留待 2B 重接)——所以這支現在不會執行。
⑦ 問卷屬性:指到別租戶問卷 uid 會怎樣 ⚠️ 讀過,無法在本棒下結論 process_survey_property(:1466)從 BPMN 任務屬性取 survey_uid,直接餵給 task_survey_service.link_task_survey(survey_uid=...),本檔內沒有任何驗證。但 task_survey_service 不在本棒 21 檔範圍內,驗證有沒有在那一層做,我沒有讀到。這是「沒讀到」不是「沒問題」,建議列入下一批掃描範圍。
⑧ 裸的 add/update/delete_workflow_execution 有沒有 route 直達 ✅ 沒問題 :1208/:1217/:1227 三支確實沒有守門。但 grep 全 repo(workflow_execution_service.(add|update|delete)_workflow_execution)零個外部呼叫者——沒有任何 route 或其他 service 呼叫它們,目前是死碼。風險是潛在的(將來有人接上 route 就立刻變成洞),建議加註解或直接移除,但不列為本棒發現。
補充:控制項對應表的查詢有無範圍條件 ⚠️ 讀過,沒報 infra/flow_engine/repository/workflow_execution_control_mapping_repo_impl.py 與相關 domain service 讀過,查詢依 workflow_execution_id/control_id 過濾,沒有租戶條件——與 H4-3 同一個病(靠資料庫兜底),不另計。ext_workflow_executions 表的隔離狀態沒有查線上資料庫(同 H4-3 的限制)。

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

分兩層看,這兩層的可信度不一樣:

第一層:「這幾條存在嗎」→ 可信度高

  • 面板(就是三個獨立的檢查員各自從零讀程式碼再投票)完整跑完,沒有中斷、沒有撞額度:9 條候選 × 3 位檢查員 = 27 票全數投出,沒有漏投
  • 8 條三票全過、1 條三票全否(被否的那條沒有列進報告,所以編號會跳過 F9)
  • 本棒範圍內的三條(H4-1/H4-2/H4-3),我自己開檔逐行核對過,不是只看工具怎麼說——核對過程寫在各條的「本棒開檔核對結果」段
  • H4-3 有一個明確的打折:只核對了版控裡的 SQL,沒有查任何線上資料庫,所以它成立的是「程式碼與 migration 是這個狀態」,不是「DEV/STG/POC 現在確實沒開」

第二層:「只有這幾條嗎」→ 不能這樣說

三個理由:

  1. low effort 是一位研究員讀完全範圍,不是逐檔窮舉。1,559 行的單檔是本 arc 最大的一棒,一次讀完必然有取捨
  2. 卡片重點⑦(問卷 uid 跨租戶)我沒讀到答案——驗證邏輯在範圍外的 task_survey_service,這是「沒讀到」不是「乾淨」
  3. 零發現不等於乾淨。本報告第 5 節刻意把「讀過但沒報」(⑤⑥)與「根本沒讀到」(⑦)分開寫,就是為了不讓後者被誤當成前者

另外,範圍外那六條憑證發現(第 3.2 節)雖然是真的,但不代表那些區域被掃過——密鑰專項只找符合金鑰特徵的字串,不做其他分析。


7. 執行概況(工程師看的,數字照 stamp 實抄)

項目 數值
verification.status verified
候選數(candidates) 9(去重後仍 9)
面板票數(panel_votes) 27(9 × 3)
達到共識的發現(panel_quorum_findings) 8(全部 3/3 一致,無 2/3 勉強過關者)
研究員派出/回報(researchers_dispatched / returned) 2 / 2(1 位範圍研究員 + 1 次密鑰專項)
驗證輪數(verificationRun) 1(沒有候選被交棒到第二輪,continued: 0、lostCandidates: [])
嚴重度被面板下修的 0 筆(severityLowered: [])
agent 總數 29 派出 / 29 完成 / 0 失敗
掃描耗時 約 4 小時 51 分(17,469,602 ms)
scope 檔數 21(啟動前 git ls-files 驗過,對上卡片)
run 產出目錄 CLAUDE-SECURITY-20260913-064104/(stamp:CLAUDE-SECURITY-REVISION-8fc8c1c1dd45-dirty.json)
run ID wf_ba3ef1cb-e96

沒有執行任何程式碼、沒有連任何資料庫、沒有打任何 API、沒有驗證任何攻擊手法——所有發現都是讀程式碼推論出來的。


8. 建議怎麼開卡

本棒範圍內(三張):

  1. H4-1 流程討論區補守門——單檔兩行,改法明確,可直接派 runner。建議優先,因為它是三條裡唯一「外人打得到」的。
  2. H4-2 完成/退回任務補「是不是你的任務」檢查——要先問決策者意圖(是漏了,還是產品刻意讓所有參與者代為完成?),確定方向再動手。修的時候把重點③的 fail-open 一併收緊。
  3. H4-3 資料庫隔離開關——開卡前先唯讀查三個環境的實況。這條牽涉 DB 行為變更,且修法分兩半(開開關+程式不再依賴開關),建議寫成一張卡兩個子項。

功能面(非資安,一張):重點④範本 XML 寫回母版而非副本,多流程共用時會互相覆蓋。

要補掃的(記入覆蓋率缺口,不開修正卡):重點⑦問卷 uid 的跨租戶驗證在 task_survey_service,本棒沒讀到,列入下一批範圍。

範圍外六條:先與 CM-1607/1608/1629/1631 比對;未重複的部分,外-1 的 Google API key 與 Google OAuth client secret 經逐把比對確認至今未輪替、仍是活的,這件事本身就該先處理,不必等開卡流程。