檢查日期 2026-09-16|對應卡片 CM-1827|檢查範圍 37 個檔案
工具在我指定的 37 個檔裡一條問題都沒報——它報的 4 條全部跑到範圍外的檔案,而且是 J1 上一棒早就報過、決策者也驗收過的同樣 4 條(連檔名行號都一樣),零淨新增。真正的產出來自我自己回頭開檔與查資料庫:查出 1 條工具沒報的新問題——沒有被分配部門的使用者,送出意見回饋會失敗,而且畫面上看不出原因。另外釐清一件過去被列為隱憂、這次確認不成立的事:標籤表沒有客戶隔離是正確的,因為它是全站共用的選項字典,不是客戶資料。
現況:N1(無部門使用者送不出回饋)→ ✅ 已修(FR-114.1-9);F1/F2 → ✅ 已修(FR-114.1-7/CM-2030);F3 匯出公式 → ✅ 已修(CM-2055);F4 GitHub 憑證 → ✅ 已修(CM-2066);labels 第 7 項描述修正與成員名冊 → 見 M21(成員名冊已刪,FR-114.3-4)。
意見回饋這個功能,在 FR-099 之後整組搬進了 jedi-issue 這個套件。搬完之後,套件要「接」回主系統才能運作——這一棒看的就是接線那一層,不看功能本身(功能屬上一棒 J1)。具體四件事:
掃描目標 /Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-issue,版本 589d0346163e(branch feature/review),範圍 37 個檔案,強度 low(最低)。
low 強度:一位研究員把範圍讀完後提報候選,再交給三位獨立檢查員各投一票。這個強度不做元件盤點、不做威脅建模、不跑額外的密鑰專項掃描,所以完整性檢查那欄是「不適用」。投票跑了 1 輪,4 個候選去重後仍是 4 個,沒有候選遺失、沒有被降低嚴重度、沒有被駁回。
第一位研究員跑了約 75 分鐘後被系統判定卡住、砍掉重派,第二位接手後完成;檢查員投的是第二位的結果。整棒耗時約 2 小時 28 分。
🔴 工具嚴重偏離指定範圍。 它從插件接線一路追進 api/、app/feedback/、infra/issue/,甚至讀了主專案的 core/plugins/——4 條發現全部落在那些檔案,我指定的 37 個檔一條都沒產出。那些檔案分屬 J1(已掃完驗收)與 J3(未派),所以這 4 條在本棒全部是重複、不另計。
因此卡片的六個重點,工具一項都沒碰到,六項全部由我自己回頭開檔、連 DEV 資料庫唯讀查證補上,詳見下方「卡片重點逐項人工查證」。
工具沒有執行任何程式:沒跑測試、沒發請求、沒示範攻擊,所有結論都是讀原始碼推出來的。
四條的完整白話說明已在 J1 報告 寫過且經決策者驗收,這裡只列對照,不重複展開。
| 編號 | 一句話 | 在哪裡 | 判定 |
|---|---|---|---|
| F1 | 任何登入者都能刪掉別人的意見回饋 | jedi_issue/api/routes/feedback_route.py:94 |
與 J1 的 F1 完全同一條(重複 CM-1630,位置未變),不另計 |
| F2 | 任何登入者都能改掉別人的意見回饋內容與附件 | jedi_issue/api/routes/feedback_route.py:79 |
與 J1 的 F2 完全同一條(重複 CM-1630),不另計 |
| F3 | 匯出檔沒有中和公式字元,使用者填的標題會變成 Excel 真公式 | jedi_issue/app/feedback/service/feedback_service.py:389 |
與 J1 的 F4 完全同一條(已登記為總表第 82 項),不另計 |
| F4 | 連 GitHub 時關掉了憑證檢查,存取金鑰可能被攔截 | jedi_issue/infra/issue/adapter/github/github_issue_adapter.py:40 |
與 J1 的 F3 完全同一條(重複 CM-1632),不另計 |
「憑證檢查」是指:連線到對方網站時,確認對方真的是它宣稱的那個網站。關掉之後,任何人只要能插在中間(例如控制了網路設備或 DNS),就能假扮成 GitHub 把金鑰收走。
四條投票都是 3:0 全票通過,驗證章 verified。這代表「這四條確實存在」可信;但不代表「只有這四條」——見下方可信度段。
(工具未報,人工查證)結論:套件的自我說明屬實,沒有說謊,但「宣告 10 顆、實際只擋 5 顆」這件事成立。
套件在 jedi_issue/plugin/contract.py:36-47 宣告了 10 個權限點,分兩組:
第一組 issue-integrate-config.* 四顆(建立/讀取/修改/刪除議題整合設定)——套件自陳「本套件的 route 不守這四項,但它們確實被執法,由宿主的系統設定端點分流守」。我開檔核對主系統:屬實。 core/plugins/system_core.py:69 確實有這一行對照:
"ISSUE_INTEGRATE_CONFIG": "issue-integrate-config",
而 assert_config_capability()(同檔 :93)會依這張表算出該守哪顆權限點,再交給全專案統一的 viewer_has_capability 判斷,不通過就丟 403。這四顆是真的在擋人的,不是裝飾。 另外這張表有個保護設計值得記:不在表上的設定群組會 fallback 回誰都沒有的 system_config.*(同檔 :86),也就是「漏填就變成誰都不能動」而不是「漏填就放行」——出錯時預設擋下來,方向正確。
第二組 feedback.* 五顆+feedback-view.read 一顆——套件自陳「BE 只守 feedback.export 一項,其餘五項是前端選單在認」。屬實,且主系統 core/plugins/issue.py:24-28 檔頭也寫了同一件事,原話「要補是產品決策(補了會擋掉現在能用的人)」。
判定:這與 FR-098 第 80 項(設備/資訊系統讀取端點不驗權限)是同一種產品決策題,也與 J1 的 F1/F2 同源。 不另計新項,但建議三處一次裁:要不要讓後端真的檢查這些權限點。
(工具未報,人工查證)結論:這是正面案例,設計正確。 三個子問題分別回答:
(a)缺必要零件 → 當場拒絕啟動,正確。 jedi_issue/plugin/assembly.py:87-105 的 _assert_api_wiring() 檢查 auth_required(檢查有沒有登入)與 capability_required(檢查有沒有權限)兩件是否都有給,缺任一件就丟 RuntimeError 拒絕掛載。註解寫得很清楚為什麼不能改成「有就用、沒有就跳過」:跳過等於七條 API 裸奔,而服務照樣起得來、健康檢查照樣綠燈——看不見的失效。這是好設計。
(b)同一個 app 掛兩次 → 後掛的會蓋掉先掛的,但實際打不到。 jedi_issue/plugin/__init__.py:120-127 在 mount_api=False(當程式庫用、不掛 API)那條路無條件覆寫 app.extensions,註解自陳「最後一次 register 贏」。理論上若先掛一次強守門、再掛一次弱守門,後者會蓋掉前者。但我核對主系統:core/plugins/issue.py:171 只呼叫一次 register(),且守門三件一律走全專案共用的 host_defaults(),沒有「弱 adapters」這種東西存在。不構成本系統的漏洞,但如果未來有別的宿主要掛兩次,這個覆寫語意值得記一筆。
(c)register() 不帶零件、又是 mount_api=False → 會安靜通過,但這是刻意且安全的。 這條路不掛任何 API,所以沒有端點可裸奔;拒絕它反而會讓「不掛路由」與「拿得到執行期物件」變成綁死的二選一。判定:不是缺口。
(d)三個選填零件缺了各退化成什麼(contract.py:114-126):
identity 缺 → 審計欄位查不到暱稱,畫面顯示帳號而非姓名(不報錯)user_name_searcher 缺 → 用暱稱搜尋回饋會搜不到,搜標題與帳號仍可用(安靜降級,ports.py 註解明載)integrate_config_provider 缺 → GitLab/GitHub 整合一律當成未啟用,回饋只建在本地三者都是安靜降級、不報錯。本產品三個都有給(core/plugins/issue.py:150-153),所以現況無影響。
(工具未報,人工查證)結論:🔴 先前列在總表第 7 項的「標籤表沒有客戶隔離」這條,經查判定為 false positive——標籤是全站共用的選項字典,回全表是正確行為。
判準是「這筆資料有沒有主人」。我查了 DEV 資料庫的實際結構:
public.labels 欄位:id, uid, name, color, description, scope, is_deleted
整張表沒有任何欄位標示「這筆屬於誰」——沒有客戶欄(tenant_id)、沒有部門欄、沒有建立者欄。內容是 FEEDBACK_TYPE(建議/錯誤/其他,3 筆)與 FUNCTION(功能分類,16 筆)這類下拉選單的固定選項。這種資料在資料庫設計上叫「碼表」,性質等同「縣市清單」——本來就該所有人都看到同一份,替它加客戶隔離反而會讓 A 客戶看不到選項。
判定:infra/label/repository/label_repository.py:28 的 get_labels 沒有客戶條件是正確的,不是缺口。建議把跨 arc 總表第 7 項的描述修正:該項原文說「六張表完全沒有客戶隔離」,其中 labels 這張應排除(其餘五張 issues/三張 mapping/members 是否為真,屬 J3 範圍,本棒不裁)。
另外兩個子問題:
(a)三支寫入方法(新增/修改/刪除標籤)誰在呼叫?——我 grep 過整個套件與主系統:正式程式零呼叫者,只有測試檔在用(tests/unittest/test_label_service.py、tests/integration/test_service_behaviour.py)。對外也沒有任何 route(api/routing.py 只掛了 LabelsMenuRoute 一條,是唯讀的)。判定:這三支是死碼,目前無法從外部觸發,不構成攻擊面。但它們沒有任何權限檢查,若未來有人替它們補上 route 卻忘了補守門,就會變成「任何登入者都能改掉全站共用的下拉選單」——建議在卡片備註記一筆。
(b)scope 參數從網址進來,有沒有白名單?——沒有。 api/routes/label_route.py:16 直接把網址上的 scope 傳進查詢。但這裡不構成漏洞:它經過 SQLAlchemy 的參數化查詢(Labels.scope == scope 等值比對),不會有 SQL 注入;沒白名單的後果只是「傳一個不存在的分類會拿到空清單」。不計。
(工具未報,人工查證)結論:交易邊界安全(原本擔心的情況不成立),但查出一條真的缺口,見下方「新發現」。
(a)002 開頭那句 SET LOCAL app.is_super_admin = 't' 會不會失效?——不會。 SET LOCAL(只在當前交易內生效的設定)的風險在於:如果套用工具不是把整支包在單一交易裡跑,這句會立刻失效,而後面的寫入照跑——症狀是「腳本看起來成功,但資料因為權限被擋而少寫了幾筆,且沒有錯誤訊息」。我追了正規套用路徑:scripts/init/migrate.sh:153 是
"${PSQL[@]}" --single-transaction -v ON_ERROR_STOP=1 -q -f "${SQL_DIR}/${rel}"
--single-transaction 明確把整支包在單一交易內,所以 SET LOCAL 涵蓋整支腳本。判定:安全。(套件自己的 plugin/migrations.py 只負責把 SQL 內容交出來、不執行,交易邊界完全由主系統決定——這個分工是對的。)
(b)四支重跑是否真的冪等(可以重複執行不出錯)?——是。 逐支核對:
001:用 WHERE NOT EXISTS 判斷 (分類, 名稱) 是否已存在。檔頭還註明了一個陷阱——labels 表沒有 (scope, name) 唯一約束、只有 uid 唯一,而 uid 每次都是新產生的隨機值,所以不能用 ON CONFLICT(永遠不會衝突,會變成每跑一次就多塞三筆)。這個判斷正確。002:WHERE NOT EXISTS + ON CONFLICT DO NOTHING,正確。003:CREATE TABLE IF NOT EXISTS、CREATE INDEX IF NOT EXISTS,唯一約束用 DO $$ ... EXCEPTION WHEN duplicate_object 包覆,正確。004:CREATE POLICY 沒有 IF NOT EXISTS 語法,改用 DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL 包覆達成冪等,正確。(c)004 的 RLS 規則與 003 的欄位可不可為空,兩者搭起來會怎樣?——這裡查出真缺口,見下一節。
(工具未報,人工查證)結論:乾淨。
common/enum/error_code.py 六個錯誤碼全部是固定中文短語(「Issue不存在」「Label不存在」這類),沒有任何一個把檔案路徑、SQL 語句或第三方系統的原始回應塞進訊息裡。common/utils/common_utils.py 只有一支產生隨機識別碼的函式,無風險。common/enum/issue_provider_code.py 是三個固定字串常數,無風險。
domain/ports.py:GitLab/GitHub 金鑰會不會外流(工具未報,人工查證)結論:套件內乾淨。
IIntegrateConfigProvider.get_integrate_config() 回的設定包含 GitLab/GitHub 的存取金鑰。我 grep 過整個套件,確認這份設定:
logger/logging/print 搭配 integrate_config/token,零命中)ports.py:150 的「adapter 註冊表是空的」,不帶設定值)feedback_issue_service.py:55 被讀進來判斷「整合有沒有啟用」,不進任何回應)注意:這只涵蓋套件內部。 主系統的 log 會不會印出這些值,屬於既有的待辦 [followup_be_logs_request_body_plaintext_credentials](決策者 2026-09-03 已裁「記錄不修」),不在本棒範圍。
這是什麼問題。 資料庫上有一條隔離規則要求「建立回饋時,這筆資料要屬於某個部門」。但系統允許使用者不隸屬任何部門,這種使用者送出回饋時,「部門」那一欄是空的,於是被規則擋下來。使用者只會看到送出失敗,不會知道原因是自己沒有部門。
出事會怎樣。 沒有被分配部門的一般使用者完全無法使用意見回饋功能。這不是資安外洩,是功能對特定使用者整組失效,而且症狀具誤導性——管理員查權限會發現權限都對,但就是送不出去。DEV 資料庫現在就有這樣的帳號(blspan,不是超級管理員、沒有部門)。這條在新客戶剛裝好、部門還沒建的階段特別容易踩到。
要先有什麼才打得到(或說:什麼情況下會發生)。
users.org_unit_id 為空)在哪裡(三個檔合起來才看得出來,這是它難被發現的原因)。
jedi_issue/migrations/004-feedback-issues-rls-grants.sql:56-61——新增資料的規則要求 app_org_allowed_for_session(org_unit_id) 為真(或持有「可讀所有部門」旁路)。注意這條規則的形狀與其他三條不同:查詢/修改/刪除三條都有超級管理員旁路,只有「新增」這條沒有,改成檢查部門。 檔頭第 16-18 行自陳「實況如此,不要順手統一」。jedi_issue/migrations/003-feedback-issues-table.sql:38-39——tenant_id 與 org_unit_id 兩欄都可以為空。jedi-common 的 jedi_common/session/database/db_mw.py:82-86——新增資料時自動把使用者的部門填進去;使用者沒有部門,這欄就填成空值。我實際在 DEV 資料庫驗過這個規則對空值的判定:
SELECT public.app_org_allowed_for_session(NULL); -- 回 f(false)
部門為空 → 規則判定 false → 新增被擋。 這不是推論,是實查結果。
怎麼修(三選一,屬產品決策)。
004 的新增規則改成允許部門為空,寫法是 (org_unit_id IS NULL OR app_org_allowed_for_session(org_unit_id) OR 可讀所有部門旁路)。理由:回饋是「誰都該能送」的功能,不該卡在部門上。注意這要另開一支新的 migration 改,不能改 004 原檔(已出貨的腳本不回頭改)。為什麼是 MEDIUM 不是 HIGH: 這不會外洩資料、不會被拿來攻擊,只是功能失效;且要踩到需要「沒有部門的非管理員帳號」這個特定條件。但它症狀完全靜默,這是它值得登記的原因。
這一條工具完全沒報——它連 003/004 兩支 SQL 都沒有交叉比對,更沒有連資料庫驗證判定函式對空值的行為。
分兩層講,這兩層的可信度差很多:
第一層:「報出來的這些,是真的嗎?」——可信度高。 工具報的 4 條都是三位檢查員 3:0 全票通過,我也逐條開檔核對過,位置與描述都對得上。我自己查出的 N1 更是實際連 DEV 資料庫驗過判定函式的回傳值,不是推論。這一層沒有問題。
第二層:「只有這些嗎?」——可信度低,這一棒對指定範圍的覆蓋不足。
理由有三個,都要講清楚:
coverage.research 是空的)。SET LOCAL、標籤碼表性質)都屬實,但也正因為讀起來很有說服力,工具很容易被說服而不去驗證。這次工具乾脆跑去掃別的檔,某種程度上也是這個現象的變體。結論:這 37 個檔不能算「掃過且乾淨」,只能算「卡片列的六個重點查過了,其中五個沒問題、一個查出新缺口」。 如果要對這一塊有更高的把握,需要重掃一次並限制工具不得越界。
| 項目 | 數字 |
|---|---|
| 檢查範圍 | 37 個檔案(插件骨架 5+標籤三層 13+domain/ports.py+common/ 6+4 支 SQL+各層 __init__ 8) |
| 檢查強度 | 最低(low),未設定 focus |
| 候選問題 → 去除重複 | 4 → 4 |
| 投票數 | 12(4 條候選 × 3 位檢查員),全數投出、沒有漏投 |
| F1/F2/F3/F4 投票結果 | 全部 3:0 |
| 被駁回候選 | 0 |
| 沒投到票的/投票中斷的/被降低嚴重度的 | 0/0/0 |
| 研究員派出/回收 | 2 派出(第一位 75 分鐘後被判定卡住砍掉重派)/1 回收 |
| 驗證章狀態 | verified |
| 掃描耗時 | 8893 秒(約 2 小時 28 分) |
| 掃描當下的程式碼版本 | commit 589d0346163e,branch feature/review |
| scan_id / workflow run | 673f88d0-9361-48bd-993a-2d8d8a5a86f2 / wf_414762e2-436 |
| 🔴 範圍內發現 | 0 條(4 條全部越界到 J1/J3 範圍) |
| 首腦人工補查項目 | 卡片六個重點逐項查證:①②④(a)(b)⑤⑥ 無新問題、③判定原總表第 7 項對 labels 表為誤判、④(c) 查出新缺口 N1 |
| 對總表的影響 | 淨新增 1 條中(N1,沒有部門的使用者送不出回饋);另建議修正第 7 項描述(labels 應排除) |
labels 表從「沒有客戶隔離」名單移除,因為它是碼表,回全表正確)。feedback.* 五顆權限點後端要不要真的守——這題與 FR-098 第 80 項、J1 的 F1/F2 同源,建議三處一次裁。