A2 檢查結果:資訊系統全鏈(jedi-asset)

A2 檢查結果:資訊系統全鏈(jedi-asset)

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

§1

🔴 一句話結論

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

§2

這一棒在檢查什麼

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

掃描目標 /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、零漂移。

§3

Coverage

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

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

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

§4

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 項影響的確是全站。

§5

What was verified

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


§6

卡片重點逐項人工查證

🔴 同 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 ⑤ 的結論(正確的「吵鬧拒絕」設計,不是漏洞)直接適用。

§7

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

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

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

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


§8

這份結果可信到什麼程度

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

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

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

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


§9

執行概況

項目 數字
檢查範圍 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

§10

交付狀況

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