---
title: J1 檢查結果：意見回饋全鏈＋對外 route 與守門殼（jedi-issue）
---

# J1 檢查結果：意見回饋全鏈＋對外 route 與守門殼（jedi-issue）

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

## 🔴 一句話結論

**找到 4 條發現、0 條高風險，全部中等。** 其中 2 條（誰都能改刪別人的回饋、誰都讀得到全部回饋）是 **CM-1630 搬家後的新落點，位置更新、不另計**；1 條（GitHub 連線關掉憑證驗證）是 **CM-1632 的重複，位置更新、不另計**；**淨新增只有 1 條**——匯出檔的 Excel 公式注入。另有 **1 條人工查出、工具沒報的新發現**：任何登入者都拿得到全站使用者名冊（成員清單端點）。

最該優先處理的是 **CM-1630**：使用者的意見回饋，任何一個登入的人都能改掉或刪掉，刪除還會連附件一起實際刪除。

**現況（以 `docs/security-report/M21-issue-rescan.md`／`M23-issue.md` 為準）**：F1／F2 → ✅ 已修（FR-114.1-7／CM-2030，1.21.0 出貨；原卡 CM-1630 作廢）；GitHub 憑證驗證 → ✅ 已修（CM-2066）；匯出公式注入 → ✅ 已修（CM-2055）；成員名單端點 → 🗑️ 死程式已刪除（FR-114.3-4）。

## 這一棒在檢查什麼

使用者在產品裡送意見回饋、改回饋、刪回饋、刪附件、匯出、看標籤選單、查成員名單——這七個對外功能，FR-099 之後全部搬進了 jedi-issue 套件。這一棒看的是：誰能做哪件事、能不能動別人的、內容流到 GitLab／GitHub 時帶了什麼、守門是怎麼從主專案注入進來的。

掃描目標 `/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-issue`，revision `4c208d401c01`（branch `feature/review`），mode `scan`，scope 31 個檔案（`api/` 整層 10 支、`app/feedback/` 6 支、`domain/feedback/` 8 支、`infra/feedback/` 7 支），effort `low`。檔數與卡片列的 31 條路徑逐檔比對完全一致。

## Coverage

這是一次**限定範圍**的掃描。套件內其他區塊——`plugin/`（屬 J2）、`common/`、標籤三層（屬 J2）、`infra/issue/`、`infra/member/`、`migrations/`（屬 J2 或 J3）——**不在這次範圍內，未被當作稽核目標**。

`low` 強度：一位研究員讀完 31 個檔案就提報候選，未做元件盤點、未做威脅建模、未跑額外密鑰專項掃描，`completenessCheckOutcome` 為 `not-applicable`（低強度本就不跑盤點）。驗證跑了 1 輪，4 個候選去重後仍是 4 個，沒有候選遺失、沒有候選被駁回。F1 的嚴重度被面板從 HIGH 一致下修為 MEDIUM。

有一條發現（F3）的位置落在**範圍外的檔案** `jedi_issue/infra/issue/adapter/github/github_issue_adapter.py`（屬 J3 範圍）。那是研究員從範圍內的資料流（送出回饋 → 建 GitHub issue）追出去讀到的，不是把範圍擴大去稽核；**標「⚠️ 越界（屬 J3）」**，留下來是因為範圍內的功能會實際走到那行。

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

**工具碰到卡片列的七個重點裡的三個（①③的一部分、④）。** ②⑤⑥⑦ 與 ①③ 的其餘部分，由首腦回頭開檔並連 DEV DB 唯讀查證補上，詳見下方「卡片重點逐項人工查證」段。

## Findings

### F1 — 任何登入者都能改掉／刪掉別人的意見回饋（MEDIUM，confidence high）

**這是什麼問題。** 使用者送出的意見回饋，後端在「要改」「要刪」的時候完全沒檢查「這筆是不是你建的」。前端雖然有判斷（不是你建的就不顯示按鈕），但那只是把按鈕藏起來，直接呼叫 API 就繞過去了。

**出事會怎樣。** 同一家客戶裡任何一個最低權限的登入使用者，可以竄改或刪除**全部**意見回饋紀錄——包含別人回報的問題內容、標籤與附件。刪除不只是隱藏：會連帶把本地 issue 關掉、把附件檔實際刪掉，畫面上救不回來。

**要先有什麼才打得到。**
- 攻擊者持有任一有效登入 token（不需要任何權限點）
- 目標回饋的識別碼——直接打清單端點就拿得到（該端點同樣只驗登入，見 F2）

**在哪裡。**
- `jedi_issue/api/routes/feedback_route.py:92-95`（`FeedbackRoute.delete`）、`:75-87`（`FeedbackRoute.put`）、`:103-108`（`FeedbackFileRoute.delete`）——三支都只掛 `@auth_required`（只驗有沒有登入）
- `jedi_issue/app/feedback/service/feedback_service.py:241-243`（`update_feedback`）、`:278-280`（`delete_feedback`）、`:304-306`（`delete_feedback_file`）——三支都是 `get_feedback_issue_by_uid(uid)` 撈出來就直接動，`user_login_name` 只被拿去寫 `updated_user` 欄位與關單紀錄，沒有拿去做任何判斷
- 對照：前端 `compliance-manager-fe/src/views/feedback/system/SystemFeedbackManage.vue:165-175` 的 `canEdit()` 明確比對 `item.created_user === userInfo.value.login_name`——**擁有者概念只活在前端**

**怎麼修。** 在 `FeedbackService.update_feedback()` / `delete_feedback()` / `delete_feedback_file()` 內，取到 `feedback_issue` 之後比對 `created_user` 與當前登入帳號，不符且非管理員時丟 `ForbiddenError`。管理員旁路走宿主注入的權限點判定（`feedback.update` / `feedback.delete`，DEV 資料庫已有這兩顆），不要只靠前端。

**驗證。** 3/3 三位檢查員（可達性／影響／既有防禦）一致確認成立。研究員原評 HIGH，三位一致評 MEDIUM，嚴重度已依面板結果下修——理由是要先有這家客戶的帳號、且跨客戶被資料庫隔離擋住。

**首腦核對註記與判定。** 屬實，逐檔開過，前端那側也開檔核對過。**這是 CM-1630 搬家後的新落點，標「重複 CM-1630，位置更新」，不另計。** 比原卡多出來的細節：`delete_feedback_file` 這支當年 CM-1633 另外開過卡，現在確認它與改／刪走同一個缺口（都沒驗擁有者），修法應一次三支一起。

### F2 — 回饋清單／明細端點沒有權限守門，任何登入者讀得到全租戶的回饋內容（MEDIUM，confidence high）

**這是什麼問題。** 「查看意見回饋」這件事，資料庫裡明明有權限點（`feedback.read`、`feedback-view.read`），但後端的清單與明細端點根本不檢查它，只看有沒有登入。

**出事會怎樣。** 沒被授予任何回饋權限的一般使用者（甚至只該看自己那筆的人），可以讀到全租戶所有人回報的問題內容。回饋內文常帶操作截圖說明、系統錯誤訊息、內部流程細節，屬資訊外洩。跨客戶仍被資料庫隔離擋住，所以是**同一家客戶內部**的外洩。

**要先有什麼才打得到。**
- 攻擊者持有任一有效登入 token
- 宿主已掛載 jedi-issue 的 route（`mount_api=True`，Guidant AI 預設如此，見 `core/plugins/issue.py:155`）

**在哪裡。** `jedi_issue/api/routes/feedback_route.py:35-46`（`FeedbacksRoute.post`，清單）與 `:51-56`（`FeedbackRoute.get`，明細）只掛 `@auth_required`。對照同檔 `:113-118` 的匯出端點多掛了 `@capability_required(lambda rt: rt.config.feedback_export_capability)`——**形狀對照明顯**。回出去的欄位見 `jedi_issue/api/serializers/feedback.py:23-38`：標題、描述、附件清單、建立者帳號與暱稱全在內。

**怎麼修。** 比照匯出那條，在清單與明細端點補上 `@capability_required(...)`（讀取用 `feedback.read` 或 `feedback-view.read`），並讓「只能看自己那筆」的使用者走一條以 `created_user` 過濾的路徑。權限點名沿用 `jedi_issue/plugin/contract.py` 的 `CAPABILITIES` 清單內既有字串，不要另創。

**驗證。** 3/3 一致確認成立，嚴重度 MEDIUM。

**首腦核對註記與判定。** 屬實。**同屬 CM-1630 的範圍（該卡原文是「意見回饋六操作只有匯出守權限」），標「重複 CM-1630，位置更新」，不另計。** 另查 DEV 資料庫確認：`feedback.read`、`feedback-view.read`、`feedback.create`、`feedback.update`、`feedback.delete`、`feedback.export` 六顆權限點都已建立，但只有 `feedback.export` 在後端被實際檢查——**其餘五顆目前只有前端在用來藏選單**。這與 FR-098 第 80 項（設備／資訊系統讀取端點不驗權限）是同一種產品決策題，套件 `feedback_route.py:3-4` 檔頭也自陳「要加是產品決策」。**要不要守，建議與第 80 項一次裁。**

### F3 — GitHub 整合關閉 TLS 憑證驗證，存取權杖可被中間人竊取（MEDIUM，confidence high）⚠️ 越界（屬 J3）

**這是什麼問題。** 後端連到 github.com 時，程式碼寫死「不要驗對方的憑證」。等於打電話報密碼前不確認接電話的是不是本人。

**出事會怎樣。** 能插進後端與 github.com 之間網路路徑的攻擊者，可以用假憑證攔截連線，取得 GitHub 存取權杖（該權杖對設定的專案有問題單讀寫權），並竄改往返的內容。**憑證外洩的影響跨出本系統邊界**——拿到權杖就能直接操作那個 GitHub 專案。

**要先有什麼才打得到。**
- 管理員在系統設定啟用 GitHub 整合（README 記載 DEV 目前為關閉）
- 攻擊者位於後端對外連線的網路路徑上（同網段 ARP 欺騙、惡意 proxy、被攻陷的出口設備）

**在哪裡。** `jedi_issue/infra/issue/adapter/github/github_issue_adapter.py:40`（`GitHubIssueAdapter.init_config`）的 `Github(auth=auth, ..., verify=False)`；`jedi_issue/infra/github.py:22` 的 fallback 版本同病。回饋建立／更新流程（`FeedbackService.add_feedback` → `FeedbackIssueService.create_github_issue`）會實際走到。

**怎麼修。** 移除 `verify=False`（PyGithub 預設即驗證）。若目標是內網自架 GitHub Enterprise 需要私有憑證機構，改成把憑證路徑傳給 `verify=`，不要整個關掉；兩處一起修。

**驗證。** 3/3 一致確認成立。

**首腦核對註記與判定。** 屬實。**這是 CM-1632 的重複（原卡即記「`verify=False` 兩處」），標「重複 CM-1632，位置更新」，不另計。** 兩處位置與 FR-101 README 開卡時記的完全一致，確認搬家沒修。**這兩支檔屬 J3 範圍**，研究員為追資料流讀到，本報告不把它們計入 J1 的覆蓋率。

### F4 — 回饋匯出未中和公式字元，使用者提交的標題會變成 Excel 真公式（MEDIUM，confidence medium）

**這是什麼問題。** 使用者在回饋標題裡填的字，匯出成 Excel 檔時直接照抄進儲存格。如果那串字是以等號開頭，Excel 會把它當成**公式**執行，而不是當成文字顯示。

**出事會怎樣。** 管理員下載匯出檔用 Excel 或 LibreOffice 打開時，攻擊者植入的公式會被當成公式處理；配合特定手法可以把同一份檔案裡其他儲存格的內容偷偷傳到外部網址，或在管理員按下安全性提示的「啟用」後，於該台工作站執行外部程式。

**要先有什麼才打得到。**
- 攻擊者是能提交回饋的一般登入使用者（建立端點只驗登入）
- 受害者是持有 `feedback.export` 權限的管理員，且把匯出檔用試算表軟體打開
- 受害者需點過試算表軟體的公式／外部連結安全性提示

**在哪裡。** `jedi_issue/app/feedback/service/feedback_service.py:389`（`FeedbackService.export_feedbacks` 的 Excel 分支 `ws.append(...)`），CSV 分支在同函式 `:378`（`writer.writerow(row)`）。組進去的欄位有標題、問題描述、標籤名稱、建立者暱稱，全部來自使用者填的內容。openpyxl 對開頭是 `=` 的字串會把儲存格型別設成公式。

**怎麼修。** 匯出前對每個字串欄位做中和：開頭為 `=`、`+`、`-`、`@`、Tab、換行時前置單引號（或明確把儲存格型別鎖成字串）。中和函式放在 `export_feedbacks()` 組資料的地方，Excel 與 CSV 兩種格式共用一份。

**驗證。** 3/3 一致確認成立，嚴重度 MEDIUM，confidence medium。confidence 不是 high 的原因：利用鏈需要受害者主動用試算表軟體開檔並點過安全性提示，這一步無法從程式碼確認一定會發生。

**首腦核對註記與判定。** **屬實，本棒唯一淨新增。** 開檔核對：`export_rows` 六個欄位裡，`功能`（標籤名）、`標題`、`建立使用者`（暱稱）、`問題描述` 四個都來自使用者可控內容；`問題描述` 那欄雖然過了 `html_to_text()`，但那只是把 HTML 標籤拔掉、不會處理開頭的等號。**與 FR-097 第 78 項（另一處匯出的公式注入）是同一種病、不同位置**，建議兩處一起修，或抽一支共用的中和函式。

## What was verified

低強度單研究員掃描提報 4 個候選，去重後仍 4 個；三位檢查員各自獨立對四個候選投票，共 12 票全投出、沒有漏投。四條全部 3:0 通過，沒有候選被駁回。F1 的嚴重度被面板一致從 HIGH 降為 MEDIUM。票數由工作流程的程式碼統計，不是人工填寫。驗證章狀態 `verified`，無拒收理由。

---

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

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

### ① CM-1630 搬家後的現況（**工具已報＝F1＋F2，但漏了跨租戶那半**）

- 七條 route 的守門現況逐條核對：`/issue/get_members`（只登入）、`/feedbacks` 清單（只登入）、`/feedback` 建立（只登入）、`/feedback/<uid>` 讀／改／刪（只登入）、`/feedback/file/<uid>/<file_uid>` 刪附件（只登入）、`/labels/menu/<scope>`（只登入）、`/feedback/export/<type>`（**唯一有權限點**）。與卡片描述完全一致。
- **「有沒有任何一層比對 `created_user`」→ 沒有。** app 層三支方法（`update_feedback` / `delete_feedback` / `delete_feedback_file`）全部只用 uid 定位，`user_login_name` 只寫審計欄位。domain 與 infra 層更不可能有（它們拿不到「誰在操作」）。
- **「任何登入者是否看得到全租戶所有人的回饋」→ 是。** `get_feedbacks_and_pager`（`:151`）不帶任何身分條件，送空 filters 就回本租戶整表（分頁內）。
- 🔴 **卡片問的「跨租戶 join」那半——工具完全沒碰，人工查證結果是：風險成立但比卡片假設的更廣。** DEV 唯讀實查（2026-09-16）：`feedback_issues` 開了隔離（4 條規則、不強制），但 `_attach_issues`（`:131`）撈 issue 內容走的是 `feedback_issue_service.get_issues()` → `IssueProviderCode.LOCAL` → `issues` 表，而**該表隔離是關的（`relrowsecurity=f`、0 條規則）**。同理 `labels`、三張 mapping 表也全是關的。

  **但實際影響被一件事限縮**：`_attach_issues` 是「先撈本租戶的 `feedback_issues`，再用它的 `issue_uid` 去比對」，所以清單頁不會憑空多出別租戶的回饋。真正的缺口是**方向相反的**——`issues` 表零隔離代表任何走到那張表的路徑都沒有租戶邊界，而 `feedback_issues` 的隔離等於**只鎖了門、牆是空的**。這與跨 arc 總表 §3.1 第 7 項（六張表零隔離）是同一根因，**歸第 7 項，不另計**。

### ② 守門殼與宿主注入的接縫（**工具未報，人工查證，結論：設計正確，非漏洞**）

- `api/guards.py:28-64` 三支：`auth_required` 與 `capability_required` 都是把判斷委派給宿主注入的實作（lazy，在 request 期才解），`current_user_login_name` 缺了回 None。
- 🔴 **卡片問「`current_user_login_name` 為 None 時 `add_feedback` 的 `created_user` 是空，之後誰都算不上主人」——查證結果：這個假設的前提在 Guidant AI 不成立。** `plugin/assembly.py:88-105` 的 `_assert_api_wiring` 在掛 route 前檢查 `auth_required`／`capability_required` 兩項，缺了直接 `RuntimeError` 拒絕掛載；而 `current_user_login_name` **不在必檢清單內**——這是真的。但主專案 `core/plugins/_host.py:208-210` 的 `host_defaults()` 三樣都給齊了，`core/plugins/issue.py:155` 用 `**host_defaults()` 全收。**所以在本產品內它永遠有值。** 風險是「別的宿主接錯」的假設情境，不是現況缺口；且就算真的是 None，`created_user` 空只會讓那筆回饋沒有主人，不會讓別人變成主人——在 F1 已經沒人驗擁有者的現況下，這件事沒有額外影響。**不成立，不計。**
- **`auth_required` 給的是 `jwt_required()` decorator 本身（不是 lambda）——查證結果：兩種寫法在此處等價。** 套件的 `auth_required` 每次呼叫都是 `guard(fn)(*args, **kwargs)`，`jwt_required()` 回的那顆 decorator 是無狀態的、可重複套用，每個 request 重新包一次也不會殘留狀態。`_host.py:197` 的註解自己也標明了這個差異。**不成立，不計。**
- **`_guard` 有沒有漏 method——逐條核對 `routing.py:24-29` 六個 resource 對應的所有 HTTP 方法，七條端點全部都有掛 `@auth_required`，沒有裸奔的。**

### ③ 輸入到第三方的流向（**工具只報了 GitHub 的 TLS（F3），其餘人工查證**）

- **`request.files['json']` 不存在時炸什麼（`feedback_route.py:62`／`:77`）**：Flask 的 `request.files` 是 MultiDict，`['json']` 找不到會丟 `BadRequestKeyError`，那是 `HTTPException` 的子類、映到 **HTTP 400**，不是 500。`jedi_common` 的錯誤處理器對 HTTPException 保留原生狀態碼。**行為可接受，不是缺口**（送壞的請求拿到 400 是對的），但錯誤訊息對前端不友善，屬體驗問題不屬資安。
- **描述是 HTML，存進 `issues` 與送到 GitHub 時有沒有清**：**沒有清。** `add_feedback` 收到的 `content` 原樣往下傳，`html_to_text()` 只在匯出時用（`feedback_service.py:364`）。**但這不構成本系統的漏洞**：前端顯示回饋內容時如何渲染，決定了會不會變成 XSS——查過 `SystemFeedbackManage.vue` 沒有 `v-html`。**送到 GitHub 那側是外部系統的渲染政策**，GitHub 自己會處理 Markdown 的安全性。**不計，但若前端未來改用 `v-html` 顯示，這條會立刻變成儲存型 XSS，值得記一筆。**
- **labels 清單是 uid 還是名字、能不能塞不存在的**：本地走 uid（`create_issue` 直接把 `labels` 往下傳），GitLab／GitHub 走名字（`feedback_issue_service.py:169` 的 `self._label_names(labels)` 轉一次）。塞不存在的 uid 會在 mapping 寫入時失敗或被忽略，**不會造成越權**。不計。
- **檔名／大小限制在哪一層**：大小由 Flask 的 `MAX_CONTENT_LENGTH` 擋（`core/app_factory.py:93`，主專案設定），屬宿主層、本套件不管。檔名處理在 `infra/issue_upload_files/`（屬 J3 範圍），**本棒未查，留給 J3**。

### ④ 匯出（**工具已報＝F4**）

- 公式注入屬實，見 F4。
- **`export_type` 有沒有白名單——有。** `feedback_service.py:370/385` 是 `if export_type == 'csv'` / `elif export_type == 'excel'` / `else: raise BadRequestError`，只有兩個值會過。URL 上塞任何其他字串都回 400。**不成立，不計。**
- 補一個工具沒報的小問題：`export_rows[0].keys()`（`:373`／`:387`）在**一筆回饋都沒有時會 IndexError→500**。屬穩定性不屬資安，順手記一筆。

### ⑤ 成員名單端點（**工具未報，人工查證，🔴 新發現**）

- `api/routes/issue_member_route.py:11-15` 只有 `@auth_required`，直接回 `member_service.get_members()` 的結果、不包回應信封（檔頭註明那是既有對外契約）。
- 回的是 `members` 表（`app/member/service/member_service.py:13-21` → domain → `infra/member/`），DEV 唯讀實查：**該表隔離是關的（`relrowsecurity=f`、0 條規則）**，目前 0 筆。
- 回的欄位（`MemberDTO`）：`uid`、`project`、`username`、`email`、`role`、`type`——**含電子郵件**。
- **結論：任何登入者可拿到整張成員表，且該表沒有客戶隔離。** 這正是套件自己在 `plugin/assembly.py:92-94` 寫的那句「跳過等於七條端點裸奔（成員清單回全站使用者名冊）」所描述的狀態——只是它防的是「宿主沒注入守門」，而**注入了守門的現況也只擋到「有沒有登入」而已**。
- **判定：新發現，但根因歸總表 §3.1 第 7 項（六張表零隔離）**，端點缺權限點守門歸 **CM-1630 的同一條產品決策**。**不另開新項，但建議 CM-1630 的描述補上成員端點**——原卡只講六個回饋操作，沒提成員名單。DEV 該表 0 筆所以現況無實害，但那是資料未落地、不是防護存在。

### ⑥ 暱稱搜尋轉譯與哨兵值（**工具未報，人工查證，結論：論證成立**）

- 🔴 **卡片問「哨兵字串的『永遠不成立』論證在 `ILIKE` 下是否真的成立」——查證結果：成立。** 哨兵值是 `"__jedi_issue_no_display_name_match__" + "_" * 28`（`feedback_service.py:47`）。共用底層 `base_repository_impl.py:135-138` 的 `__patternize()` 規則是：**字串已含 `%` 或 `_` 就尊重原值、不再包 `%...%`**。哨兵值含 `_`，所以直接當 pattern 用。在 `ILIKE` 裡 `_` 是「任一單一字元」的萬用字元，所以這個 pattern 要求比對對象**長度恰好等於哨兵長度、且前段字元逐一吻合**——`created_user` 是登入帳號，不可能長成那樣。**論證成立，不是缺口。** 而且設計動機是對的：不放哨兵會讓條件從 SQL 消失，搜一個不存在的暱稱會從「查無」變成「回全部」。
- 🔴 **卡片問「`created_user` filter 合併時，能不能用 filter 直接指定別人帳號撈他的回饋」——查證結果：能，但這不是新缺口。** `FeedbackQueryRequest`（`serializers/feedback.py:13`）開放 `created_user` 當查詢條件，而底層 `_merge_string_filter` 把它與暱稱搜出的帳號併成 OR 群組（`base_repository_impl.py:235` 的 `or_(*string_conds)`）。所以送 `{"created_user": "someone"}` 就能撈出那個人的全部回饋。**但在 F2（誰都看得到全部）的現況下，這只是把「全部」過濾成「某人的」，沒有拿到本來拿不到的資料。** 等 F2 修好、加上權限點與「只看自己」的過濾之後，**這個 filter 必須一併處理**，否則會變成繞過那層過濾的後門。**不另計，但列為 F2 修正時的必查項。**
- 補一個工具沒報的：`or_` 群組語意讓「標題含 X」與「建立者含 X」變成 OR，所以同時送標題與建立者兩個條件時，結果會比使用者預期的寬。屬功能語意不屬資安。

### ⑦ 序列化回傳欄位（**工具未報，人工查證，結論：有夾帶但影響有限**）

- `api/serializers/feedback.py:23-38` 的 `FeedbackResponse` 確實回了 `gitlab_issue_uid` 與 `github_issue_uid`——**卡片的假設屬實**。
- **但這兩個識別碼拿不去打第三方**：GitLab／GitHub 的 API 需要存取權杖才能用，光有問題單編號打不進去；而且那兩個專案是廠商自己的內部專案，編號本身不是機密。**內部資料庫的數字 id 沒有回出去**（serializer 只回 `uid`）。
- **判定：不成立，不計。** 但若未來 GitLab／GitHub 專案改為公開可讀，這兩個編號會變成「能對照到內部回饋」的資訊，值得在那個時間點回頭看。

---

## FR-101 README 開卡時已核出的四件，工具有沒有重報

| 開卡時已核出 | 本棒工具 | 處置 |
|---|---|---|
| CM-1630 搬家沒修（七條 route 只有匯出守權限、改刪不驗本人） | **重報了**（F1＋F2） | 標「重複 CM-1630，位置更新」，不另計；建議描述補上成員端點（現況：✅ 已修 FR-114.1-7／CM-2030；成員端點已刪 FR-114.3-4） |
| CM-1632 `verify=False` 兩處仍在 | **重報了**（F3） | 標「重複 CM-1632，位置更新」，不另計；⚠️ 越界（屬 J3）（現況：✅ 已修 CM-2066） |
| 六張舊表零隔離（門上了鎖、牆是空的） | 未報 | ①⑤人工查證確認，歸總表第 7 項 |
| 守門殼只驗兩項必填、10 顆權限點 6 顆自陳不守 | 未報 | ②人工查證，設計正確非漏洞；權限點那半歸 CM-1630 |

**工具重報了前兩件，都已按規則標重複，沒有膨脹計數。**

---

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

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

12 票全投出、沒有漏投，四條全部 3:0。首腦逐條開檔核對（route 的守門、app 層三支方法的實作、序列化欄位、匯出的資料組裝、前端的 `canEdit`）全部對得上，F1 還額外從前端那側取得反證（前端有擁有者判斷、後端沒有）。

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

最低強度快篩、一位研究員、不做盤點不做威脅建模。卡片七個重點工具只碰到三個，②⑤⑥⑦ 全部由首腦回頭開檔並連 DEV 資料庫唯讀查證補上——其中 ⑤（成員名單端點回全站名冊）是**工具完全沒碰、人工查出來的新發現**。與 FR-097 L1／L2、FR-098 A1／A2 撞到的教訓完全一致：**低強度掃描會咬住最顯眼的攻擊模式，然後停在那裡。**

另外，本套件的註解密度極高且處處自我辯護（每個可疑設計都附一段說明為什麼安全）。實際核對下來，**那些辯護多數是對的**（哨兵值論證、守門殼的吵鬧拒絕、`_attach_issues` 的批次撈法），但也正因如此，工具很容易被說服而停止追問——②⑥兩項的結論雖然與註解一致，是首腦獨立驗過才寫下的，不是採信註解。

---

## 執行概況

| 項目 | 數字 |
|---|---|
| 檢查範圍 | 31 個檔案（逐檔比對與卡片 scope 完全一致） |
| 檢查強度 | 最低（`low`），未設定 focus |
| 候選問題 → 去除重複 | 4 → 4 |
| 投票數 | 12（4 條候選 × 3 位檢查員），全數投出 |
| F1／F2／F3／F4 投票結果 | 全部 3:0 |
| 被駁回候選 | 0 |
| 沒投到票的／投票中斷的 | 0 / 0 |
| 被降低嚴重度的 | 1（F1 由 HIGH 降 MEDIUM） |
| 研究員派出／回收 | 1 / 1 |
| 驗證章狀態 | `verified`（無拒收理由） |
| 掃描耗時 | 3177 秒（約 53 分） |
| 掃描當下的程式碼版本 | commit `4c208d401c01`，branch `feature/review` |
| workflow run | `wf_c2e430d6-115` |
| 工具產物位置 | `jedi-issue/CLAUDE-SECURITY-20260916-064039/`（該目錄有自己的 `.gitignore`，不入版控） |
| 首腦人工補查項目 | 卡片七個重點逐項查證，②⑥⑦ 判定不成立、⑤ 為新發現、①③ 補足工具未碰的部分 |
| 對總表的影響 | **淨新增 1 條中（F4 匯出公式注入）**；F1／F2 併 CM-1630、F3 併 CM-1632、⑤與跨租戶那半歸第 7 項 |
