---
title: A2 檢查結果：資訊系統全鏈（jedi-asset）
---

# A2 檢查結果：資訊系統全鏈（jedi-asset）

> 檢查日期 2026-09-16｜對應卡片 CM-1815｜檢查範圍 42 個檔案

## 🔴 一句話結論

**找到 3 條發現、0 條高風險。** 其中 1 條（讀取端點不驗權限）與 A1 第 80 項是同一個產品決策、併進第 80 項；1 條（每頁筆數沒上限）是總表第 31 項的重複、不另計；**淨新增只有 1 條低風險**（只有「修改」權限的人也能把資訊系統「刪掉」），登記為總表第 81 項。都不急，但第 81 項修法很小，順手修掉即可。

## 這一棒在檢查什麼

這一棒看的是「資訊系統」這張清冊——客戶在產品裡登記自己有哪些資訊系統（名稱、負責人、機密性／完整性／可用性等級、授權邊界等），之後做稽核專案時會拿來引用。要確認的是：不該看的人看不看得到、不該改的人改不改得動、不同客戶之間有沒有隔好、合規文件引用這些資料時有沒有繞過隔離。

掃描目標 `/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-asset`，revision `cdb0d0f4f4c2`（branch `main`，工作區乾淨），mode `scan`，scope 42 個檔案（資訊系統模組全鏈＋它騎在上面的套件共用骨架 `api/guards.py`、`api/routing.py`、`common/error_code.py`、`domain/ports.py`、`plugin/*`；設備那半屬第 1 棒範圍，排除），effort `low`。**A1 與 A2 掃的是同一個版本 `cdb0d0f4`，兩棒之間 jedi-asset 零 commit、零漂移。**

## Coverage

`low` 強度：一位研究員讀完 42 個檔案就提報候選，未做元件盤點、未做威脅建模、未跑額外密鑰專項掃描，`completenessCheckOutcome` 為 `not-applicable`（低強度本就不跑盤點）。驗證跑了 1 輪，3 個候選去重後仍是 3 個，沒有候選遺失、沒有嚴重度被降低、沒有候選被駁回。研究員為了看懂脈絡另外讀了範圍外的兩處（主專案 `core/plugins/` 的插件接線、`jedi-common` 的 `PagerSchema`），但那兩處只當佐證、沒有納入稽核。

工具沒有實際執行任何程式碼：沒跑測試、沒發請求、沒示範攻擊，三條發現全部是讀原始碼推出來的。

**工具只碰到卡片列的五個重點裡的其中一個（①）。** ②③④⑤ 四個重點工具完全沒有提出任何候選，全部由首腦回頭開檔／連 DEV DB 唯讀查證補上，詳見下方「卡片重點逐項人工查證」段。

## Findings

### F1 — 資訊系統三支讀取端點只驗登入，未驗已宣告的 `information-system.read` 能力點（MEDIUM，confidence high）

**現況**：已修（M19-1，FR-114.1-6，套件 commit `0ecdd3a1`，1.21.0 出貨）

**這是什麼問題。** 只要登進這家客戶的帳號，不管角色有沒有被給「查看資訊系統」的權限，直接打 API 都拿得到整份資訊系統清冊。

**出事會怎樣。** 同一家客戶裡沒被授權的人（例如刻意限制過權限的外部稽核員、約聘人員），拿得到：系統名稱、描述、機密性／完整性／可用性等級、狀態、部署模式、授權邊界、系統負責人——等於把這家公司的資安態勢地圖交給不該看的人。跨客戶仍被 RLS（就是「每個客戶只能看自己資料」的資料庫隔離機制）擋住，所以是**同一家客戶內部**的外洩，不是跨客戶。

**要先有什麼才打得到。**
- 攻擊者已持有這家客戶的有效登入憑證（任何角色都行，包括一個資訊系統權限全空的角色）
- 插件以 `mount_api=True` 掛載（Guidant AI 宿主預設就是這樣，`core/plugins/asset.py:167`）

**在哪裡。** `jedi_asset/api/routes/information_system_route.py`：選單 `:45`（`InformationSystemMenuRoute.get`）、清單 `:69`（`InformationSystemListRoute.post`）、明細 `:105`（`InformationSystemDetailRoute.get`）三支只掛 `@auth_required`（只驗有沒有登入）；同一檔的寫入三支 `:82`（create）／`:114`（put）／`:135`（delete）每一支都多掛了 `@capability_required`（驗有沒有這個權限），形狀對照明顯。能力點本身在 `jedi_asset/plugin/contract.py:35` 有宣告、DB 也綁進 `route_capabilities`，但只有前端拿它來藏選單，API 層完全不驗。

**怎麼修。** 比照寫入端點的既有形狀：在 `AssetPluginConfig`（`jedi_asset/plugin/contract.py`）新增 `information_system_read_capability` 設定槽（預設 `information-system.read`），然後在上述三支讀取端點的 `@auth_required` 下面加 `@capability_required(lambda rt: rt.config.information_system_read_capability)`。**但這是產品決策，不是純技術修法**——`plugin/contract.py:27` 檔頭原話「`read` 兩項 BE route 不守（守門只在寫入類）」，這句同時涵蓋設備與資訊系統兩邊，是刻意的取捨。

**驗證。** 3/3 三位檢查員（reachability／impact／defenses）一致確認成立，嚴重度維持 MEDIUM。為什麼是 MEDIUM 不是 HIGH：要先有這家客戶的帳號、且只能看到自己客戶的資料。

**首腦核對註記與判定。** 屬實，逐檔開過。**但與 A1 第 80 項（設備讀取端點不驗權限）是同一個產品決策，不另計新項。** 處置：把總表第 80 項的描述從「設備清冊」擴為「設備與資訊系統兩張清冊」，補上資訊系統這三支的行號。要不要守，由決策者一次裁兩邊。

### F2 — 只有「修改」權限的人送 `is_active:false` 就達成「刪除」（LOW，confidence medium）

**現況**：已修（M19-2，FR-114.1-6，套件 commit `0ecdd3a1`）

**這是什麼問題。** 產品裡「刪除資訊系統」其實是把它標成停用（列還在），而「修改」端點也接受「停用」這個欄位，所以只被給了修改權限、刻意不給刪除權限的人，走修改那條路也能做到一模一樣的效果。

**出事會怎樣。** 一個只該改描述的操作員，可以讓任何一個資訊系統從選單、清單、稽核專案的挑選器裡消失——跟真的刪掉看起來一樣。「刪除」這個獨立權限點形同裝飾。好消息是可逆（資料列還在，改回來就好）、而且只在自己客戶內。

**要先有什麼才打得到。**
- 攻擊者持有這家客戶的帳號，角色有 `information-system.update` 但沒有 `information-system.delete`
- 知道目標系統的 `uid`（配合 F1，清單端點任何登入者都打得到，所以拿得到）

**在哪裡。**
- `jedi_asset/api/routes/information_system_route.py:124`（`InformationSystemDetailRoute.put`，只掛修改權限）
- `jedi_asset/api/serializers/information_system.py:59`：`InformationSystemUpdateSchema.is_active = fields.Boolean(load_default=None, allow_none=True)`——修改請求格式把 `is_active` 開放成可寫欄位
- `jedi_asset/infra/repository/information_system_repo_impl.py:127`：`model.is_active = entity.is_active`，直接寫進去
- 對照：delete 端點 `information_system_route.py:136` 做的也只是 `model.is_active = False`——兩個端點、兩種權限、同一個結果

**怎麼修。** 二擇一：① 從 `InformationSystemUpdateSchema` 拿掉 `is_active` 欄位（最小改動）；② put 收到 `is_active=false` 時改要求刪除權限（重新啟用時要求新增權限）。停用就是這個產品的刪除語意，不管從哪條路觸發都該由刪除權限守。

**驗證。** 3/3 三位檢查員一致確認成立，嚴重度 LOW。為什麼是 LOW：可逆、限自己客戶、要先有修改權限。

**首腦核對註記與判定。** **屬實，新發現。** 另查前端：修改頁 `InformationSystemManage.vue` 沒有送 `is_active`（只在清單篩選用 `is_active: true`），所以這是純後端旁路，正常操作 UI 不會誤觸。設備那半的更新格式沒有 `is_active` 欄位，不同病、不擴散。嚴重度低，首腦維持。**登記為總表第 81 項。**

### F3 — 每頁筆數（`page_size`）沒有上限（LOW，confidence medium）

**現況**：已修（M19-3，CM-2065：每頁筆數上限 1000）

**這是什麼問題。** 查清單時「一頁幾筆」由客戶端自己填，伺服器完全不檢查，填一億筆伺服器就真的去撈一億筆。

**出事會怎樣。** 登入的人反覆送超大 `page_size`，每一次請求都把這家客戶整張資訊系統表撈進記憶體三次（資料庫列、entity、DTO）再加一次使用者名錄查詢；幾個請求同時打，就能把 gunicorn worker 的記憶體與 CPU 耗光，讓共用同一組 worker 的其他客戶也變慢。**純可用性影響，不會洩資料。** 另外送負數會直接到資料庫變成無效的 `LIMIT`，回 500。

**要先有什麼才打得到。**
- 任何一個有效登入憑證（清單端點只驗登入，見 F1）
- 影響大小取決於這家客戶有多少筆資訊系統（DEV 現況 91 筆，實際客戶量級可能更大）

**在哪裡。** 消費點 `jedi_asset/infra/repository/information_system_repo_impl.py:60` 的 `.limit(page_size)`；根因在 `jedi-common/jedi_common/interfaces/schema/common.py:13`：`PagerSchema.page_size = fields.Int(missing=25)` 沒有 `validate.Range`（同檔的 `page` 欄位有，`page_size` 漏了）。

**怎麼修。** 在 `PagerSchema.page_size` 加 `validate.Range(min=1, max=<上限>)`，**一處修全站**——所有繼承 `RequestMetaSchema` 的清單端點都會一起收斂。

**驗證。** 2/3 通過：reachability 與 defenses 兩位確認，impact 那位認為路徑是真的但可用性後果太有限、不值得報，所以 confidence 是 medium 不是 high。異議在影響程度、不在程式路徑。

**首腦核對註記與判定。** 屬實，**但根因是共用底層，不是資訊系統專屬**——**這是總表 §3.1 第 31 項的重複**（FR-085 C3-2，同一行 `jedi-common/jedi_common/interfaces/schema/common.py:13`，中等，已登記），標「重複第 31 項，不另計」。補一句佐證：資訊系統這條分頁路徑與設備那條（`device_repo_impl.py:93/114` 直接 `.limit(pagination.page_size)`）都吃到同一個缺口，證明第 31 項影響的確是全站。

## What was verified

低強度單研究員掃描提報 3 個候選，去重後仍 3 個；三位檢查員各自獨立對三個候選投票，共 9 票全投出、沒有漏投。F1、F2 各 3 票全過，F3 2 票過 1 票異議（異議在嚴重度不在路徑）。沒有候選被駁回、沒有嚴重度被降低。驗證章狀態 `verified`，無拒收理由。

---

## 卡片重點逐項人工查證

🔴 **同 A1：低強度工具只跑一位研究員，卡片列的五個重點只碰到一個。逐項回頭人工核對如下。**

### ① 讀取端點沒有權限檢查（**工具已報＝F1**）

- 三支讀取端點（選單 `:45`、清單 `:69`、明細 `:105`）全部只有 `@auth_required`，一致無例外；寫入三支全部有 `@capability_required`。與 F1 描述一致。
- 補充：明細端點吃 `uid`（UUID 格式，不可枚舉），要一筆一筆拿得先透過清單端點拿到 `uid`；跨客戶被 RLS 擋住。**已按規則併入 A1 第 80 項。**

### ② 兩張表的隔離模式差別（**工具未報，人工查證**）

- DEV 唯讀實查（2026-09-16 13:02）：`information_systems` 表 `relrowsecurity=t`、**`forcerowsecurity=t`**（有開隔離、且開了「強制」模式）；`devices` 表 `t / f`（有開隔離、沒開強制）。兩表各 4 條 policy（select／insert／update／delete）；資訊系統表目前 91 筆。
- 資訊系統這四條 policy 全部是「super_admin 逃生門 OR 本客戶」——只看 tenant（客戶）維度，**沒有 org（部門）維度**，四條範圍一致、沒有 A1 ④ 那種新增比查改刪窄的不對稱。
- 強制模式在這張表上沒有副作用：強制模式只影響「表擁有者自己查」的情境，而應用程式實際連線角色是 `cm_app`（非擁有者），一般模式本來就已生效——設備表現況見 A1 ③，同一個結論。**兩張表的差別是設計取捨、不是漏洞。**

### ③ 新增規則少部門維度會不會寫到別人部門（**工具未報，人工查證**）

- 新增／修改的請求格式**沒有** tenant／org 欄位（serializer 與 entity 都無），`tenant_id` 由 `TenantScopedMixinModel` 從連線身分自動填入，客戶端填不了。
- insert policy 是 `WITH CHECK (super_admin OR app_tenant_allowed_for_session(tenant_id))`，所以新增範圍＝讀取範圍＝本客戶。
- 結論：**沒有「寫到別人部門」的路，不成立。** 少了 org 維度只代表同客戶內不分部門，這與讀取端一致，不是缺口。

### ④ 合規文件引用資訊系統的路徑有沒有繞過隔離（**工具未報，人工查證**）

- 主專案 `app/oscal/service/ssp_resources_context_service.py:148-156` 用 `InformationSystemQueryEntity(_in_id=ids)` 呼叫套件的 `information_system_service.get_information_systems`（`@transaction`），走的是同一個 repo、同一套 RLS，**沒有另開一條裸查詢繞過**。這條沒有問題。

### ⑤ 送空條件會不會回整張表（**工具未報，人工查證，命中已知共用線**）

- `InformationSystemQueryEntity` 十個欄位全選填、`to_dict()` 只回有值的條件；清單端點送空 filters 會落到 `jedi-common` 共用底層 `get_all_by_fields_and_pager` 回本客戶整表（分頁內）。
- **這與跨 arc 總表 §3.1 第 70 項是同一根因**（源頭在 `jedi-common` 的 `base_repository_impl.py`，所有套件共用），**歸第 70 項，不另計**。真正擋線的是 RLS（見②）。

### 附：共用骨架守門殼（**A1 ⑤ 結論適用**）

- `plugin/assembly.py:74-90` 的 `_assert_api_wiring` 在掛 route 前檢查 `auth_required`／`capability_required`／`current_user_login_name` 三個必填欄位，任一缺了直接 `RuntimeError` 拒絕掛載。資訊系統這半與設備共用同一個殼，A1 ⑤ 的結論（正確的「吵鬧拒絕」設計，不是漏洞）直接適用。

---

## A1 已查的四件有沒有被重報

A1 開卡前與掃描時已經查證過四件事，本棒逐一核對工具有沒有把它們當新發現重報：

| A1 已查 | 本棒工具 | 處置 |
|---|---|---|
| 讀取端點不驗權限＝同一個產品決策（第 80 項） | **重報了**（F1） | 併第 80 項，描述擴為兩張清冊 |
| 沒開強制隔離不是漏洞（設備表） | 未報 | 資訊系統表反而有開強制，②人工核對無副作用 |
| 送空條件回整表歸第 70 項 | 未報 | ⑤人工核對，仍歸第 70 項 |
| 隔離與權限點層級正確（八個能力點全是客戶層級） | 未報 | 不變 |

**工具只重報了第一件，已按規則併入第 80 項，沒有膨脹計數。**

---

## 這份結果可信到什麼程度

### 「工具報的這三條是真的嗎」→ ✅ 可信

9 票全投出、沒有漏投：F1 與 F2 都是 3:0，F3 是 2:1，而那一票異議是「影響太小不值得報」、不是「路徑不存在」。首腦逐條開檔核對（route 的 decorator、serializer 的欄位、repo 的 `.limit()`、`jedi-common` 的 `PagerSchema`）全部對得上。

### 「只有這三條嗎」→ ❌ 不可信

最低強度快篩、一位研究員、不做盤點不做威脅建模。卡片五個重點工具只碰到一個（①），②③④⑤ 全部由首腦回頭開檔並連 DEV DB 唯讀查證補上——人工查證的結論是四項都沒有新問題，但這是人工補的結論，不是工具給的。與 FR-097 L1／L2、A1 撞到的教訓一致。

---

## 執行概況

| 項目 | 數字 |
|---|---|
| 檢查範圍 | 42 個檔案（資訊系統半＋共用骨架；設備半排除） |
| 檢查強度 | 最低（`low`），未設定 focus |
| 候選問題 → 去除重複 | 3 → 3 |
| 投票數 | 9（3 條候選 × 3 位檢查員），全數投出 |
| F1 / F2 / F3 投票結果 | 3:0（MEDIUM）/ 3:0（LOW）/ 2:1（LOW） |
| 被駁回候選 | 0 |
| 沒投到票的 / 投票中斷的 / 被降低嚴重度的 | 0 / 0 / 0 |
| 研究員派出 / 回收 | 1 / 1 |
| 驗證章狀態 | `verified`（無拒收理由） |
| 掃描耗時 | 3374 秒（約 56 分） |
| 掃描當下的程式碼版本 | commit `cdb0d0f4f4c2`，branch `main`，工作區乾淨（與 A1 同版本，零漂移） |
| scan_id / workflow run | `6c5e9744-a425-47e0-bc09-fa0a7419a05a` / `wf_b79d51ef-5d0` |
| 首腦人工補查項目 | 卡片五個重點逐項查證，②③④⑤ 均無新問題 |
| 對總表的影響 | 淨新增 1 條低（第 81 項）；§3.1 80→81、§3 97→98、risk-overview 低 37→38（資安類 20→21）、共 112→113 |

---

## 交付狀況

🔴 **本棒 runner 四件全未交**：沒有寫報告、沒有 commit、沒有回寫 Notion（CM-1815 仍停在「未開始」）、沒有回報。工具產物（stamp 與 `CLAUDE-SECURITY-RESULTS.md`）是首腦到掃描目錄自行撈出來的。**本報告由首腦補寫，五個卡片重點的人工查證亦由首腦完成，這是全計畫第八次 runner 交付掛零。**
