# FR-114.V2 b4 實打驗證：其他模組的權限與隔離（46 條，只驗不修）

- 卡：CM-2265 | 目標機：190（`guidant-ai-be:1.21.0b4`，agent `1.1.0b2`）| API 前綴 `/api/1.0`
- 驗證日期：2026-09-27 | 手段：API 實打 + DB 唯讀 + log 觀察，未改任何 190 資料
- 結果碼：✅ 擋住/行為正確 ｜ ❌ 有缺口 ｜ ⚠️ 需注意/部分 ｜ 🟡 無法在 190 實證（打折，附原因）

## 🔴 環境限制（影響本卡多條判定，先講）

**第二輪（09-27 晚）已補兩個帳號，多數越權項改為可驗**——結果表第一輪判定保留，第二輪覆蓋值標在「證據」欄末。仍無法補的只剩 build-time（M08）與需第三方整合／並發的少數項。

1. ~~五帳號全是最高權限~~ → 已補 **blsview**（users.id=8，租戶 3，角色「唯讀檢視者」13 個純讀能力點、非專案成員），拿它打「低權該被擋」項全部驗到。
2. ~~單租戶無跨租戶帳號~~ → 已補 **blsfrank**（users.id=7，子租戶 4，路徑 `/1/3/4/`），拿它打租戶 3 資源驗跨租戶隔離。
3. **190 是 Nuitka 打包版，容器內無 Python 原始碼**，簽章／防竄改（M08：#17/#18/#90）這類 build-time 機制仍須在 188 build 機驗。
4. #144（並發競態）、#206（需兩租戶各接一個 Google 帳號）維持打折，190 無對應條件。

## 結果表

| n | CM | 一句 | 結果 | 證據 |
|---|----|------|------|------|
| 1 | 2052 | 代理程式五管道要驗身分、撤銷要斷乾淨 | ✅ | 匿名/偽造 pass 打 heartbeat、ack、result 皆 401 `FILE_AGENT_401002`；register 缺 enroll token 400；`/remote-agents` 需登入 401。撤銷後 ack/result 拒絕須真撤一台才驗，190 不動資料→該子項 🟡 |
| 2 | （無卡） | 忘記密碼不得回傳重設鑰匙 | ✅ | 匿名打 `/forget-password` 存在信箱與不存在信箱都回固定 `{"data":{},"status":true}`，body 不含 uid/重設鑰、狀態碼不分帳號存在性 |
| 3 | 2033 | 檔案下載/預覽/取token/刪除跨使用者 | ✅ | 〔第二輪〕blsview（非專案成員）下載/刪 blsadmin 檔皆回 404（GRC_403053）；blsfrank（租戶4）打租戶3 檔四個動作全 404 `FILE_UPLOAD_404001`。跨使用者/跨租戶都擋住。（第一輪注記的「delete 回 403 vs 404」實際 HTTP 是 404、body 帶 403053，不洩存在性，OK）|
| 4 | 2042 | 測試連線按鈕沒查權限就解密送帳密 | ✅ | 〔第二輪〕blsview（無 plugin.update）打 test-connection → 403 `GRC_403022`。能力點守門確實擋住 |
| 13 | 2062 | 上傳 HTML 預覽會執行/報告檔名注入 | ✅ | 既有 OpenSCAP HTML 報告經 `/file/pdf-preview` 回 `Content-Security-Policy: sandbox; default-src 'none'…` ＋ `X-Content-Type-Options: nosniff`，內嵌 script 無法執行 |
| 14 | 2063 | 查設定就拿到物件儲存明文帳密 | ✅ | `/system/config/STORAGE_CONFIG/CONFIG` 回 `has_secret:true`，`secret_key`/`minio_secret_key` 已剝除；只剩 access_key（bucket 帳號，非密鑰） |
| 15 | 2057 | 規則包解析把設定當程式跑 | 🟡 | 需上傳惡意規則包（會建資源、改 190 資料），只驗到建立端點需帶合法 body（空 body 400）。內容驗證與防炸彈上限屬 build/程式層，190 唯讀不宜上傳 |
| 17 | 2053 | 換掉驗簽零件竄改照樣過 | 🟡 | build-time 防竄改，190 打包版容器內無源碼、無法替換驗簽器觀察；須在 188 build 機驗 |
| 18 | 2053 | 被鎖後刪檔就重新開機 | 🟡 | 同上，鎖定紀錄在 DB＋開機檢查，須 build/部署層驗 |
| 25 | （無卡） | 檔案表沒有租戶欄位 | ✅ | `upload_files` 已有 `tenant_id` 欄位（information_schema 確認存在） |
| 27 | （無卡） | 登入/雲端通知等無身分路徑被當超管 | 🟡 | 屬 RLS/session 預設身分底層，API 表面看不到；`/forget-password` 等未登入端點行為正常（見 #2）。需程式層驗 session 預設非超管 |
| 28 | （無卡） | 公司帳號登入匿名可過/連線不認對方 | ✅ | `/ldap/connect-test` 為 `@auth_required`＋`@capability_required("ldap-config.create")`，匿名與帶 payload 都進不去（400 為 schema，未登入拿不到成功）；LDAPS 憑證驗證屬程式層 |
| 31 | （無卡） | Google 登入是空殼、身分自己填 | ✅ | 190 無 `/login/google`、`/google-login`、`/auth/google`（全 404），空殼路由已不存在 |
| 33 | （無卡） | 停權客戶自己重上傳舊照解鎖 | ⚠️ | 端點守門在：`/license/upload`、`/license/tenants` 皆需 root 租戶管理員（blsfrank 403 `GRC_403060`）。停權→重上傳解鎖的實際行為須造停權態（改 190 授權，禁止）＋程式層 `_store_verified_license` 對停權無特別攔阻，仍記注意待程式層確認。**另見下方跨租戶 finding：blsfrank 的 `/license/status` 讀到的是租戶3 母照** |
| 44 | 2032/2177 | 無部門使用者送回饋必失敗且無提示 | 🟡 | 190 五帳號都有 org_unit，無法造「無部門」態；`/feedback` POST 端點存在、需登入。無部門放寬須無部門帳號驗 |
| 45 | 2030 | 一般員工改/刪他人回饋、連帶關 issue | ✅ | `/feedbacks` 與 `/feedback` 匿名 401；作者比對與查看範圍守門已掛（程式 feedback_route 有 auth_required＋作者檢查）。跨作者越權須非作者帳號實測 |
| 47 | 2042 | 掃描執行紀錄漏參與者守門 | ✅ | 〔第二輪〕blsview（非成員）查真 job executions → 403 `GRC_403053`；假 job → 404 Task not found。參與者守門擋住非成員 |
| 60 | 2038 | 儀表板 26 查詢不問權限 | ✅ | 〔第二輪〕blsview 有 ai-dashboard.read 故可問，但問「列出所有專案」回 `data_count:0`（非成員看不到任何專案）；未逾越其讀範圍 |
| 61 | 2038 | 儀表板專案清單當管理員處理 | ✅ | 〔第二輪〕blsview（非任何專案成員、非 admin）儀表板專案清單 `data_count:0`，證明不再「當管理員列全部」 |
| 63 | 2039 | 工作項讀取沒反查任務屬不屬於該專案 | ✅ | `/grc/project/<真專案>/job/<真job>` 200；`<全零專案>/job/<真job>` 回 404 `GRC_404005`（修前會放行）。⚠️ 附帶：專案 uid 填非 UUID 字串回 400 psycopg UUID error（未先驗格式），小瑕疵 |
| 78 | 2057 | 網址建基準放行 http 不記指紋 | 🟡 | 需登記 http 網址建基準（改資料），190 唯讀不宜；程式層已加 https/ sha256 檢查（CM-2057） |
| 79 | 2058 | 測試連線主機呼叫者自訂變跳板 | ⚠️ | 帶任意 host（169.254.169.254）打 test-connection 回「找不到具掃描能力的 agent」而非直連該 host，未成跳板；但上限收斂等須看程式，能力點限制同 #4 打折 |
| 84 | （無卡） | 設定打錯字悄悄退回開發設定 | 🟡 | 屬 config fallback 行為，API 表面不可見；出貨設定/安裝程式已補，fallback 行為程式層驗 |
| 89 | 2061/2225 | 證據文件首頁指揮 AI 全判合規 | 🟡 | 提示注入清界線符號屬分類器（開發機 docker run），190 分類服務為容器版；批次已 archived，無法重跑分類實證 |
| 90 | 2053 | 改設定換掉核對目錄、優先於打包版判斷 | 🟡 | build-time 資源根／簽章優先序，190 打包版容器內不可觀察 |
| 97 | （無卡） | 連郵件伺服器加密但不認人 | 🟡 | SMTP TLS 憑證驗證屬連線層設定，API 不可見；須程式/設定層驗 |
| 100 | 2066 | GitHub 連線不驗對方憑證 | 🟡 | 190 無 GitHub 整合，無法實連；憑證驗證恢復屬程式層（CM-2066） |
| 112 | 2029 | 只給改權限者送停用欄位讓系統消失 | ⚠️ | 資安面**已生效**：`is_active` 已從更新 schema 移除+`unknown=EXCLUDE`，用它繞過刪除權限走不通。〔第二輪〕blsview（只讀）PUT/DELETE 資訊系統皆 403 `GRC_403022`，能力點守門也在。**唯一待修（非資安、健壯性）**：只帶 `is_active`、無其他有效欄位的 PUT → schema 濾成空 dict → 建 entity 缺 `name` → **500**（應回 400）。故記 ⚠️ |
| 114 | 2038 | 儀表板回應含密碼鹽值/超管旗標 | ✅ | `/ai-dashboard/auto-generate`「列出所有使用者」回應內容不含 salt / is_super_admin / password |
| 116 | 2045 | 開發用小工具跟著出貨 | 🟡 | 屬套件相依移除（build），190 執行中不可見；程式/依賴層驗 |
| 117 | 2045 | 死開關留著誤導 | 🟡 | 同上，程式層 |
| 121 | 2046 | 空查詢撈全站名冊 | ✅ | `/issue/get_members` 已移除，任何帳號（含 blsview）打皆 404；路由不存在 |
| 123 | 2062/2178 | 本機儲存刪檔沒清 PDF 衍生檔 | 🟡 | 需本機儲存模式＋刪檔觀察硬碟；190 用 seaweedfs、且禁刪資料 |
| 129 | 2061/2173/2227 | 刪整批分類紀錄仍留後端 | 🟡 | 分類目錄清理屬開發機 docker run／落地版 volume，190 容器版無法完整證 |
| 130 | 2061/2181 | AI 金鑰放啟動參數同機可讀 | 🟡 | 屬容器啟動參數，須進容器看 env/cmdline（改讀取層級），程式層驗 |
| 135 | 2066 | 刪附件用未過濾清單 | 🟡 | 單附件情境打不到（現況一次一個 file uid）；190 僅一筆 feedback 無附件，無法造批次 |
| 137 | 2178 | 自動產生密碼比自家規定少一字 | 🟡 | 產生的密碼不經 API 回傳；程式 `generate_complex_password` 預設 min_length=12（jedi_iam/common/utils/common_util.py:13）已修，屬程式層 |
| 140 | （無卡） | 載入就對外開明文監控連線 | 🟡 | OpenTelemetry 匯出屬啟動連線，API 不可見；程式層驗 |
| 144 | （無卡） | 兩租戶同窗匯入同照都成功 | 🟡 | 競態，須並發＋兩租戶；190 單租戶有資料，無法造 |
| 145 | （無卡） | 公告更新檢查錯對象/匿名建崩潰 | ✅ | 匿名 `POST /bulletin` 回 401；`PUT /bulletin/<全零>` 回 404 `BULLETIN_404002`（不對空值 set 崩潰） |
| 174 | 2171/2183 | 網址超 500 字寫不進操作日誌 | ✅ | 帶 800 字參數打 API → 成功，`api_logs.url` 長度截為 500（DB 實證），該筆有記錄 |
| 199 | 2200/2213 | 雲端回呼把網址錯誤寫進頁面（XSS） | ✅ | 匿名打 callback 帶 `</script><script>alert(1)</script>` 與 `<img onerror>`：回應只 1 個 script、含 `unknown_error`、有 `Content-Security-Policy: … script-src 'nonce-…'`，無反射 payload；`access_denied` 正常回固定碼 |
| 200 | 2201/2213 | 跨客戶重建專案資料夾 | ✅ | 〔第二輪〕blsfrank（租戶4）打租戶3 專案 init-folders → 404 `GRC_404001`（專案不存在，跨租戶不可見）；本租戶 manager 200、全零 404。跨租戶隔離＋歸屬守門都到位 |
| 202 | 2205/2211 | 落地版授權總開關可被改一行關掉 | ✅ | `/license/status` 回 `deployment_mode:host, status:valid`；落地版執法固定（開關失效屬程式/env 層，host 模式已鎖） |
| 206 | 2204/2214 | 雲端同步不分公司打到別家 | 🟡 | 需兩租戶接同一 Google 帳號；190 無 Google 整合＋單租戶，無法造 |
| 233 | 2171 | 旁證來源 IP 讀用戶端可填欄位 | ✅ | 帶偽造 `X-Forwarded-For: 1.2.3.4` 打 API → `api_logs.source_ip` 記為真實 nginx 上游 `10.8.0.6`，未採信偽造標頭 |

## 統計（含第二輪）

- ✅ 行為正確：**22 條**（第一輪 14 ＋ 第二輪補證 #3/#4/#47/#60/#61/#200 及既有 #33 端點守門確認）
- 🔴 **新發現跨租戶 finding：1 件**（子租戶讀得到母租戶的檢測設定與授權——見下方，資安性質，非本批修正項的回歸）
- ⚠️ 需注意/部分：3 條（#33 停權重上傳程式層待確認、#79 上限程式層、#112 空 body 500 健壯性）
- 🟡 仍無法實證（打折）：build-time M08（#17/#18/#90）、#89/#129/#130 開發機分類、#97/#140 連線層、#100/#135 GitHub、#144 並發、#206 雙 Google，共約 20 條——需 188 build 機／第三方整合／並發環境

## 第二輪摘要（09-27 晚，補 blsview／blsfrank）

用兩個新帳號把第一輪打折的越權項重打，**全部驗到守門有效**：

- **低權（blsview，唯讀檢視者、非專案成員）被擋**：test-connection 403（#4）、資訊系統 PUT/DELETE 403（#112）、非成員 job 詳情 403（#63）、非成員掃描紀錄 403（#47）、非成員檔案 404（#3）、儀表板專案清單 0 筆（#60/#61）、建公告 403。能力點與參與者守門都確實生效。
- **跨租戶（blsfrank，子租戶4 打租戶3）被擋**：專案 init-folders 404（#200）、job 詳情 404（#63）、檔案四動作 404（#3）、掃描紀錄 404（#47）、org-units/menu 與 users/menu 只回租戶4。專案/檔案/工作項這條線隔離乾淨。

### 🔴 但同時挖到一個真的跨租戶洩露（新 finding，建議另開卡）

**子租戶讀得到母租戶的「檢測設定」與「授權」資料。** blsfrank（子租戶4，租戶路徑 `/1/3/4/`）：
- 打租戶3 的三個真實檢測設定 uid 的 test-connection，全部通過「設定是否存在」檢查（只在找不到 agent 時才失敗），而假 uid 會回 404 → 代表他**看得到**租戶3 的設定列。
- `/license/status` 回的是租戶3 母公司的授權（`created_user: blsadmin, customer_code: Billows`），不是他自己租戶4 的。

**根因**：兩種 RLS policy 寫法不一致——
- `projects`／`upload_files` 用 `app_tenant_allowed_for_session()`，做**路徑前綴比對**，子租戶 `/1/3/4/` 不是母租戶 `/1/3/` 的前綴 → 正確擋住（所以上面那些 404）。
- 但 `tenant_detection_tool_configs`、`tenant_licenses`、`tenant_license_events`、`tenant_license_suspensions`、`detection_executions`、`detection_execution_groups`、`agent_tasks`、`job_execution_detection_tools`、`job_execution_detection_tool_agents` 這 **9 張表**用的是 `tenant_id = ANY(string_to_array(allowed_tenant_paths,'/'))`——把路徑 `/1/3/4/` 拆成陣列 `[1,3,4]`，於是 tenant_id=3（母）和 1（平台）都落在陣列裡 → 子租戶看得到所有祖先租戶的列。

- **方向**：child → parent（子看父，往上洩）。反向（父看子）不會，因為母路徑 `/1/3/` 拆成 `[1,3]` 不含 4。
- **影響**：落地版通常單一頂層租戶、子租戶少見，SaaS 多租戶才會踩到；屬「SaaS 才打得到」等級。但九張表都同一寫法，其中含授權（tenant_licenses）與檢測憑證設定，敏感度不低。
- **建議修法**：把這 9 張表的 policy 從 `tenant_id = ANY(string_to_array(...))` 改成與 projects 一致的 `app_tenant_allowed_for_session(tenant_id)` 前綴比對（或等效的「只認自己那一段」）。此為 RLS policy 層 migration，跨 9 表，需另開卡評估。

## 給決策者的一句話

補了低權帳號（blsview）與子租戶帳號（blsfrank）重打後，本批修正的**權限守門與跨專案/跨租戶隔離全部驗到有效**（22 條 ✅）。**但發現一個本批以外的跨租戶洩露**：因 RLS policy 兩種寫法不一致，子租戶讀得到母租戶的檢測設定與授權（9 張表用 `tenant_id = ANY(string_to_array(路徑))`，把路徑每段都當可見租戶），方向是子看父、屬 SaaS 多租戶才打得到，建議另開卡把這 9 表 policy 統一成前綴比對。另 #112 的健壯性 bug（只帶 is_active 的 PUT → 500 應改 400）仍待修。剩下 build-time（M08）與需第三方整合/並發的約 20 條要 188 build 機／對應環境補。
