本卡屬 FR-114 資安修正(母卡 CM-2019),b4 驗證 V2(CM-2265)第二輪用子租戶帳號挖到、首腦在 190
pg_policies親自核過的新洞,收集卡 CM-2231 #38。三個套件的 migration+一支主線 migration+一支守衛測試。方向是子看父,落地版通常單一頂層租戶所以打不到,SaaS 多租戶才打得到;依「落地版優先、SaaS 預留正確形狀」修法要對,不擋 b5 出包但進 b5。
租戶有樹狀關係,每個租戶有一條路徑,例如母公司 Billows 是 /1/3/、子公司是 /1/3/4/。資料庫的資料列隔離(RLS)大多數表用的是「前綴比對」:你的路徑是 /1/3/4/,只看得到路徑以它開頭的租戶,母公司 /1/3/ 不是,所以看不到。但有九張表用的是另一種寫法:把路徑 /1/3/4/ 拆成數字陣列 [1,3,4],只要資料列的 tenant_id 在陣列裡就可見——於是子公司的人查這九張表時,母公司(3)與平台(1)的資料全部算可見。
190 實證(runner 用 blsfrank、子租戶 4):打 /license/status 回的是母公司 Billows 的授權(created_user: blsadmin, customer_code: Billows);對母租戶三個檢測設定 uid 打測試連線都通過「設定存在」檢查,假 uid 才 404。反向(母看子)不會,因 /1/3/ 拆成 [1,3] 不含 4。
首腦已核(190 pg_policies,qual ILIKE '%string_to_array%',正好 9 張):
compliance.agent_tasks agent_tasks_tenant_isolation
compliance.detection_execution_groups detection_execution_groups_tenant_isolation
compliance.detection_executions detection_executions_tenant_isolation
config.job_execution_detection_tool_agents jedta_tenant_isolation
config.job_execution_detection_tools jedt_tenant_isolation
config.tenant_detection_tool_configs tdtc_tenant_isolation
config.tenant_license_events tenant_license_events_tenant_isolation
config.tenant_license_suspensions tenant_license_suspensions_tenant_isolation
config.tenant_licenses tenant_licenses_tenant_isolation
/Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/.claude/worktrees/wt-fix-security(branch fix/security-b1)——主線 migration、守衛測試、scripts/sql/packages/ 攤平副本都在這。/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/.claude/worktrees/jedi-wt-fix-security(branch fix/security-b1)——三支套件各自的 migrations/:jedi-detection(1.2.2)、jedi-license-runtime(1.1.1)、jedi-remote-agent。只動這三支套件的 migrations/ 子目錄、只 git add 該檔。壞寫法(九處,全部同一句):
... OR (tenant_id = ANY ((string_to_array(TRIM(BOTH '/' FROM current_setting('app.allowed_tenant_paths', true)), '/'))::bigint[]))
來源(套件 migration,各自 DO 區塊包 CREATE POLICY、duplicate_object 即略過——所以只改這些檔對既有庫無效,見怎麼修):
/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/.claude/worktrees/jedi-wt-fix-security/jedi-detection/jedi_detection/migrations/002-detection-rls-grants.sql:34-40, 86-97 (5 張:detection_execution_groups/detection_executions/jedta/jedt/tdtc)
/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/.claude/worktrees/jedi-wt-fix-security/jedi-license-runtime/jedi_license_runtime/migrations/002-license-rls.sql、003-tenant-license-suspensions.sql (3 張 license)
/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/.claude/worktrees/jedi-wt-fix-security/jedi-remote-agent/jedi_remote_agent/migrations/002-remote-agent-rls.sql (agent_tasks)
主線舊檔也有一份(歷史,不動):scripts/sql/2026-07-26-fr056-3-agent-tasks.sql、2026-08-08-fr062-2-tenant-licenses.sql、2026-08-09-fr062-tenant-license-events.sql
BE 攤平副本:scripts/sql/packages/jedi_detection/002-*.sql、jedi_license_runtime/002-*.sql、jedi_remote_agent/002-*.sql(build 期由 flatten_pkg_migrations.sh 從套件產,改套件後重跑攤平)
對的寫法(projects/upload_files 等在用):
... OR app_tenant_allowed_for_session(tenant_id)
函式定義 scripts/init/02-schema.sql:623-640:SECURITY DEFINER,對 tenants.path 做 LIKE 前綴,空/未設 allowed_tenant_paths 一律 false
守衛測試風格:test/test_project_scoped_tables_rls.py(讀 pg_policies 的 polqual 斷言)
scripts/sql/2026-09-28-fr114-rls-tenant-prefix-unify.sql:對九張表各 DROP POLICY IF EXISTS <name> ON <table>; CREATE POLICY <name> ON <table> FOR ALL USING (COALESCE(current_setting('app.is_super_admin', true), 'f') = 't' OR app_tenant_allowed_for_session(tenant_id));。這支才是既有客戶(含 190)真正生效的路徑——套件 migration 的 DO 區塊撞 duplicate_object 就跳過,改了也不會重建。照 sql-migration skill:檔頭 -- Date:、每語句日期註解、收尾 INSERT public.schema_migrations、--single-transaction -v ON_ERROR_STOP=1 用 cmmgr 套 DEV。scripts/build/flatten_pkg_migrations.sh(先 cat 看它怎麼用)讓 scripts/sql/packages/ 攤平副本跟上,攤平產物一起 commit。test/test_rls_no_path_array_policy.py:連 DEV 讀 pg_policies,斷言全庫沒有任何 policy 的 qual 含 string_to_array(不只九張,防未來再長)。突變:把 ① 的其中一張改回舊寫法套上去要紅。風格照 test_project_scoped_tables_rls.py。app_tenant_allowed_for_session 參數型別是 integer,九張表 tenant_id 若是 bigint 要 cast(app_tenant_allowed_for_session(tenant_id::integer)),先 \d 查型別。is_super_admin 那半句照 projects 的寫法用 COALESCE(..., 'f')。app_tenant_allowed_for_session 函式本身。SET app.allowed_tenant_paths='/1/3/4/'; SET app.is_super_admin='f'; SELECT count(*) FROM config.tenant_licenses WHERE tenant_id=3; 期待 >0(能看到母租戶,重現洞)。DEV 若沒有子租戶,先用 cmmgr 在 tenants 補一筆 path /1/3/4/ 的測試租戶(手測完刪)。app.allowed_tenant_paths='/1/3/' 期待 >0(母看自己與子孫仍正常);九張表各打一次。