---
title: C05 api／worker → 資料庫
eyebrow: Guidant AI 資安檢視總報告 · 連線清單
h1: C05 api／socketio／worker → 資料庫（guidant-db，PostgreSQL :5432，RLS）
lede: 全部業務資料都存在這條線的另一端。這條線的特別之處是**它自己會擋人**——資料庫用「列級安全規則」（RLS）依每次連線設定的身分變數，決定哪一列看得到、改得動。程式層漏掉的守門，理論上由這道牆兜底；但牆有沒有建、建對了沒、身分有沒有帶上，就是這條線上 **35 條**掃描問題的全部。這一頁回答：這條線**怎麼連**、**哪些功能走它**、它帶來**什麼威脅、駭客怎麼打**、我們**要怎麼防、目前做到哪**。
chips:
  - { text: "必有", kind: plain }
  - { text: "DB 帳密・RLS 兜底", kind: warn }
  - { text: "威脅 7 種", kind: accent }
  - { text: "掃描命中 35 條", kind: plain }
---

> **這一頁怎麼來的**：[DFD Level 0](../DFD/dfd-level0.html#c05) 把產品運作時的連線編成 C01～C25；[STRIDE 六頁](../STRIDE/S-spoofing.html)每一條問題都標了發生在哪幾條線。這裡以**連線**為單位整理。C05 命中的 35 條依「駭客實際怎麼打」歸成**七種手法**，每種寫駭客怎麼打、得手什麼，底下列全部實例連回 STRIDE 頁。個別問題修了沒不在這頁講。

## 一、連線圖

```{.mermaid cap="C05 — 三個行程連同一個資料庫。每次開交易，程式把「你是誰、能看哪些客戶」寫進連線變數，資料庫的規則讀這些變數決定每一列給不給看。紅框是「身分沒帶上」或「牆根本沒建」的狀況——這條線上所有問題都落在這兩種。"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB'}}}%%
flowchart LR
    subgraph PROC["三個行程（同一組 DB 帳密 cm_app）"]
        direction TB
        API["guidant-api<br/>請求期：身分來自 JWT"]
        SIO["guidant-socketio"]
        WK["guidant-worker・排程<br/>無人登入：身分要自己宣告"]
    end
    subgraph SS["每次開交易（session_scope）"]
        direction TB
        S1["有身分 →<br/>SET app.user_id<br/>SET app.allowed_tenant_paths<br/>SET app.is_super_admin"]
        S2["沒身分 →<br/>什麼都看不到＋記一行警告"]
        S3["具名系統身分 →<br/>is_super_admin = t<br/>（繞牆，顯式宣告）"]
    end
    subgraph DB["guidant-db :5432（不對外開埠）"]
        direction TB
        W1[("有牆的表<br/>規則讀變數決定每列")]
        W2[("沒牆的表<br/>oscal.* 52 張只 5 張有牆<br/>靠程式層當唯一防線")]
        V[("視圖<br/>以查詢者身分執行")]
    end
    API --> SS
    SIO --> SS
    WK --> SS
    S1 == "C05 · TCP :5432 明文（docker 內網）" ==> W1
    S1 --> W2
    S3 -. "看全部" .-> W1
    S2 -. "0 列" .-> W1
    W1 --> V
    classDef bad fill:#FDECEC,stroke:#C0392B
    class S2,W2 bad
```

## 二、這條線怎麼連

| 項目 | 現況 |
|---|---|
| **誰 → 誰** | `guidant-api`、`guidant-socketio`、`guidant-worker`（含 worker 內的排程）三個行程 → `guidant-db`（PostgreSQL 16 容器）。走 docker 內部網路 `guidant_default`，`guidant-db` **沒有 `ports:` 映射、不對外開埠**，只有 stack 內的服務連得到 |
| **協定／埠** | TCP :5432，psycopg 驅動，連線字串 `postgresql+psycopg://cm_app:…@guidant-db:5432/guidant_ai`，**沒帶 `sslmode`，是明文**（內網；與 C02、C06 同一個接受前提）。連線池每個行程 10 常駐＋20 溢出 |
| **傳什麼** | 全部業務資料的讀寫：專案、問卷、範本、稽核紀錄、授權、系統設定；**也包含紀錄類**——操作日誌（`api_logs`）與系統日誌（`system_logs`）兩張表直接寫在這裡，worker 的工作單（`background_jobs`）也經這裡入列（即 [C09](../DFD/dfd-level0.html#c09)） |
| **怎麼驗身分** | 兩層。**第一層是 DB 帳密**：三個行程全程只持 `cm_app`（無特權、受 RLS），叢集管理帳號 `cmmgr`（BYPASSRLS）只在建庫與升級時用，服務容器不持有。**第二層是每次交易的身分變數**：`session_scope()` 開交易時把登入身分寫進連線變數——`app.user_id`、`app.allowed_tenant_paths`（你能看的客戶路徑，如 `/1/102/`）、`app.is_super_admin`；資料庫的 RLS 規則讀這些變數決定每一列給不給看、給不給改 |
| **沒身分時怎麼辦** | **什麼都看不到（fail-closed）**：`session_scope()` 查不到身分時設 `app.is_super_admin = 'f'`、不設客戶路徑，規則的兩個條件都不成立，查詢回 0 列；同時記一行 WARNING 指出是哪個呼叫點沒帶身分。要繞牆只認兩個**顯式**訊號：`system_context("<排程名>")` 具名系統身分，或身分本身就在原廠最上層租戶路徑 |
| **牆怎麼判** | 每張有牆的表掛四條規則（查／增／改／刪），形狀固定：「`app.is_super_admin = 't'`，或 `app_tenant_allowed_for_session(tenant_id)`」。後者做**前綴比對**——你的路徑是 `/1/102/`，就看得到 `/1/102/` 與 `/1/102/…/` 底下的（自己與子公司），看不到 `/1/` 母公司與 `/1/103/` 兄弟；路徑變數為空一律拒絕 |
| **牆蓋到哪** | **不是每張表都有牆**（DEV 實查 2026-10-02）：有客戶欄位的 49 張表 48 張已開牆（剩 `config.log_forwarding_settings` 一張，裁定屬平台層設定）；另有 15 張沒客戶欄位、靠「看得到專案才看得到它」的跟隨規則。按 schema 看：`compliance` 42 張有 31 張、`survey` 14／14、`config` 13 張有 8 張、`public` 72 張有 17 張、**`oscal` 52 張只有 5 張**。沒牆的表分三類：① 合規文件那一整塊（`oscal.*`）——連客戶欄位都沒有，程式層是唯一防線；② 紀錄與系統表（`api_logs`／`system_logs` 各月分區、`capabilities`／`ui_routes`／`system_menus`）——天生不分客戶；③ 關聯表與子表（`job_evidences`、`bulletin_org_units`、`*_trans`）——靠掛它的主表擋 |
| **何時存在** | 必有。三個行程實測都持有連線 |

依據：`core/app_factory.py` 連線字串、jedi-common `jedi_common/session/database/db.py` 的 `session_scope()`、`jedi_common/session/auth/system_context.py`、`scripts/init/00-cluster.sql` 角色設計、`scripts/init/02-schema.sql` 的 `app_tenant_allowed_for_session()` 與 222 條 `CREATE POLICY`、`docker/production/docker-compose.yml` 的 `guidant-db` 段、DEV 庫 `pg_class.relrowsecurity` 逐 schema 統計、DFD Level 0。

## 三、哪些功能會走這條線

**全部**——沒有一個功能不讀寫資料庫。但對這條線的威脅分析，功能依「牆對它有沒有效」分四群，每群出的問題形狀不同：

| 群 | 功能 | 牆的狀況 | 威脅的來源 |
|---|---|---|---|
| **① 有牆、靠牆兜底** | 專案、任務、問卷、設備、公告、授權、檢測工具與掃描紀錄、使用者與角色、檔案紀錄 | 表上有客戶欄位＋四條規則 | 牆建錯：開關沒開、規則方向反、視圖繞過、新增規則寫成一律允許 |
| **② 沒牆、程式層是唯一防線** | 合規文件整塊（系統安全計畫、評估計畫、稽核發現、改善計畫及其子表）、範本內容 | `oscal.*` 連客戶欄位都沒有，歸屬只記在 `compliance.module_frames` 這張對照表 | 程式層守門漏一支就跨客戶，資料庫不會兜底 |
| **③ 無人登入的背景作業** | 半夜清理排程（過期工作單、孤兒綁定）、日誌分區維護、授權到期檢查、worker 撿單、Drive 同步 | 要掃全部客戶，必須顯式宣告系統身分才看得到東西 | 身分沒帶上：以前是靜默拿到全庫，現在是靜默看到 0 筆——判斷方向反過來就會刪錯東西 |
| **④ 紀錄類寫入** | 每個請求寫一筆操作日誌、程式錯誤寫系統日誌、寄信與整合設定的錯誤訊息 | 兩張紀錄表沒牆（天生不分客戶） | 寫進去的內容帶祕密（密碼、通行證、整組設定）；寫失敗拖垮請求；保存期限沒生效 |

## 四、這條線會帶來什麼威脅、駭客怎麼打

35 條歸納成**七種手法**。與 C02 不同，這條線上的攻擊者**不只是「有帳號的人」**——T4、T5 的攻擊者是「拿得到資料庫備份或唯讀帳號的人」（維運、外包、離職者、拿到回廠診斷包的人），他們不經過 api，直接讀資料庫。每種寫駭客實際的步驟與得手什麼；實例欄是該手法的全部條目，點了連回 STRIDE 頁。

| # | 風險 | 威脅 | 駭客怎麼打 | 得手什麼 | STRIDE | 實例 |
|---|---|---|---|---|---|---|
| T1 | 🟠 高 | **牆沒蓋到：表上有客戶欄位但隔離沒生效、或連欄位都沒有**（6 條） | ① 任一家客戶的員工登入，打開專案清單、我的任務、改善計畫、檔案下載這類頁面；② 程式層那一支剛好沒過濾客戶（C02 的 T1／T2 任一條都算）；③ 本該兜底的資料庫規則不在——開關沒開、規則只寫一半、視圖以建立者（最高權限）身分查、或表上根本沒有「這是哪家的」欄位；④ 別家客戶的資料直接出現在畫面上。 | 別家客戶的專案、待辦、任務執行紀錄、稽核缺失、改善計畫、上傳的證據檔；SaaS 版上還有系統安全計畫與稽核發現。 | [I 資料外洩](../STRIDE/I-information-disclosure.html) | [M20-1 13 張標有客戶歸屬的資料表，資料庫那道隔離牆沒有真正生效](../STRIDE/I-information-disclosure.html#m20-1) 🟠<br>[M20-2 4 個資料查詢畫面整個繞過隔離機制](../STRIDE/I-information-disclosure.html#m20-2) 🟠<br>[M02-6 存放檔案紀錄的資料表沒有客戶歸屬欄位，資料庫擋不住跨客戶](../STRIDE/I-information-disclosure.html#m02-6) 🟠<br>[M20-5 一整類業務資料表連客戶歸屬欄位都沒有，牆沒有東西可依據](../STRIDE/I-information-disclosure.html#m20-5) 🟡<br>[M17-5 公告與部門的關聯表沒有設定客戶資料隔離](../STRIDE/I-information-disclosure.html#m17-5) ⚪<br>[M11-41 兩張解析工作單表的資料庫新增規則寫成一律允許](../STRIDE/T-tampering.html#m11-41) ⚪ |
| T2 | 🟠 高 | **牆蓋反了：子公司看得到、改得動母公司的東西**（4 條） | ① 被停權客戶、或任一子公司的成員登入（只要手上有一個子單位帳號）；② 打開檢測工具設定、授權狀態、解析工作單這類會讀「母公司那層」的頁面；③ 規則把子公司路徑 `/1/102/152/` 拆成 `[1, 102, 152]` 逐一比對（或用純文字包含比對），於是母公司 102、原廠 1 的列全被算成「你碰得到的」，而且**讀得到＝改得到＝刪得到**；④ 刪掉停權紀錄當場復權；改母公司的掃描紀錄；疊上「測試連線」把母公司的維運帳密送到自己主機。 | 被停權客戶自行復權並改寫授權內容、刪掉異動時間線；母公司的檢測設定、維運帳密與掃描紀錄；母公司稽核計畫的解析內容。 | [E 權限提升](../STRIDE/E-elevation-of-privilege.html)、[I 資料外洩](../STRIDE/I-information-disclosure.html) | [M18-8 子單位帳號改得動、刪得掉母公司的授權與停權紀錄](../STRIDE/E-elevation-of-privilege.html#m18-8) 🟠<br>[M03-7 資料庫的客戶隔離規則方向寫反](../STRIDE/I-information-disclosure.html#m03-7) 🟡<br>[M12-7 兩張「解析工作單」資料表照抄了五月就判定壞掉的隔離寫法](../STRIDE/I-information-disclosure.html#m12-7) ⚪<br>[M11-40 人員對帳收了公司編號卻沒用，會拿所有客戶的人員來比對](../STRIDE/I-information-disclosure.html#m11-40) ⚪ |
| T3 | 🟠 高 | **身分沒帶上：沒身分時牆的方向整個反過來**（6 條） | 兩個方向——(a) **以前**：讓請求在「沒有登入身分」的狀態下進到業務邏輯（登入入口本身、雲端硬碟通知入口、漏掛認證的路由、沒還原身分的背景執行緒），資料庫把「查不到身分」解讀成「最高權限」，那段程式碰得到的每一筆都從一家放大成全部客戶；(b) **現在**：沒身分改成什麼都看不到，於是半夜清理排程若忘了宣告系統身分，會「看不到任何任務」→「每一筆綁定都指向不存在的任務」→ 把所有客戶的任務與證據對應一次清空；或安靜掃到 0 筆、孤兒資料永遠堆積。另外「每次連線都設、但沒有規則讀」的假開關，與「用字串拼接寫身分變數」的寫法，是下一次有人改這段時的陷阱。 | (a) 那段程式碰得到的全部客戶資料；(b) 沒有人得手——是一夜之間所有客戶的任務與證據對應被清掉，或清理機制在不知不覺中停擺。 | [E 權限提升](../STRIDE/E-elevation-of-privilege.html)、[T 竄改資料](../STRIDE/T-tampering.html)、[D 讓服務停擺](../STRIDE/D-denial-of-service.html) | [M04-2 沒有登入身分時，客戶資料隔離整個關掉](../STRIDE/E-elevation-of-privilege.html#m04-2) 🟠<br>[M20-4 兩支半夜清理程式靠「找不到身分就給最高權限」才跑得動](../STRIDE/D-denial-of-service.html#m20-4) 🟡<br>[M06-25 孤兒清理排程若沒帶身分，會把正常資料整批刪掉](../STRIDE/T-tampering.html#m06-25) ⚪<br>[M04-14 每次連線都設定、但全系統沒人讀的開關](../STRIDE/E-elevation-of-privilege.html#m04-14) ⚪<br>[M04-13 資料庫隔離設定值用字串拼進查詢語句](../STRIDE/E-elevation-of-privilege.html#m04-13) ⚪<br>[M21-3 沒有部門的一般使用者送意見回饋必定失敗、畫面不給理由](../STRIDE/D-denial-of-service.html#m21-3) 🟡 |
| T4 | 🟠 高 | **祕密以明文落在資料庫裡：密碼、通行證、主機帳密**（4 條） | ① 不用打——使用者正常登入、管理員設定 LDAP 或 SSH 整合、按「開始掃描」，程式就把密碼原文、登入通行證、解密後的客戶主機帳密寫進操作日誌表或派工單表；② 拿得到資料庫備份、唯讀帳號、或回廠診斷包的人打開搜尋 `password`／`Authorization`；③ 直接拿到密碼與可冒用身分的通行證，不必破解任何東西。遮蔽機制遇到密碼裡的雙引號只遮一半——**密碼越強越容易中招**；寄信失敗時整組郵件設定連密碼一起進紀錄。 | 使用者登入密碼原文、可直接冒用的通行證、客戶正式機房的主機密碼與掃描器管理員權杖、公司郵件帳密、系統整合密碼的後半截。 | [I 資料外洩](../STRIDE/I-information-disclosure.html) | [M04-1 登入密碼與通行證原文寫進紀錄檔與資料庫](../STRIDE/I-information-disclosure.html#m04-1) 🟠<br>[M03-9 開始掃描時把解密後的主機帳密多存一份明文](../STRIDE/I-information-disclosure.html#m03-9) 🟡<br>[M04-3 密碼裡有雙引號就只遮一半](../STRIDE/I-information-disclosure.html#m04-3) 🟡<br>[M16-2 寄信失敗把整組郵件設定含密碼寫進紀錄](../STRIDE/I-information-disclosure.html#m16-2) 🟡 |
| T5 | 🟡 中 | **紀錄表本身：寫不進去拖垮請求、寫太長就不留痕、該刪的不刪**（5 條） | 三種路——(a) 已登入的人要刪資料或改設定，在網址後面加一段無意義參數撐過 500 字，操作照做、寫操作日誌時因為太長整筆失敗，事後查不到這次操作；(b) 資料庫那一刻寫不進去（連線斷、表被鎖），「記一筆日誌」的錯誤往上冒，把使用者已經做完的請求一起弄成失敗；(c) 無需攻擊者——按月分區從沒被建，90／180 天保存期限從沒生效，含密碼原文的舊日誌一直留著；兩張紀錄表天生不分客戶，完整錯誤堆疊帶出內部路徑與資料片段。 | (a) 刪資料、改設定而不留紀錄；(b) 沒有人得手，是正常請求被連累；(c) 早該刪掉的舊日誌、全部客戶的運作紀錄、系統內部結構。 | [R 事後無法追查](../STRIDE/R-repudiation.html)、[D 讓服務停擺](../STRIDE/D-denial-of-service.html)、[I 資料外洩](../STRIDE/I-information-disclosure.html) | [M09-6 把網址加長，這次操作就不會出現在操作日誌](../STRIDE/R-repudiation.html#m09-6) 🟡<br>[總表#103 寫日誌進資料庫失敗會連帶弄壞使用者的請求](../STRIDE/D-denial-of-service.html#r103) 🟡<br>[M09-5 日誌表按月分區失效，保存期限沒生效](../STRIDE/I-information-disclosure.html#m09-5) 🟡<br>[M04-4 兩張運作紀錄表沒有客戶隔離](../STRIDE/I-information-disclosure.html#m04-4) ⚪<br>[M04-5 完整的程式出錯內容被寫進那張沒有隔離的紀錄表](../STRIDE/I-information-disclosure.html#m04-5) ⚪ |
| T6 | 🟡 中 | **一個請求讓資料庫做太多：整張表撈出來、交易開著一列列讀**（4 條） | ① 任何登入帳號打開任一清單頁，把「一頁幾筆」改成超大數字，資料庫把整張表一次撈出來組好回傳；② 或拿一份幾 KB 的稽核表在第 20 萬列填一格去匯入，系統從頭一列列讀到那裡，**整段時間資料庫交易一直開著**；③ 或有法規框架修改權的人存一筆空白流程範本，之後每個人打開清單頁都讀到它、解析出錯、整頁 500，不會自己恢復；④ 連送幾次，處理程序與資料庫連線池全被佔住。 | 讓整個產品對所有客戶停止回應，或讓所有人的某個清單頁持續打不開。 | [D 讓服務停擺](../STRIDE/D-denial-of-service.html) | [M04-7 一次要求回傳幾筆資料，沒有上限](../STRIDE/D-denial-of-service.html#m04-7) 🟡<br>[M12-3 稽核紀錄 Excel 在很後面填一格，系統就從頭一列列讀到那裡](../STRIDE/D-denial-of-service.html#m12-3) 🟡<br>[M06-8 一筆空白內容的流程範本讓所有人的清單頁一直出錯](../STRIDE/D-denial-of-service.html#m06-8) 🟡<br>[M19-3 設備清單一頁要顯示幾筆，沒有上限](../STRIDE/D-denial-of-service.html#m19-3) ⚪ |
| T7 | 🟠 高 | **沒牆的表上，程式層漏檢查就是整份改寫**（6 條） | ① 任一登入帳號（連唯讀稽核員都算）拿到一份範本或輪次編號——別家公司的或原廠公版的；② 上傳一份自己做的 Excel／Word，去向選「覆蓋既有範本」、目標填那個編號，或換網址讀別人專案的稽核判定；③ 範本內容與稽核判定落在 `oscal.*` 沒牆的表，程式層只查「範本存不存在」、只驗「有沒有登入」，資料庫不會兜底；④ 整份清空重寫，之後每個用這份範本開專案的客戶都複製到被改過的內容。另外兩條是同一塊的時序與一致性問題：平台管理員刪框架不問有沒有客戶在用（各客戶資源庫斷鏈）；兩家同時上傳同一張授權檔，查重與寫入不在同一個交易，兩邊都查到「沒人用」。 | 整份改寫別家公司或原廠公版的合規範本並汙染之後所有專案；別人專案的稽核判定與改善計畫；一次誤刪讓所有客戶資源庫斷鏈；一張授權兩家同時用。 | [T 竄改資料](../STRIDE/T-tampering.html)、[I 資料外洩](../STRIDE/I-information-disclosure.html) | [M11-2 用 Excel 蓋掉既有範本，從頭到尾不問你是誰](../STRIDE/T-tampering.html#m11-2) 🟠<br>[M11-3 用 Word 蓋掉既有範本，只查範本存不存在](../STRIDE/T-tampering.html#m11-3) 🟠<br>[M10-4 不是這個專案的人，換網址就看到別人專案的稽核判定與改善計畫](../STRIDE/I-information-disclosure.html#m10-4) 🟠<br>[M12-1 同公司不是這個專案的人，換個網址就看得到稽核判定與改善計畫](../STRIDE/I-information-disclosure.html#m12-1) 🟠<br>[M11-27 刪框架或版本時不檢查還有沒有資源庫在用](../STRIDE/T-tampering.html#m11-27) 🟡<br>[M18-9 同一張授權檔可能被兩家客戶同時用上](../STRIDE/T-tampering.html#m18-9) ⚪ |

**七種手法的共同點**：T1～T3、T7 共 22 條都是同一件事——**這道牆是「最後一道防線」，但它的覆蓋範圍、方向、和身分有沒有帶上，都是人手一張表、一支排程地維護，沒有機制保證**。牆蓋到哪（T1、T7）、蓋對了沒（T2）、有沒有人在門口（T3）三個問題各自獨立，任何一個出錯，程式層的漏洞就從「一家公司」放大成「全部客戶」。T4～T6 是另一件事——**資料庫被當成「什麼都能放、放多少都行」的地方**：祕密原文放進去、紀錄無限留、一次撈整張表；這些不是隔離問題，是「寫進去之前沒人把關」。

## 五、我們要怎麼防、目前做到哪

對應七種威脅，防線分九條。D1～D7 直接對應掃描抓到的手法；**標 ◇ 的是依這條線的特性補的標準防線，掃描範圍沒涵蓋**。「目前」欄寫程式與資料庫裡**實際有的機制**；⚠️ 表示只靠慣例或只守到局部。

| # | 防線 | 擋哪種威脅 | 目前做到哪 |
|---|---|---|---|
| D1 | **每張業務表都要有牆，牆的形狀只有一種**。表上有客戶欄位＋四條標準規則（查／增／改／刪，各自「超管或 `app_tenant_allowed_for_session(tenant_id)`」）；沒有客戶欄位的子表用「看得到父表才看得到子表」的跟隨規則；視圖一律以查詢者身分執行 | T1、T7 | ✅ 標準規則函式 `app_tenant_allowed_for_session()` 已是全庫唯一判準（路徑為空一律拒絕）；專案底下 13 張無客戶欄位的表用跟隨規則並有守衛測試 `test_project_scoped_tables_rls.py`；`upload_files` 補欄位＋開牆並有 `test_upload_files_rls.py`；兩支視圖寫進定義正本 `WITH (security_invoker = true)`。⚠️ **`oscal.*` 52 張只有 5 張有牆**（DEV 實查），合規文件整塊靠程式層當唯一防線——跟隨規則的方向已定（53 張、4 個接點）但未施工，決策者排在 SaaS 前；沒有一支守衛測試掃「哪些有客戶欄位的表沒開牆」或「開了牆的表四條規則齊不齊」（DEV 實查有 11 張開了牆但只掛一條 `ALL` 規則：弱點檢測五張、遠端代理 `agent_tasks`、`background_jobs`、`ai_call_log` 等——`ALL` 一條管四件事不算錯，但形狀與標準四條不同，守衛寫起來要先定義哪種算合格），新加一張表漏開不會被抓到 |
| D2 | **牆的方向只准前綴比對，禁止拆陣列或純文字包含**。子公司只看自己與底下，看不到母公司與兄弟；需要「子讀母」的例外（授權）另加只管讀的祖先規則，改刪仍只吃前綴 | T2 | ✅ 九張表統一改前綴比對；授權三張另加 `app_tenant_is_ancestor_of_session()` 讀取規則；守衛測試 `test_rls_no_path_array_policy.py` 讀全庫規則、斷言不再有拆陣列寫法；`test_rls_license_tables_ancestor_read.py` 守子讀母＝1、子改母＝0。這一條是全庫掃描、有機制保證 |
| D3 | **沒身分一律看不到，繞牆必須具名宣告**。`session_scope()` 查不到身分時 fail-closed 並記警告；排程與背景作業只能用 `system_context("<名字>")` 繞牆；刪除類背景作業刪之前先確認「我看得到東西」 | T3 | ✅ `session_scope()` 無身分時設 `is_super_admin = 'f'`、不設路徑、每個呼叫點警告一次；超管只認 `is_system_context` 與原廠路徑兩個顯式訊號（程式碼註明「不可再加缺值即超管的條件」）；守衛測試 `test_scheduler_system_context.py` 掃 `core/scheduler.py` 每支 tick 都要有具名身分；孤兒清理刪前先數 `count_visible_jobs()`，0 筆就本輪不刪。⚠️ 守衛只掃主專案 scheduler，套件內自己起的背景執行緒（Drive 同步、webhook 續約）靠人記得包；身分變數仍用 f-string 寫入（M04-13 裁定不修，值目前全由系統產生） |
| D4 | **祕密在寫出去的那一刻就遮掉，解密後的值只留在記憶體**。寫日誌的三個出口（請求標頭、請求內容、回應內容）全接同一支遮蔽；遮蔽要能處理跳脫字元；整合設定的錯誤訊息只記位址與錯誤類別；解密後的帳密不寫回任何表 | T4 | ✅ `mark_password` 遮蔽範圍擴到 secret／token／credential／authorization／api_key，三個出口都接上，請求標頭改白名單；正則改成允許反斜線跳脫；派工單寫明文那一行已刪；寄信失敗只記伺服器與錯誤類別。⚠️ 遮蔽靠「欄位名清單」，新欄位沒加進名單就漏；診斷包那端另有一套 `diag_masking.py`，兩套名單要同步 |
| D5 | **紀錄是旁路，失敗不拖垮請求、超長先截斷、過期要真的刪**。寫日誌包錯誤處理走標準出口；欄位寫入前截到上限、截斷後仍失敗補最小替代紀錄；分區維護有排程、以擁有者身分執行 | T5 | ✅ `DBLogHandler.emit()` 整支包 try／except 走 `handleError`；攔截器 `_API_LOG_COLUMN_LIMITS` 截斷＋ `_add_fallback_api_log` 替代紀錄；`maintain_log_partitions()` 改 SECURITY DEFINER、排程每日 03:30 UTC 呼叫。✅ 兩張紀錄表不分客戶屬裁定（落地版一客戶一庫） |
| D6 | **資料庫做的事有上限**：每頁筆數在共用的分頁格式設上限；讀檔解析用串流、連續空白即視為檔尾、列數上限；寫入端拒絕空內容、讀取端一筆壞資料不拖垮整份查詢 | T6 | ✅ `PagerSchema.page_size` 上限 1000（`MAX_PAGE_SIZE`），全站十二處一起生效；稽核表解析改串流＋200 列空白即停＋5,000 列上限；流程範本寫入拒絕空白、讀取端防呆。⚠️ 沒有資料庫層的 `statement_timeout`（基線 `SET statement_timeout = 0`、連線字串沒設），一個卡住的查詢可以無限期佔連線；上限散在各模組，沒有「所有解析端點必過」的一處守門 |
| D7 | **沒牆的表上，程式層守門要能被看見**。資源歸屬只記在對照表的那幾塊（`oscal.*`），所有寫入與讀取端點走統一守門，守門的使用者參數改必填讓漏傳直接報錯；跨客戶的唯一性用資料庫索引兜底 | T7 | ✅ 範本覆蓋走 `resource_library_overwrite_guard.py`（能力點＋歸屬）、上傳與確認兩端都跑；稽核輪次七支讀取守門參數改必填；刪框架前用獨立超管連線數引用數、查不了就拋錯不放行；授權檔部分唯一索引兜底。⚠️ 這一條與 C02 D1／D9 是同一件事——`oscal.*` 沒牆，程式層漏一支就是一個洞，而「哪些端點掛了守門」沒有程式產的清單 |
| D8 ◇ | **DB 帳號最小權限、連線不對外、憑證不進 image**。服務容器只持無特權帳號；管理帳號只在建庫與升級時用；資料庫埠不映射到主機；密碼由安裝程式產生、不寫死 | 這條線的前提——帳密若外流，牆對拿到帳密的人不存在 | ✅ 三個行程只持 `cm_app`（受 RLS、無 CREATEROLE／BYPASSRLS）；`guidant-db` 無 `ports:` 映射；密碼 installer 產生、`.env.example` 留空。⚠️ bundle 內建 PG 時 `cmmgr` 實際是 superuser（compose 註明「可接受」，降規設計服務的是客戶自備 DB）；連線**不加密**（psycopg 沒帶 `sslmode`，DB 容器 `ssl=off`），docker 內網被認為可信——與 C02、C06 同一個前提，SaaS 或客戶自備 DB 時要重新評估 |
| D9 ◇ | **備份與匯出受同等保護**。T4 的攻擊者拿的是「備份或唯讀帳號」——備份檔要加密或至少限權，放在資料目錄下不隨意複製；回廠診斷包經同一套遮蔽 | T4 的另一條路 | ⚠️ 升級前 `pg_dump | gzip` 落到資料目錄 `backup/`，**不加密、沒有清理**；沒有定期備份機制（客戶自理）。診斷包有 `diag_masking.py` 遮蔽，但備份是整庫原文——備份檔外流等於 T4 全部條目一次成立，而且不受 D4 遮蔽保護（D4 遮的是寫進去的紀錄，備份裡的業務表本來就該有明文） |

**最該補的一步**：D1 的「牆覆蓋率用程式掃、不靠人記」。35 條裡 T1～T3 共 16 條，病因全是「某張表／某支排程漏了」——D2、D3 已經各有一支守衛測試掃全庫，唯獨「有客戶欄位的表有沒有開牆」「有牆的表是不是四條規則都在」沒有守衛，而這正是 M20-1 那 13 張表的病。一支讀 `pg_class`＋`pg_policies` 的測試就能把這條從「規範」變「機制」。

## 六、依這些防線，掃描還沒看過的地方

- **牆覆蓋率守衛**（D1）：DEV 實查 49 張有 `tenant_id` 的表 48 張已開牆，但「開了牆的表是否四條規則齊全」（11 張只掛一條 `ALL`）「新加的表有沒有開」沒有測試守著；`oscal.*` 47 張沒牆的表，哪些是「裁定靠程式層」、哪些是「還沒輪到」，只有 M20-5 的盤點清單，沒有機器可讀的標記。
- **套件內的背景執行緒**（D3）：`test_scheduler_system_context.py` 只掃主專案 `core/scheduler.py`；jedi-* 套件自己起的背景作業（Drive 同步、webhook 續約、授權時鐘）有沒有都包了具名身分，沒盤過。
- **查詢逾時**（D6）：連線字串與基線都沒設 `statement_timeout`，一個卡住的查詢可以無限期佔住連線池的一格；T6 的 4 條修的是「別讓它卡」，沒有「卡了多久就斷」的兜底。
- **備份檔的保護**（D9）：升級備份落地明文、不清理；這輪掃描範圍是程式碼，備份與匯出的檔案層面沒看。
- **連線加密**（D8）：docker 內網明文是三條內部線（C05、C06、C07）共同的接受前提，但 SaaS 或客戶自備 DB（RDS 類）的部署型態下這個前提不成立，需要在那之前補 `sslmode=require`。

---

*依據：STRIDE 六頁信任邊界連線標記（CM-2403／2404 驗收後版本）、jedi-common `session/database/db.py`／`elevated_session.py`／`auth/system_context.py`、`scripts/init/00-cluster.sql`／`02-schema.sql`、`core/app_factory.py`、`core/scheduler.py`、`docker/production/docker-compose.yml`、`scripts/installer/install.sh` 升級備份段、DEV 庫 `pg_class`／`pg_policies` 實查（2026-10-02）、`test/test_rls_*.py`／`test_scheduler_system_context.py`／`test_project_scoped_tables_rls.py`／`test_upload_files_rls.py`。分組是依標題與「駭客怎麼打」歸納的，同一條若兩種手法都沾，歸主要那種（M10-4／M12-1 在 C02 歸「換編號讀別人專案」，這裡歸 T7 是因為它們落在沒牆的表上、資料庫不會兜底；M04-7／M19-3／M12-3 在 C02 歸 T2，這裡看的是資料庫端的負載）。*
