FR-085.C3 掃描報告:輸出面與打包(jedi-common)

  • 卡片CM-1649(母卡 CM-1646
  • 掃描範圍jedi-commoninterfaces/utils/enums/constants/.envpublish.sh,共 30 檔
  • 版本:commit 8aa6f060(jedi monorepo,branch feature/FR-075
  • 日期:2026-09-11
  • 工具:claude-security plugin,low effort,focus attack-surface
  • 面板:6 條候選 × 3 位檢查員 = 18 票全數投出,stamp verified

1. 🔴 一句話結論

我們安裝套件的管道是不加密的,只要有人能插進公司內網那條線,就能在我們安裝時把惡意程式掉包進來,直接拿下開發機與打包機——而打包機產出的東西是要送到客戶手上的。

第二件事:全站唯一那道「密碼遮罩」,遇到含有雙引號的密碼會遮一半、剩下半截原文寫進資料庫,而那張表要留 90 天。

好消息有一個:卡片原本列為「本棒最重要」的那條(信封組裝失敗把錯誤原文當資料回前端)經查不成立,那段程式進不去。詳見第 4 節 C3-5。


2. 這一棒在檢查什麼(白話)

jedi-common 是所有 jedi 套件與主產品共用的底層。這一棒看的是它的**「輸出面」**——資料整形完之後,要交出去的那一段:

  • 回給前端的信封長什麼樣({"status": ..., "data": ...} 那層)
  • 前端送來的分頁參數怎麼收(要第幾頁、一頁幾筆)
  • 物件怎麼變成 JSON(會不會夾帶不該給的欄位)
  • 密碼遮罩怎麼做(寫 log 之前把密碼蓋掉的那道)
  • 套件本身怎麼打包發布(會不會把憑證一起包出去)

要回答的是四個問題:回給前端的東西會不會夾帶不該有的欄位、分頁參數能不能拿來拖垮資料庫、共用工具裡有沒有不該存在的東西、打包腳本會不會把憑證帶出去。

只找問題、不修問題——所有修法都只寫建議,這一棒不動任何程式碼。


3. 掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 嚴重度 來源
C3-1 安裝套件走的是沒加密的連線,別人可以中途掉包 惡意程式裝進開發機與打包機,打包機產出的是要給客戶的東西 能插進內網那條線(公司 LAN、被入侵的網路設備) pyproject.toml:50 MEDIUM
(一位檢查員評 HIGH)
工具 3/3 票
C3-2 分頁的「一頁幾筆」沒有上限 一個請求就能叫資料庫把整張表撈出來,重複幾次服務就癱 任何登入者 interfaces/schema/common.py:13 MEDIUM 工具 3/3 票
C3-3 密碼遮罩遇到含雙引號的密碼只遮一半,剩下原文進資料庫 LDAP 綁定密碼、SSH 密語等強密碼的後半段明文留 90 天 密碼裡有 ";能讀到 log 表或備份的人 utils/common_utils.py:160 MEDIUM 工具 3/3 票
C3-4 同一道遮罩,JSON 跳脫形式的引號也一樣漏 同上,且 log 紀錄變成壞掉的 JSON、後續解析不可靠 同上 utils/common_utils.py:159 LOW 工具 3/3 票
C3-5 信封組裝失敗把錯誤原文當資料回前端 經查不成立——該分支進不去 utils/response_util.py:26 無問題 runner 人工查證
C3-6 一支開發輔助工具混在正式套件裡,且會把整個檔案覆寫成空 目前無人呼叫,但它跟著出貨,且拖著 openai 成為產品必需依賴 需有人真的去執行它 utils/gen_comment.py:49 LOW runner 人工查證
C3-7 序列化工具原樣吐出整個物件,沒有欄位過濾 物件上有什麼就回什麼,前端會拿到內部欄位 呼叫端沒有自己收斂欄位 utils/serialization_util.py:18 LOW(套件層)
個案在別處
runner 人工查證
C3-8 .env 受版控(CM-1598 已知) 經查:值是真的可連線設定,但密碼與 DEV 主庫不同 .env LOW(維持原評) 密鑰專項+人工比對

沒有發現的項目:⑥ savepoint 靜默吞例外——查了,結論寫在第 4 節 C3-9。⑦ 兩套信封並存——查了,舊信封零使用者,結論同節。


4. 每條發現的詳述

C3-1(MEDIUM)安裝套件走沒加密的連線,別人可以中途掉包

這是什麼問題(白話)

我們所有的套件(包含 25 支 jedi-* 自製套件,以及 Flask、SQLAlchemy 這些外部套件)都從公司內部的一台套件伺服器下載。問題是這條下載連線用的是 http:// 而不是 https://——白話說就是沒有加密、也沒有驗對方身分

這代表:只要有人能插進「我們的機器」到「那台套件伺服器」中間的網路路徑上(術語叫網路中間人,例如同一個辦公室網段、一台被入侵的交換器、或假冒的 DHCP/DNS 伺服器),他就可以假裝自己是套件伺服器,回一個被動過手腳的套件給我們,而我們的工具完全分辨不出來

更要緊的是這一行還標了 priority = "primary":51),意思是優先用它——所以不只是自家套件,連外部套件都走這條線。

出事會怎樣

被掉包的套件一旦裝上去,程式碼在 import 的當下就會執行。所以:

  • 開發人員的電腦被拿下
  • 打包機(188)被拿下——而打包機產出的 image 與安裝包是要送到客戶機房的,等於惡意程式跟著出貨
  • 執行 publish.sh 發版時,發版用的帳密也走同一條沒加密的線,一併被看光

要先有什麼才打得到

  • 攻擊者要能插進內網那條路徑(例如接進公司網路、或拿下一台網路設備)
  • 有人跑 poetry installpoetry updatepublish.sh——這是每天都在發生的事

在哪裡

  • pyproject.toml:50url = "http://192.168.50.171:8082/repository/pypi-group/simple"
  • pyproject.toml:51priority = "primary"(所以所有套件都走這條)
  • 同樣的設定在其他 24 支 jedi-* 套件與主專案的 pyproject.toml 內都有一份

怎麼修

  1. 讓那台 Nexus 伺服器支援 HTTPS(架 TLS 憑證)
  2. 把這 25 支套件與主專案 pyproject.toml 內的 http:// 全部改成 https://
  3. 如果用的是公司自簽憑證,把根憑證發到每台開發機與打包機,而不是因為「憑證有問題」就退回用 http://

嚴重度說明:評 MEDIUM 而非 HIGH,是因為需要先取得內網中間人位置這個前提。三位檢查員中有一位評 HIGH(理由是打包機的產出會流向客戶),工具取三票的中位數記為 MEDIUM。以我們的實際情境看,這條的投報率最高——修法明確、影響面最大、而且是一次改完 25 支就永久解決。

首腦核對註記pyproject.toml 不在本棒 30 檔的 scope 內(scope 只含 .envpublish.sh 兩支根目錄檔)。工具是為了追 publish.sh 的發版行為而越界讀到它。判定為真實且應報——publish.sh 的內容就是 poetry publish --build -r nexus,那個 nexus 指的正是 pyproject.toml:50 這一行,兩者是同一件事的兩半,不算離題。


C3-2(MEDIUM)分頁的「一頁幾筆」沒有上限

這是什麼問題(白話)

所有列表 API(專案列表、稽核列表、POA&M 列表……)都吃同一組分頁參數:「要第幾頁」和「一頁幾筆」。「第幾頁」有檢查(至少要 1),但**「一頁幾筆」完全沒有上限**。

所以任何登入的人都可以說「給我一頁一億筆」,系統就真的會去資料庫撈一億筆。

出事會怎樣

一個請求就能讓資料庫把整張表讀進記憶體、再轉成物件、再轉成 JSON。同時送幾個這種請求,應用程式的記憶體與資料庫連線就被吃光,其他客戶跟著一起卡住。

另一個角度:分頁本來也有「限制一次能撈走多少資料」的作用,沒上限等於這個限制不存在。

要先有什麼才打得到

  • 只要有任何一個有效登入帳號即可,不需要特殊權限
  • 影響的是所有繼承 RequestMetaSchema 的列表端點(feedback、module_frame、information_system、audit、poam 等)
  • 已確認沒有任何呼叫端自己補上限(整個主專案只有一支 flow_engine 的 schema 有上限,但它用的是不同的欄位名 size,救不到這裡)

在哪裡

  • jedi_common/interfaces/schema/common.py:13page_size = fields.Int(missing=25)
  • 對照上一行 :12page = fields.Int(missing=1, validate=validate.Range(min=1)),有檢查

🔴 這個上限是被刻意拿掉的,不是忘了寫

我查了 git 紀錄,commit a79d002(2026-03-03,標題「update pager max limit」)把這一行從:

page_size = fields.Int(missing=25, validate=validate.Range(min=1, max=100))

改成:

page_size = fields.Int(missing=25)

原本有 max=100,被整個移除了。 這件事很重要:修的時候要先弄清楚當初為什麼拿掉(很可能是某個頁面需要一次撈超過 100 筆),否則改回去會把那個功能弄壞。

怎麼修

  1. schema/common.py:13 加回上限,但值要放寬到實際需要的量: page_size = fields.Int(missing=25, validate=validate.Range(min=1, max=200))
  2. 另外在 base_repository_impl.py 的分頁實作裡再夾一層保險——在 .limit() 之前把值壓到上限內。這樣即使將來又有人把 schema 的檢查拿掉,資料庫也不會被打爆
  3. 先查清 a79d002 當初為何移除,確認真正需要的上限值

C3-3(MEDIUM)密碼遮罩遇到含雙引號的密碼只遮一半

這是什麼問題(白話)

系統會把每個 API 請求與回應的內容寫進資料庫的 api_logs 表存 90 天,方便除錯。為了不把密碼一起寫進去,中間有一道遮罩:看到欄位名稱像 passwordsecrettoken 這類,就把值換成 ***

問題出在這道遮罩是用文字比對做的,它認的格式是 "欄位名":"值",而它判斷「值到哪裡結束」的方式是**「找到下一個雙引號就算結束」**。

所以如果密碼本身含有雙引號——例如 ab"cdSUPERSECRET——遮罩會在密碼中間那個引號就停住,只遮掉前半段,後半段 cdSUPERSECRET 原文寫進資料庫

諷刺的地方:越是強度高、含特殊符號的密碼,越容易踩到這個洞。

出事會怎樣

api_logs.requestapi_logs.response 這兩個欄位會留著密碼的後半段明文,保留 90 天。會受影響的包括 LDAP 綁定帳號密碼、SSH 私鑰密語、各種偵測工具的憑證。

能看到的人:資料庫管理者、任何有 log 匯出權限的人、以及拿到資料庫備份的人(含 90 天內的任何一份備份)。

要先有什麼才打得到

  • 密碼裡有 " 這個字元。目前 LDAP 的 secret 欄位與工具憑證欄位都沒有限制可用字元,所以使用者完全可能設出這種密碼
  • 請求經過 common/middleware/app_mw.py——這是全域註冊的,每個請求都會過
  • 有人能讀到 api_logs 表、備份或匯出檔

在哪裡

  • jedi_common/utils/common_utils.py:160 — 那一行的值比對寫成 "[^"]*"[^"] 的意思就是「不是引號的字元」,所以碰到引號就停
  • 遮罩函式:同檔 :150 mark_password
  • 呼叫端:compliance-manager-be/common/middleware/app_mw.py:51(request)與 :101(response)

怎麼修

不要用文字比對來遮密碼。 正確做法是把內容當成 JSON 解析成結構,然後逐層走訪,看到欄位名是密碼類的就整個值換掉,最後再轉回文字。解析不了的就整包不要寫。

這個改法同時修掉同一道遮罩的另外兩個漏遮情形(見下方 C3-4 與補充):

  • 值不是字串就完全不遮"token": 123456789(數字)、"secret": ["abc"](陣列)都會原封不動寫進去,因為比對式只認 "值" 這種字串形態
  • 非 JSON 格式完全不遮:表單格式的 password=xxx 整包漏掉

首腦核對註記:這兩個補充情形(非字串值、表單格式)在面板被單獨列為一條候選(C5/F6),三位檢查員以 2:1 否決。否決理由是:目前系統裡所有標記為 secret 的欄位在資料庫 seed 裡都是字串型別、前端也只送字串;而表單格式的請求在到達遮罩之前內容已被讀空,實際寫進 log 的是空的。所以那條不成立——但既然遮罩要重寫,順手把這兩種形態一起處理掉,成本是零。


C3-4(LOW)JSON 跳脫形式的引號也一樣漏

這是什麼問題(白話)

這是 C3-3 的同一個根因,差別在密碼裡的引號寫進 JSON 之後會變成 \" 這種形式(前面多一個反斜線)。那道遮罩同樣不認識這種寫法,一樣在那裡停住。

分開記錄是因為它多一個副作用:產生出來的紀錄變成壞掉的 JSON,後續要解析 log 的程式會出錯。

出事會怎樣

密碼後半段留在 log 裡(同 C3-3),加上 log 紀錄本身格式壞掉、不能可靠地被解析。影響範圍比 C3-3 窄——只有引號之後那一段會漏。

在哪裡

  • jedi_common/utils/common_utils.py:159return re.sub( 那一段的值比對式

怎麼修

同 C3-3,改成結構化解析。如果基於某些理由一定要保留文字比對當備援,值的比對式要改成能認得跳脫字元的形式:"(?:\\.|[^"\\])*"

嚴重度說明:評 LOW 而非 MEDIUM,是因為只有「引號之後」那一段會漏,且要密碼剛好含引號。C3-3 評 MEDIUM 是因為它涵蓋的情境更廣(包含未跳脫的原始形態,以及整個密碼以引號開頭時全部外漏的情形)。


C3-5(無問題)信封組裝失敗把錯誤原文當資料回前端——查了,進不去

卡片把這條列為「本棒最重要」,結論是不成立。

卡片的疑慮是什麼

utils/response_util.py:23-27 有一段:

except Exception as e:
    status = False
    current_app.logger.exception(e)
    data = e.args[0]
    return {"status": status, "data": data}

擔心的是:如果信封組裝過程出錯,e.args[0](錯誤的原文,可能含檔案路徑、資料表名稱)會被當成 data 回給前端。

為什麼不成立

我把 try 區塊裡的程式逐行看過,裡面只有三件事:

  1. isinstance(data, dict) — 型別判斷,不會拋例外
  2. data.keys() — 前一行已確認是 dict 才會執行
  3. data.get(...) 與組一個新的 dict — 都不會拋例外

這個 try 區塊裡沒有任何會失敗的操作,所以 except 分支實際上進不去。真正的序列化(把 dict 轉成 JSON)發生在這個函式回傳之後,由 Flask 處理,不在這個 try 的範圍內——那裡出錯會走 Flask 自己的錯誤處理,不會走到這一行。

還是值得做的小事

這段程式碼本身是個誤導——它看起來像在防某件事,實際上什麼也沒防,反而讓後來的人(包含開卡的首腦)以為有風險。建議直接把 try/except 拿掉,或者改成重新拋出例外。這不是資安問題,是可讀性問題,優先度低。


C3-6(LOW)一支開發輔助工具混在正式套件裡

這是什麼問題(白話)

jedi_common/utils/gen_comment.py 是一支開發時用來自動產生程式註解的小工具——它把程式碼送給 OpenAI,請 AI 寫註解,再把結果寫回原始檔案

這種東西不該放在正式出貨的套件裡。它現在造成三個具體問題:

① 它會把檔案覆寫成空的。 :49 那一行寫 with open(file_path, "w"),但 file_path 這個變數在那個函式裡根本沒有定義(函式收到的參數叫 path,不是 file_path)。"w" 的意思是「開啟並清空」——所以真的有人去執行它,檔案會先被清空,然後才因為變數不存在而出錯,內容就沒了。

② 錯誤被整個吞掉。 :54 except Exception as e: return f"Error: {str(e)}",出了任何事都只回一個字串,不會有人發現。

③ 它讓 openai 變成產品的必要依賴。 pyproject.toml:24openai 列在正式依賴裡,而整個套件只有這支檔案用到它。等於每個客戶的機器都裝了一套 OpenAI 用戶端,只為了一支沒人呼叫的開發工具。

出事會怎樣

目前沒有立即的外洩風險——我已確認(grep 過整個 monorepo 25 支套件與主專案)零個地方 import 它,而且它需要 OPENAI_API_KEY 這個環境變數才會動,客戶機上不會有。

實際的問題是:它會跟著出貨。它放在 jedi_common/utils/ 底下,而打包設定 pyproject.toml:44 寫的是 packages = [{ include = "jedi_common" }]——整個目錄都包進去,所以這支檔案確實會進到客戶機器上的 wheel 裡。加上多一個沒必要的依賴、多一份攻擊面。

要先有什麼才打得到

  • 要有人真的去執行這支程式(目前沒有任何自動路徑會執行它)
  • 才會踩到「覆寫成空」那個問題

在哪裡

  • jedi_common/utils/gen_comment.py:8 — 模組載入時就讀 OPENAI_API_KEY
  • jedi_common/utils/gen_comment.py:49with open(file_path, "w"),用未定義的變數開檔覆寫
  • jedi_common/utils/gen_comment.py:54 — 吞光所有錯誤
  • pyproject.toml:24"openai (>=2.8.0,<3.0.0)" 列在正式依賴

怎麼修

直接把 gen_comment.py 從套件刪掉,同時把 openaipyproject.toml 的正式依賴移除。如果這支工具還有人要用,搬到 monorepo 的 scripts/ 或開發工具目錄,不要放在會出貨的套件內。

這跟 CM-1515 當初把 pytest/testcontainers 從正式依賴搬進 dev group 是同一類問題pyproject.toml:29-35 的註解有記錄那次),只是這次漏了 openai


C3-7(LOW,套件層)序列化工具原樣吐出整個物件,沒有欄位過濾

卡片點名要回答的問題:「to_serializable 是套件層的系統性問題,還是那一支的個案?」

答案:是「套件提供的工具本身不做過濾」(套件層事實),但目前只有一支套件在用它(使用面是個案)。

這是什麼問題(白話)

to_serializable 是一支把任意物件轉成 JSON 的工具。它的轉換方式是:物件上有什麼欄位,就全部吐出來——只跳過三個資料庫框架的內部欄位(metadataregistrysa_instance_state)和底線開頭的私有欄位。

換句話說,它沒有「哪些欄位不該給前端」的概念。如果丟一個資料庫物件給它,物件上有密碼鹽值、有內部識別碼,它就原樣一起吐出來。

使用面的實際情形(grep 全 monorepo 25 支套件 + 主專案的結果)

範圍 使用處數
主專案 compliance-manager-be 0 處
其他 24 支 jedi 套件 0 處
jedi-ai-dashboard 2 處

兩處都在 jedi-ai-dashboard

  1. app/service/ai_dashboard_app_service.py:191 — 取樣本資料送給 AI 當提示詞。這裡吐出的整包內容會進到送往 AI 模型的訊息裡
  2. domain/service/dashboard_generation_domain_service.py:249 — 在 _build_dynamic_table() 內,把每一筆資料轉好之後直接放進要回給前端的表格資料table_data.append(serialized)),而且下一行 _auto_extract_columns(table_data[0])拿第一筆有什麼欄位就自動生成表格欄位

所以 FR-083 D1 F2(AI 儀表板回傳夾帶 salt)的成因是什麼

是這兩件事相乘的結果:

  • 套件層to_serializable 不做欄位過濾(這是事實,但它是一支通用工具,不過濾本身是合理的設計)
  • 呼叫端jedi-ai-dashboard 拿它的輸出未經收斂就直接當回應資料,且欄位還是自動推導的

修哪一邊?建議修呼叫端,不要修套件。 因為 to_serializable 是通用工具,加上寫死的欄位黑名單會讓它在別的情境失準;而且「哪些欄位不能給前端」是業務知識,屬於呼叫端的責任。

怎麼修

dashboard_generation_domain_service.py:249 附近,改成白名單:只把 columns_config 指定的欄位放進 table_data,而不是整包塞進去再自動推導欄位。ai_dashboard_app_service.py:191 送 AI 的樣本同理,先挑欄位再送。

若要在套件層補一層保險(非必要,可選):給 to_serializable 加一個選用參數 exclude_keys,讓呼叫端可以指定要排除哪些欄位,預設行為維持不變。


C3-8(LOW,CM-1598 已知).env 受版控——值是真的,但不是主庫密碼

卡片要求:確認值是真憑證還是佔位符,只做相等性比對、不印值。

查了,結果如下(全程未印出任何值):

項目 結果
是否受版控 git ls-files .env 有列出
共幾個設定 6 個:DB_HOSTDB_PORTDB_NAMEDB_SCHEMADB_USERNAMEDB_PASSWORD
值是空的嗎 都不是空的,六個都有實際內容(用字元長度確認,未讀取內容)
密碼是否等於 DEV 主庫密碼 不相等——與主專案 .envDB_PASSWORD 做相等性比對,結果為「不同」

判讀:這是一組真的可以拿來連線的設定(不是 xxxchangeme 這種佔位符),但密碼與主專案 DEV 主庫用的那組不同。合理推測是這支套件跑測試時用的獨立資料庫帳號。

是否會被打包出去不會。 打包設定 pyproject.toml:44 只收 jedi_common/ 這個目錄(packages = [{ include = "jedi_common" }]),而 .env 在套件根目錄、不在 jedi_common/ 裡面,不會進到 wheel。所以它的外洩途徑只有一條:任何拿得到這個 git repo 的人都看得到

維持 CM-1598 原本的 LOW 評級,本棒不重報。 建議的處置照舊:從版控移除、加進 .gitignore、改該帳號密碼,並在 README 註明要自建 .env


C3-9(無問題)savepoint 靜默吞例外——查了,行為正確

卡片的疑慮utils/savepoint.py:30-35 的 rollback 失敗時 except Exception: pass 靜默吞掉,擔心資料庫連線會停在壞掉的交易狀態,連累後續請求。

查了,這個寫法是對的。 完整程式碼是:

try:
    yield
    sp.commit()
except Exception:
    try:
        sp.rollback()
    except Exception:
        pass
    raise          # ← 關鍵在這一行

理由:

  1. 原本的錯誤沒有被吞掉——最後那個 raise 會把它往外拋,呼叫端一定會知道出事了
  2. 被吞掉的只有「回滾這個動作本身又失敗」這件事。這種情況下連線已經壞了,再拋一個新例外只會蓋掉原本那個真正有用的錯誤訊息,讓除錯更難
  3. 外層的 @transaction 會在請求結束時關閉整個 session,不會把壞掉的連線留給下一個請求

這是處理巢狀交易的標準寫法,不需要改。 唯一可以做的小改善:在 except Exception: pass 那裡補一行 logger.warning,讓回滾失敗這件事至少留下痕跡,方便日後追查。優先度很低。


C3-10(無問題)兩套信封並存——查了,舊的那套零使用者

卡片的疑慮utils/response_util.py(新,status/data)與 utils/common_response.py(舊,code/msg/data)兩套信封並存,擔心兩套的錯誤處理不一致(一套遮、一套不遮)。

查了,舊信封目前沒有任何人在用。

grep 全 monorepo 25 支套件與主專案的結果:common_response 只出現在 2 個地方,而且兩處都是 response_util.py:7:9 的註解文字(那份註解正是在說明「這兩支不是同一支、不要搞混」),沒有任何一行實際的 import 或呼叫

所以不存在「兩套行為不一致」的風險——只有一套在跑。

建議:既然零使用者,可以直接刪掉 common_response.py,連同 response_util.py 檔頭那段說明註解一起清掉。這是整理工作不是資安修補,優先度低。


5. 卡片「重點看什麼」八項逐項回覆

卡片列了八個觀察點,逐項回答如下(不留白):

# 卡片的觀察點 查證結果
信封組裝失敗把例外訊息當 data 回前端(卡片標「本棒最重要」) 不成立try 區塊內沒有任何會拋例外的操作,except 分支進不去。詳見 C3-5。(工具未報,runner 人工查證)
gen_comment.py 不該進 runtime 套件 成立。零 importer(已 grep 25 支套件+主專案),但確實會被打包進 wheel(在 jedi_common/ 目錄內),且拖著 openai 成為正式依賴。詳見 C3-6。(工具未報,runner 人工查證)
分頁 page_size 沒有上限 成立,且是被刻意移除的——commit a79d002max=100 拿掉。工具三票一致確認。詳見 C3-2
序列化不濾敏感欄位;卡片指定要答「系統性還是個案」 答案:套件層工具確實不過濾(系統性事實),但使用面只有 jedi-ai-dashboard 2 處(個案)。主專案與其餘 24 支套件皆 0 處。 FR-083 D1 F2 是「工具不過濾」×「呼叫端未收斂」相乘的結果,建議修呼叫端。詳見 C3-7。(工具未報,runner 人工查證)
密碼遮罩只吃一種格式 成立,且比卡片預期的更嚴重——不只漏掉非字串與表單格式,連字串值本身含引號都會遮一半。工具報了兩條(C3-3/C3-4,各 3/3 票)。卡片原本擔心的非字串/表單情形被面板 2:1 否決,理由見 C3-3 末段
savepoint.py rollback 失敗靜默 不成立。最後有 raise,原始例外照常往外拋;被吞的只有「回滾本身失敗」,那是標準寫法。詳見 C3-9。(工具未報,runner 人工查證)
兩套並存的回應信封 不成立。舊信封 common_response 零使用者,只在註解裡被提到。不存在行為不一致風險。詳見 C3-10。(工具未報,runner 人工查證)
打包與發布:.env 是否真憑證、會否打進 wheel 值是真的可連線設定(六項皆非空),但密碼與主專案 DEV 主庫不同不會打進 wheel(不在 jedi_common/ 內)。另外查出發版腳本走的是沒加密的連線(C3-1,本棒最高投報率的發現)。詳見 C3-8 與 C3-1

6. 這份結果可信到什麼程度

分兩層回答,這兩層的可信度差很多,不要混在一起看。

第一層:「這些問題真的存在嗎?」——

  • 工具報的 4 條(C3-1~C3-4):每一條都經過三位獨立檢查員投票,全部 3:3 一致通過。18 票全數投出,沒有漏投,stamp 記為 verified
  • runner 人工查證的 6 條(C3-5~C3-10):每一條我都自己打開檔案看過那幾行,行號逐一核對過。其中「不成立」的四條(C3-5、C3-9、C3-10 與 ⑤ 的部分)是讀完程式邏輯後的判斷,不是猜的
  • git 紀錄的證據是硬事實:C3-2 的 a79d002 移除上限、C3-7 的 grep 計數,這些是可以重現的查詢

但要講清楚:沒有任何一條做過實際攻擊驗證。 掃描過程完全沒有執行任何程式碼,所有結論都來自讀程式碼。例如 C3-1 沒有真的架中間人測試、C3-2 沒有真的送 page_size=一億 打看看。要實測的話需要另外開一棒。

第二層:「只有這些嗎?」——中等,不能說這個範圍掃透了

  • 面板完整跑完(這一點比前幾棒好):6 條候選 × 3 票 = 18 票全投出,沒有中途陣亡。所以「工具看過的部分」是有結論的
  • 但工具本來就報得保守:它只報「能講出完整攻擊路徑」的問題。C3-5~C3-10 六條全是卡片指定要追、但工具沒報的——其中兩條(C3-6、C3-7)是真的有問題,四條查完是沒問題。這代表工具的沉默不等於乾淨
  • enums/constants/ 這兩個目錄沒有任何發現。這符合預期(純常數定義),但我沒有逐檔讀完,只能說「工具讀過、沒報」
  • 這一棒是 low effort:只派一位研究員讀完整個範圍,沒有威脅建模、沒有廣度掃描。這是為了讓面板跑得完而刻意選的設定(見首腦手冊第一節),代價就是研究深度

🔴 越界說明(必讀)

C3-1 的 pyproject.toml:50 不在本棒 30 檔的 scope 內。

工具是為了追 publish.sh(在 scope 內)的發版行為而讀到它的。我判定應該保留這條,理由是 publish.sh 的全部內容就是 poetry publish --build -r nexus,那個 nexus 指向的正是 pyproject.toml:50——兩者是同一件事的兩半,拆開來看沒有意義。

但要提醒首腦:這個問題不是 jedi-common 專有的,同樣的 http:// 設定在其餘 24 支套件與主專案的 pyproject.toml 內都有一份。開修正卡時應該當成一張跨全 monorepo 的卡,不要當成 FR-085 的子問題。

其餘 9 條(C3-2~C3-10)全部落在 scope 內,無越界。


7. 執行概況

項目 數字
掃描範圍 30 檔(git ls-files 實測,與卡片一致)
版本 commit 8aa6f060
effort/focus lowattack-surface(含密鑰專項)
研究員派出/回報 2/2(零陣亡)
候選發現 6 條
面板票數 18 票(6 條 × 3 位檢查員),全數投出,零漏投
通過(3:3 一致) 4 條
否決 2 條(一條 0:3、一條 1:2)
嚴重度被面板下修 0 條
stamp verification.status verified
驗證輪數 1 輪(無候選遺失、無待驗)
工具耗時 約 41 分鐘
runner 人工追加 6 條(卡片八項中工具未答的部分)
產物位置 jedi-common/CLAUDE-SECURITY-20260911-034053/

被否決的兩條是什麼(值得記錄,這是三人面板的價值)

  1. 「巢狀物件裡的密碼不會被遮」——0:3 全數否決。檢查員指出遮罩作用在攤平的文字上而不是解析後的結構,所以不管巢狀多深都照遮,並引用主專案自己的測試 test_api_log_secret_masking.py:14-37 為證(那個測試就是在驗巢狀密碼有被遮掉)
  2. 「非字串值與表單格式的密碼會漏」——1:2 否決。理由是目前所有 secret 欄位在資料庫 seed 裡都是字串型別、前端也只送字串;表單格式的內容在到達遮罩前已被讀空

這正是面板的用處:沒有它,會有人花時間去修兩個實際上不存在的問題。


8. 建議的下一步

按投報率排序(這是建議,開卡與排序由首腦裁決):

  1. C3-1(換 HTTPS)投報率最高——修法明確(架 TLS + 改 25 支 pyproject.toml),一次解決永久有效,而且它威脅的是打包機,產出要送客戶。建議開成跨 monorepo 的獨立卡,不掛在 FR-085 底下
  2. C3-3/C3-4(重寫密碼遮罩)綁一起修——同一個根因、同一支函式,順手把非字串與表單格式一起處理掉,成本是零
  3. C3-2(分頁上限)修前要先查 a79d002 為何移除——直接改回去可能弄壞某個需要大量撈資料的頁面
  4. C3-7(AI 儀表板欄位白名單)與 FR-083 D1 F2 是同一件事——建議併卡,修呼叫端不修套件
  5. C3-6(刪 gen_comment.py + 移除 openai 依賴) — 低風險的清理,可以順手做
  6. C3-5/C3-9/C3-10 三條「無問題」不需開卡,但 C3-5 的死 try/except 與 C3-10 的零使用者舊信封建議順手清掉,避免下一個人又被誤導(C3-5 這次就誤導了開卡的首腦)

這一棒到此結束,FR-085 三棒全部掃完。