U1 檢查結果:權限檢查共用層+共用小工具(全產品守門地基)

U1 檢查結果:權限檢查共用層+共用小工具(全產品守門地基)

檢查日期 2026-09-25|對應卡片 CM-2153(母卡 CM-2152)|檢查範圍 15 個檔案 1,132 行|工具 run ID wf_444cc623-9d2

§1

🔴 一句話結論

地基是穩的。前面 40 棒一直假設「權限檢查共用層沒問題」,這次逐支查過,這個假設大致成立。 範圍內找不到能讓人越權、跨客戶、或繞過商務授權的洞。卡片最擔心的三件事查核結果如下:

  • 授權檔(license.py)在「讀不到照/過期/被竄改」時會擋下還是放行? 會擋下。讀不到照就當成「什麼模組都沒買」,這個機制叫 fail-closed(出錯時預設拒絕)。過期的照只能讀、不能寫。被竄改的照在上傳那一刻驗簽章就被擋掉。快取只活在同一個請求之內,換一個請求、換一家客戶就重查,不會拿到上一家的結果。
  • 決策表(__init__.py)的五支轉接檔有沒有指到舊地方? 沒有。我逐一載入確認,五支全部指到身分套件 jedi_iam.authz 的正牌實作。四條舊的守門路徑也已經從程式庫裡刪光,沒有人在用。
  • 簽章短期憑證的「tenant_id=None 換成最高管理員」到底是不是刻意設計? 這個說法已經過時。那條路 9 月 14 日已經被拆掉(commit 9a0b4893,CM-1787),目前出貨釘住的身分套件 1.3.0 已經包含這個修正。現在的行為是:憑證換回的是發憑證那個人自己的身分,查不到人一律拒絕,沒有任何一條路會把「缺值」推成「超級管理員」。

掃描工具這一棒只報了一條:Redis 快取連線打開加密時不驗證對方憑證(中等,三位檢查員 3:0 通過)。這一條總表已經登記過了(第 19 項,FR-085 C1-4,同一個檔、同一行),本棒不另外計數。

我依卡片逐支開檔,另外記下三件低度或資訊級的觀察,都不需要緊急處理:

  1. 授權的「總開關」只是一個環境變數。 把 LICENSE_ENFORCEMENT_ENABLED 改成 false,商務授權就整個熄火。這是刻意的上線保險絲;對能改部署設定的人來說,要不要把它視為風險是產品決策。
  2. 到期狀態只靠每天凌晨 2 點的排程往前推。 排程如果停了(例如服務只開即時通訊模式、或 license 模組沒掛上),過期的照會一直停在「正常」,不會轉成唯讀。停掉排程時 log 會記 ERROR,不是靜默失效。
  3. 稽核輪次的「過階段就唯讀」檢查,本身不問「這一輪是不是你的專案」。 目前唯一的呼叫端在同一支方法裡緊接著檢查了角色,所以沒有漏洞。但這支檢查的名字容易讓後來的人以為它包含歸屬檢查。
§2

這一棒在檢查什麼

整個產品「誰能做什麼」的判斷,大部分都經過 common/authz/ 這個目錄。後面每一棒的研究員都會追進來讀,但只有這一棒會報它本身的問題,所以它是地基。

這一層把權限檢查分成六「軸」,每一軸回答不同的問題:

軸 回答什麼 實作在哪
① 平台管理員 你是不是原廠總部的人 身分套件(本棒只看轉接)
② 超級管理員 你的帳號有沒有被標成超級管理員 身分套件(本棒只看轉接)
③ 專案角色 你在這個專案/SSP/流程裡是什麼角色 主專案 project.py/ssp.py/workflow.py(不在本棒範圍,別棒讀過)
④ 功能權限 你的角色有沒有被授予這個功能點 身分套件(本棒只看轉接)
⑤ 簽章短期憑證 瀏覽器直接開的網址(圖片、下載)帶的憑證是不是真的 身分套件(本棒只看轉接)
⑥ 商務授權 你們公司有沒有買這個模組、照是不是過期了 主專案 license.py,本棒重點

範圍內 15 支檔案:

檔案 行數 角色
common/authz/license.py 367 第⑥軸:模組授權檢查、過期唯讀判斷、子公司數量上限
common/authz/__init__.py 180 決策表+全站唯一的權限檢查匯入口
common/iam_ports.py 100 身分套件要的三個零件:讀設定、寄信、組驗證碼信
common/util/redis_client_util.py 79 連 Redis 快取的共用工具
common/authz/menu_license_filter.py 76 依授權把沒買的模組從側邊選單藏起來
common/util/common_util.py 65 雜項工具(台北時間等)
common/util/ssp_resource_name_dup.py 56 SSP 資源同名檢查
common/util/resource_path.py 50 打包後找隨版資料檔的路徑
common/util/round_guard.py 46 稽核輪次「過階段就唯讀」
common/authz/sharing.py 35 母公司分享資源給子公司時,「誰能改分享設定」
common/authz/capability.py/signed_token.py 22/22 轉接檔(→ 身分套件)
common/authz/admin.py/decorators.py/platform.py 12/11/11 轉接檔(→ 身分套件)
§3

掃到什麼(總覽)

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 該補檢查的位置 嚴重度+為什麼 來源
F1 Redis 快取的加密連線不檢查「對方是不是真的伺服器」 在網路中間攔截的人可以拿到 Redis 帳密,並偷看或竄改 AI 聊天紀錄、問卷共編名單 ① 部署時把 REDIS_SSL 打開(預設是關的)② 攻擊者站在應用程式和 Redis 之間的網路上 common/util/redis_client_util.py:32(改成強制驗證憑證並檢查主機名稱) 🟡 中:要同時滿足兩個不常見的前提 工具,三位檢查員 3:0 通過;=總表第 19 項,不另計
U1-1 商務授權的總開關只是一個環境變數 能改部署設定的人把它改成 false,所有模組都不再檢查有沒有買、過期也不再轉唯讀 能改伺服器上的 .env 或容器設定(等於已經是主機管理員) common/authz/license.py:177-179(產品決策:要不要把這個開關納入防竄改檢查,或讓它的狀態回報給原廠) ⚪ 資訊:這是刻意設計的上線保險絲,而且門檻是主機管理員權限;列出來讓決策者知道有這回事 runner 自行開檔(未經三人面板)
U1-2 過期轉唯讀只靠每天一次的排程,排程停了照就不會過期 照已經過期,客戶照樣可以新增、修改資料,直到排程恢復 排程沒在跑。例如只開即時通訊模式的部署,或 license 模組沒掛上 讀照的時候順便用當下時間算一次狀態(common/authz/license.py:280-283),不要只信資料庫裡存的狀態 🟢 低:正常部署排程一定會跑;停掉時 log 有 ERROR,不是靜默失效;延遲最多一天 runner 自行開檔(未經三人面板)
U1-3 「過階段就唯讀」這支檢查只看狀態,不看這一輪屬於誰 目前沒有後果。以後有人只呼叫這一支、以為它也檢查了歸屬,就會漏掉 目前沒有可打的路徑 common/util/round_guard.py:31(在說明裡寫明「不含歸屬檢查,呼叫端要另外檢查角色」) ⚪ 資訊:唯一的呼叫端緊接著就檢查了角色 runner 自行開檔(未經三人面板)

現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。

  • 上表 F1(Redis 憑證)=總表第 19 項,✅ 已修(FR-114.3-1/CM-2043,commit d61d5342d,主專案那份複製品已刪)。
  • U1-1(授權總開關)=總表第 182 項,✅ 已修(CM-2205,commit c8aa7d026,1.21.0 出貨)。
  • U1-2(過期只靠排程)=總表第 181 項,✅ 已修(CM-2217,commit 3ed47c7a9,1.21.0 出貨)。
  • U1-3(round_guard 只看狀態):資訊項,本來就只登記、不開卡,無修正卡。
§4

工具報的逐條

F1 — Redis 加密連線不驗證憑證(中等,3:0)

這是什麼問題。 部署方把 REDIS_SSL 打開,表示「連 Redis 要走加密連線」。但 common/util/redis_client_util.py:32 把 ssl_cert_reqs 寫死成 False,意思是「對方拿什麼憑證出來都接受」,也沒有檢查主機名稱。加密還是加密了,只是不確定對面是誰。

出事會怎樣。 在應用程式和 Redis 之間攔截的人可以冒充 Redis,應用程式會把 Redis 的帳號密碼送給他,之後讀寫的 AI 聊天紀錄(core/plugins/ai_bot.py 的 RedisChatHistoryStore)、問卷共編線上名單(infra/survey/adapters.py 的 RedisPresenceStoreAdapter)也都經過他。拿到帳密之後,他還能直接登入真正的 Redis。

要先有什麼才打得到。

  • REDIS_SSL=true(config/config.py:146 預設是 false,出貨的落地版也沒打開)
  • 攻擊者要在應用程式和 Redis 之間的網路上

修法。 打開加密時改成 ssl_cert_reqs="required" 加上 ssl_check_hostname=True,並提供自帶 CA 憑證的設定。身分套件裡同名的那支已經修好了(jedi-iam/jedi_iam/common/utils/redis_client_util.py:44),而且附了現成的測試,照抄就可以。

和總表的關係。 這條就是總表第 19 項(FR-085 C1-4,當時是研究員越界讀到,沒有經過面板)。這次是它第一次在自己的範圍內被正式掃到、並經過三人面板 3:0 確認。不另外計數,只是讓第 19 項多了一次面板背書。總表第 19 項另外提到的 config/config.py:157 的 REDIS_URL 不理會 REDIS_SSL 開關,不在本棒範圍,這次沒有重新核對。

我自己開檔核對過:redis_client_util.py:25-34 整段、兩個呼叫端、身分套件的對照組都看過,跟工具說的一致。

§5

卡片點名的疑點逐條回答

① license.py:授權檔讀不到、過期、被竄改時是擋下還是放行?快取多久?換客戶會不會拿到上一家的結果?——✅ 擋下,不成立

逐種情況:

情況 結果 依據
這家客戶沒有照 擋下。當成「什麼模組都沒買」,掛了授權檢查的端點一律 403 license.py:163-164 回傳「無照」快照,:245 無照的模組清單是空的
請求裡沒有身分(還沒登入) 擋下,同上 license.py:221-226 沒有 tenant_id 就回「無照」快照
子公司自己沒照、上層有照 用上層的照(刻意設計:一張照涵蓋整棵公司樹) license.py:161 先換算成頂層持照公司,再查照
子公司讀上層的照,會不會被資料庫的隔離規則擋掉、變成「沒照」? 不會。隔離規則允許讀自己路徑上的祖先公司 DEV 唯讀查 tenant_licenses_tenant_isolation:tenant_id = ANY(路徑上每一段),涵蓋祖先
照已過期 依設定轉成寬限/唯讀/鎖定。唯讀和鎖定時,所有寫入請求都擋 license.py:69、:263-283;全域寫入閘門 common/middleware/license_readonly_mw.py:208
被原廠手動停權 唯讀(停權和過期是兩個獨立來源,取聯集) license.py:283
照被竄改 上傳那一刻驗簽章,改過的照進不來;上傳入口還順便跑一次防竄改檢查 套件 license_verification_service.py:74-89;接線 core/plugins/license.py:339
快取活多久 只活在同一個請求之內(存在 Flask 的 g,每個請求都是新的) license.py:217-228
換客戶會不會拿到上一家的結果 不會。每個請求重新查;背景工作每次都開新的 app context,也不會共用 同上;全專案查過 set_user_context 的呼叫點,沒有「請求中途換身分」的情況
超級管理員能不能繞過 不能。只有原廠總部(root 公司)能豁免,這是刻意的:否則客戶把自己設成超級管理員就能不買照 license.py:19-22、:241 只認 viewer_is_platform_admin()

子公司數量上限(assert_sub_tenant_quota,license.py:326-367):算的是整棵樹的子孫總數,不是直屬子公司數。算不出來時擋下,不放行(:355-360)。唯一呼叫端是身分套件開新公司的流程(core/plugins/identity.py:329),只在「建子公司」時檢查;建最上層公司要平台管理員(jedi_iam/api/routes/tenant_route.py:73-74)。

附帶觀察(不成問題):「開新公司時要不要扣掉沒買的模組權限」這條路,無照時刻意放行(core/plugins/identity.py:283)。理由寫得很清楚:開通當下通常還沒照,這裡多給的權限在無照期間打不動任何一支端點。這是「給得寬、判得嚴」,不是 fail-open 漏洞。

→ 另外兩件相關觀察見 U1-1(總開關)、U1-2(排程)。

② __init__.py 決策表:五支轉接檔有沒有哪一支名字對了、實作卻指到舊路徑?——✅ 沒有,不成立

我在本機直接載入每一支轉接檔,印出每個名字實際來自哪個模組:

轉接檔 轉出的名字 實際來自
admin.py require_super_admin、viewer_is_super_admin jedi_iam.authz.admin
capability.py CapabilityGuard、require_capability、set_guard_resolver、viewer_has_capability jedi_iam.authz.capability
decorators.py require_platform_admin_route、require_super_admin_route jedi_iam.authz.decorators
platform.py require_platform_admin、viewer_is_platform_admin jedi_iam.authz.platform
signed_token.py issue_signed_token、verify_signed_token、signed_token_required、signed_token_or_jwt jedi_iam.authz.signed_token

__init__.py 自己轉出的 35 個名字也全部核對過:主體域(軸①②④⑤)全部來自 jedi_iam.authz,資源域(軸③)和商務授權(軸⑥)來自主專案自己的 common.authz.*,跟決策表寫的分工一致。

舊路徑:決策表說有四條舊路徑(common/util/permission、common/util/participant_guard、common/middleware/permission/ssp_permission、common/util/workflow_project_guard)改成了相容轉接。我查了:四條都已經從程式庫刪掉,全專案沒有任何一處還在引用它們。決策表第 3-6 行的這段描述已經過時,但不影響安全。

③ sharing.py:「分享給子公司」會不會被反過來用——子公司改到母公司的資源?——✅ 不會,兩道防線,不成立

分享的設計是:母公司把自己的範本標成「分享」(SHARED),子公司就看得到。問題是子公司看得到之後,能不能改。

第一道:資料庫。 我在 DEV 唯讀查了四張有「分享」的表(module_frames、flow_templates、workflow_templates、detection_profiles)的修改和刪除規則:分享分支只加在「讀」的規則上,改和刪的規則仍是 app_tenant_allowed_for_session(tenant_id),意思是「資料要在我自己或我底下」。母公司的資料在子公司的上游,所以子公司看得到、改不了。migration 檔頭(scripts/sql/2026-09-14-fr094-cm1790-shared-scope-subtree.sql)也明寫了這個設計。

第二道:程式。 assert_scope_writable(sharing.py:25-35)檢查「現在操作的公司」是不是「資源擁有的公司」,完全相等才准改分享設定;而且不准把任何東西改成「原廠公版」(SYSTEM)。

要特別講清楚的一點:這支檢查只管「改分享設定」這件事,不管「改內容」。四個呼叫端(module_frame_service.py:112、module_frame_item_service.py:65、flow_template_app_service.py:89、core/plugins/detection.py:188)都把它放在「分享設定有變」的 if 裡面。總表已經登記過好幾條「只改內容、不改分享設定,就繞過這道檢查」(第 120、134、137 項,以及第 67 項)。這些是呼叫端放錯位置,不是 sharing.py 本身寫錯,本棒不重複計數。

④ round_guard.py:只看狀態,還是也看「這一輪屬於你的專案」?——🟡 只看狀態,但目前沒有漏洞 → U1-3

assert_round_phase(round_guard.py:31-46)只看輪次的狀態,不問這一輪屬於誰。

目前主專案只有一個呼叫端:app/flow_control/service/assessment_plan_app_service.py:207。它在同一支方法裡緊接著呼叫 self._check_auditor(rnd.project_id, curr_user_id)(:208),檢查「你是不是這個專案的稽核員」。所以現在沒有漏洞。

套件 jedi-compliance-audit 有一份內容一模一樣的複製品(只有匯入路徑不同),給稽核結果和改善計畫用。那兩處的呼叫端不在本棒範圍,沒有核對。

另外兩個小觀察(不列條):狀態值是空的(資料異常)時放行(:42);輪次找不到時也直接放行,讓呼叫端去處理「找不到」。這兩個設計都有寫明理由,而目前的呼叫端在前面就先處理了「找不到」。

⑤ redis_client_util.py:鍵名有沒有帶客戶編號?跨客戶會不會讀到彼此的快取?——✅ 不會跨客戶(但有 F1)

這支工具本身不決定鍵名,是呼叫端決定的。我追了兩個呼叫端:

誰在用 鍵名長什麼樣 會不會跨客戶
AI 聊天紀錄(jedi-ai-bot 套件) chat_history:{使用者編號}:{對話編號} 不會。使用者編號是伺服器從登入身分取的(ai_bot_route.py:59 的 current_user_id()),每個人全系統唯一
問卷共編線上名單(jedi-survey 套件) fill-survey:{房間編號}:users 鍵名本身不帶客戶編號,但房間編號是問卷的唯一編號,本來就不會撞。真正的問題是「任何人都能進任何房間」,總表已記(FR-109 V2 F7;線上名單用前端自填的名字,FR-115 W1-1)

所以「鍵名沒帶客戶編號導致跨客戶讀到彼此快取」不成立。這支工具真正的問題是 F1(不驗證憑證)。

⑥ signed_token.py 的 tenant_id=None 換 super admin(S2 那棒 docstring 說刻意設計,但沒人驗過)——✅ 這條路已經被拆掉,不成立

先講結論:現在的程式碼裡,沒有任何一條路會把 tenant_id=None 變成超級管理員。

這條路以前是怎麼走的。 簽章短期憑證(例如 <img src="...?st=xxx">)只帶使用者編號,不帶公司。以前的做法是把公司留空,而資料庫那一層「公司是空的就當超級管理員」,於是一張憑證就能換到全資料庫的可見範圍。

什麼時候拆掉的。 jedi 套件 commit 9a0b4893(2026-09-14,CM-1787,「拔掉『tenant_id 空值=超級管理員』」)。我確認過這個 commit 已經包含在主專案目前釘住的 jedi-iam==1.3.0 裡(1.3.0 的版號 commit 573e0b7d 是它的後代)。

現在怎麼走(jedi_iam/authz/signed_token.py:85-133):

  1. 驗簽章、驗用途(scope)、驗是否過期(預設 120 秒)
  2. 用憑證裡的使用者編號,開一個只能查這一句、查完就關的提權連線,把那個人查出來
  3. 查不到人 → 401;資料庫還沒準備好 → 401;不會退回「沒有公司」的身分
  4. 用那個人自己的預設公司組出完整身分(tenant_id_override=None 在這裡的意思是「不讓網址指定公司」,而不是「公司留空」)

資料庫那一層也改了(jedi_common/session/database/db.py:188-218):「要不要繞過隔離」現在只看兩個明確訊號:系統排程身分,或身分本身就在原廠總部的路徑上。完全沒有身分時,什麼都看不到。

S2 那棒 docstring 說的「刻意設計」,指的是「憑證不帶公司、由伺服器查人決定」這個設計,不是「空值換超管」。後者已經被當成漏洞拔掉了。

附帶一個小觀察(不列條):用簽章憑證進來時,不會檢查帳號是不是已經被停權。一般登入路徑會檢查(jedi_iam/middleware/core.py:90),憑證路徑直接用 build_user_context 組身分,沒有這一步。影響窗口只有憑證的 120 秒有效期,而發憑證本身需要當時有效的登入。這個在身分套件裡、不在本棒範圍,列給 FR-114 修正線參考。

⑦「有人守了一半」共通疑點

疑點樣式 本棒結果
只驗「你是誰」沒驗「這筆是不是你的」 🟡 兩處屬於這種形狀,但不是本棒的漏洞:sharing.py 只管分享設定(呼叫端放錯位置已登記在總表);round_guard.py 只看狀態(唯一的呼叫端有補上,見 U1-3)
列表有守、單筆沒守 不適用:這一層是被呼叫的零件,沒有列表/單筆端點
守門寫在 route 層,但有第二支路由沒掛 不在本棒範圍:要看每支 route 有沒有掛 @require_license,要逐支盤點。主專案有 69 處掛授權檢查、68 處掛功能權限,套件裡另有 35 支檔案掛授權檢查
守門條件用 or 串、其中一個恆真 ❌ 不成立:viewer_module_licensed 的四個放行條件(總開關關閉/原廠總部/基礎設施類/照裡有)逐一看過,沒有恆真的
「查不到」跟「沒權限」混成同一個回應 刻意分開:沒買回 LICENSE_403001、過期唯讀回 LICENSE_403002,兩者出路不同(license.py:263-268)
背景排程/系統身分假設呼叫者是自己人 ❌ 不成立:到期排程用 system_context() 明確宣告要繞隔離,只更新狀態、不擋任何操作
防重放/計次只在單一進程內有效 ❌ 不成立:快取是單一請求範圍;到期狀態轉換用資料庫的條件更新(WHERE status = 原狀態),多台機器同時跑也只會生效一次
註解寫「這裡刻意不檢查」但上一層沒檢查 🟡 一處描述過時:決策表說四條舊路徑「已改相容轉接」,實際已經刪光。不影響安全

⑧ 範圍內其他檔案通讀結果

檔案 結論
menu_license_filter.py 只是把沒買的模組從選單藏起來,不是防線。真正擋人的是 API 層的 @require_license。沒有掛功能權限的頁面一律保留,這是刻意的:藏錯了比露出來更糟,而且露出來也打不動 API
iam_ports.py 三個零件:讀環境變數、非同步寄信、組驗證碼信的文字。寄信失敗只寫 log、不讓登入失敗(刻意取捨,有寫理由)。沒有安全問題
common_util.py 大部分是轉接到 jedi_common;留下的是台北時間工具和問卷選項文字組合。domain_regex 全專案沒有人用。沒有安全問題
ssp_resource_name_dup.py 純比對,由呼叫端傳入「同一份 SSP」的清單,本身不查資料庫。沒有安全問題
resource_path.py GUIDANT_RESOURCE_ROOT 環境變數可以換掉資料檔根目錄,總表已記(第 15 項),不另計
§6

各條詳細

U1-1 — 商務授權的總開關只是一個環境變數(資訊)

這是什麼。 license.py:177-179 讀 LICENSE_ENFORCEMENT_ENABLED。只要是 false,模組授權、過期唯讀、子公司數量上限全部放行。第二個開關 LICENSE_READONLY_GATE_ENABLED 只關過期唯讀。

為什麼存在。 檔頭寫得很清楚:上線後如果誤擋了真實客戶,改一個環境變數就能整個熄火,不用回滾程式碼。安裝程式出貨時兩個都寫 true(scripts/installer/install.sh:1420-1421)。

為什麼還是列出來。 落地版是裝在客戶自己的機器上。能改 .env 的人(客戶的主機管理員),不必動任何程式碼、也不會觸發防竄改檢查,就能把授權整個關掉。防竄改(FR-064)驗的是程式檔,不驗環境變數;FR-064 的 design 也寫明「主機層審計是客戶責任範疇」。唯一留下的痕跡是系統診斷包會回報這兩個開關的狀態(app/support/service/diag_bundle_app_service.py:209-215)。

要不要處理是產品決策,不是程式錯誤。可以考慮的方向:開關關閉時在畫面或送回原廠的資料上留下明顯標記;或者落地版的關閉需要一張原廠簽發的解鎖憑證(FR-064 已經有「unlock」這種憑證類型)。

U1-2 — 過期轉唯讀只靠每天一次的排程(低)

這是什麼。 照是不是過期、該不該唯讀,看的是資料庫裡存的 status 欄位(license.py:283)。這個欄位只有兩個時間點會更新:

  1. 上傳照的那一刻:用當下時間算一次(套件 license_verification_service.py:192-198)
  2. 每天凌晨 2 點(UTC)的排程(core/scheduler.py:275-323)

讀照的時候不會用當下時間重算。

出事會怎樣。 排程沒在跑的期間,過期的照會一直停在「正常」,客戶照常新增、修改資料。

什麼情況下排程會沒在跑。

  • 服務只開即時通訊模式(RUN_MODE=socketio):這個模式本來就不起排程(main.py:142),但正常部署會另外有一個 API 模式的服務在跑排程
  • license 模組沒掛上:排程會跳過,但會記 ERROR(core/scheduler.py:296-304),不是靜默
  • 排程本身出錯:記 exception

為什麼是低。 正常部署一定有排程在跑,延遲最多一天;停掉時 log 有 ERROR。修法是在讀照時用 compute_target_status(now, expires_at, expiry_policy) 當場算一次,取「存的狀態」和「當場算的狀態」中比較嚴格的那個。這個函式是純函式、不查資料庫,成本很低。

附帶:改系統時間能不能延長照? 上傳照時有「時鐘回撥」防護(用浮水印記住見過的最大簽發時間),但排程推算用的是機器當下時間,沒有對照浮水印。能改系統時間的人等同主機管理員,跟 U1-1 同一個門檻,一併列在這裡、不另列一條。

U1-3 — 「過階段就唯讀」只看狀態、不看歸屬(資訊)

見上方 ④。建議在 round_guard.py 的說明裡加一句:「本函式只判斷階段,不判斷這一輪屬於誰;呼叫端必須另外檢查專案角色」。

§7

可信度(分兩層看)

第一層:工具正式報告(經過三人面板投票)

  • 驗證章 verified:1 條候選、3 票全數投出、1 條成立(3:0;一位評低、兩位評中,取中等)、零漏投、零中斷。
  • 研究員 2 派出/2 交回(全範圍 1 位+找寫死密碼的專掃 1 位),failed 0。
  • 工具只報了 F1。工具對「權限邏輯」這一層零候選,不代表這層乾淨。這一層大部分是轉接和分工設計,真正的判斷要跨到身分套件、授權套件、資料庫規則才看得出來,工具的研究員在低 effort 下不會追那麼遠。

第二層:runner 自行開檔核對(未經三人面板投票)

  • 範圍 15 支逐支通讀;卡片「重點看什麼」五條+交辦時點名的 signed_token 疑點+「有人守了一半」八種樣式逐條答完。
  • 跨讀了範圍外的相關程式(只讀不報):身分套件的 signed_token.py、platform.py、middleware/context.py、middleware/core.py、tenant_route.py、tenant_provisioning_service.py;授權套件 jedi_license_runtime 的讀照、驗章、到期狀態機、頂層公司解析、三張表的隔離規則;jedi_common 的 session_scope;主專案的全域唯讀閘門、到期排程、分享和輪次檢查的所有呼叫端、Redis 的兩個呼叫端。
  • 用本機 Python 載入每一支轉接檔,印出實際來源模組(見 ②)。
  • DEV 資料庫唯讀查證(2026-09-25 約 11:50 +08):四張分享表與兩張授權表的修改/刪除隔離規則(pg_policies)、最上層公司清單。全程只有 SELECT。
  • 用 git 確認 CM-1787 修正包含在主專案釘住的 jedi-iam==1.3.0 裡。
  • 打折處:
    • 本機讀到的 jedi 套件原始碼來自修正線的 worktree(.claude/worktrees/jedi-wt-fix-security),因為 BE 的虛擬環境是用開發模式指向那裡。版號是 1.3.0,和 pyproject.toml 釘的一致,但修正線可能已經有還沒發版的改動。對照 git 歷史,本報告引用的 signed_token.py、platform.py 跟 main checkout 內容一致;jedi_common/db.py 有差異,差的是一行已知的無用設定(總表第 20 項),不影響本報告結論。
    • U1-2 沒有實際停掉排程來驗證,是讀程式推論的。
    • 軸③(project.py/ssp.py/workflow.py)不在範圍內,本棒沒有逐行核對,只順帶看了 workflow.py 的「管理員或沒有身分直接放行」段落。
§8

執行概況

項目 值
掃描目標 BE repo compliance-manager-be,branch feature/review
版本 3f6290400(工作區有平行線未 commit 的改動,範圍 15 支檔案本身沒有改動)
工具 Claude Code 官方 claude-security plugin 0.11.0
參數 mode scan/effort low/focus attack-surface/scope 15 檔
run ID wf_444cc623-9d2
報告目錄 CLAUDE-SECURITY-20260925-032449/(不入版控)
耗時 約 17.5 分鐘(1,054 秒)
研究員 2 派出/2 交回(全範圍 1+找寫死密碼專掃 1),failed 0
候選/面板票 1/3(3:0)
驗證章 verified
工具發現 1 條中等(=總表第 19 項,不另計)
runner 自行發現 3 條(低 1、資訊 2),都沒有經過面板
淨新增 0 條中等以上;低/資訊 3 條,建議不開修正卡,只登記