U10 檢查結果:系統診斷包的下載入口與組裝

U10 檢查結果:系統診斷包的下載入口與組裝

檢查日期 2026-09-25|對應卡片 CM-2162(母卡 CM-2152)|檢查範圍 8 個檔案 1,295 行|工具 run ID wf_c03e5bb6-44b

§1

🔴 一句話結論

下載入口只有兩條,都守得住。但診斷包的「遮罩」(把密碼換成 *** 的那道手續)認得的密碼寫法太少,而且三種收集物用的遮罩強度不一樣。 實測有十幾種常見寫法的密碼會原樣進包。

掃描工具提出 1 個候選,三人面板 0:3 否決,正式清單零條。我照卡片逐支開檔,也真的在本機組了一包診斷包打開檢查,另外找到四件事:

  1. (中)遮罩漏網。 設定檔、應用日誌、資料庫紀錄、錯誤事件各走不同的遮罩函式。其中資料庫紀錄和「最近錯誤」頁面用的那支最弱,連 Authorization: Bearer … 這種最典型的登入權杖寫法都不遮。四條路徑都不認得 password=值、網址裡的帳密、export 名稱=值。
  2. (低)稽核只記「有人按了匯出」,沒記「成功還是失敗」。 程式開頭的說明寫著「成敗另記一筆,見 _audit_export」,但這支函式根本不存在。
  3. (低)只帶起點、不帶終點時會回 500。 起點帶時區、終點沒帶時,兩個時間比大小會當掉。只有平台管理員打得到。
  4. (資訊)資源用量。 畫面版有 30 天上限,打包期間佔住一個工作程序。日誌切片宣稱上限 40MB,實測記憶體峰值是檔案大小的 1.3 倍。

⚠️ 給首腦:派工提到「總表第 173 項也是診斷包遮罩」,但總表第 173 項實際是「SSP Word 檔卡死解析」。 診斷包遮罩的既有項目是第 162 項(AI 金鑰以 KEY=值 形狀進日誌,診斷遮罩不認得這種寫法)。

§2

這一棒在檢查什麼

「系統診斷包」是客戶系統出問題時,平台管理員把整台機器的現場(設定、日誌、資料庫紀錄、錯誤事件)打成一個 tar.gz 壓縮檔,寄給原廠分析用的。這個包不加密,會經過客戶的電腦、郵件或隨身碟、原廠的電腦,最後被貼進 AI 對話。每一站都可能外洩,所以包裡的密碼遮得乾不乾淨是這個功能唯一的防線。

要回答的問題是:誰能產這個包?產出來的包裡,密碼真的都被遮掉了嗎?

檔案 行數 角色
app/support/diag_cli.py 366 指令版入口(sudo guidant diag),含應用起不來時的降級產包
app/support/service/diag_bundle_app_service.py 334 打包核心:指令版與畫面版共用,逐項收集後交給打包器
domain/support/service/diag_slice_domain_service.py 270 決定從資料庫撈哪些紀錄;另供「最近錯誤」頁面用
app/support/service/diag_bundle_export_service.py 133 畫面版匯出:守門、檢查時間範圍、寫稽核事件
api/support/routes/diagnostic_bundle_route.py 67 畫面版下載網址 POST /support/diagnostic-bundle
app/support/service/recent_error_app_service.py 52 支援頁「最近的系統錯誤」
api/support/routes/recent_error_route.py 43 GET /support/recent-errors
api/support/__init__.py 30 登記上面兩支網址

實際做遮罩、讀檔的程式在 infra/support/,屬 U11 範圍。本棒為了回答卡片的問題有讀、也有實測,但該處的問題以本棒視角(組裝時沒有把關)報,U11 不必重報。

§3

掃到什麼(總覽)

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 該補檢查的位置 嚴重度+為什麼 來源
U10-1 遮罩只認少數幾種密碼寫法,四條收集路徑用的遮罩強度還不一樣 系統別處只要用常見寫法把密碼寫進日誌或資料庫紀錄(例如 password=xxx、Bearer xxx、postgresql://帳號:密碼@主機),就會原樣進診斷包,跟著包寄到原廠、貼進 AI 對話 客戶產過一次診斷包,而且系統某處剛好用這些寫法記錄了機密。不需要攻擊,正常維運就可能發生。已知一個真實來源:第 162 項(AI 金鑰) 組裝入口 app/support/service/diag_bundle_app_service.py:115-120(打包前沒有統一的最後一道遮罩);遮罩規則本身在 infra/support/diag_masking.py:48、:57-61、:108-117;資料庫紀錄走弱版的位置 infra/support/diag_db_reader.py:237;最近錯誤 domain/support/service/diag_slice_domain_service.py:245 🟠 中:不必攻擊、正常維運就會發生;前提是別處把機密寫進日誌,已有真實案例。出貨設定檔本身的 11 個機密欄位全部遮得到,所以不是「設定檔直接外洩」那種高 runner 自行開檔+實測(未經三人面板)
U10-2 稽核事件只記「有人按匯出」,沒記成功或失敗;說明文字指向一支不存在的函式 查帳時看得到誰試圖匯出,但不知道他有沒有真的拿到包 平台管理員匯出過診斷包 app/support/service/diag_bundle_export_service.py:90-99(產包成功、失敗後各補一筆稽核);說明文字 :30 🟢 低:意圖有記,只是缺結果;不影響守門 runner 自行開檔(未經三人面板)
U10-3 起點帶時區、終點沒帶時,比對時間會當掉,回 500 平台管理員看到「系統錯誤」而不是「時間範圍不對」;日誌多一筆 ERROR 平台管理員身分,而且只送 since、不送 until app/support/service/diag_bundle_export_service.py:120-122(比大小前先把兩個時間統一成同一種時區格式) 🟢 低:只有平台管理員打得到,後果只是錯誤訊息難看 runner 實測(未經三人面板)
U10-4 畫面版產包同步佔住一個工作程序;日誌切片的記憶體用量是檔案大小的 1.3 倍 最壞情況:選 30 天、日誌很大時,產包那段時間少一個工作程序可用,記憶體吃掉數百 MB 平台管理員身分 app/support/service/diag_bundle_export_service.py:91(改背景工作);infra/support/diag_log_reader.py:128-150(邊讀邊丟最舊的行,不要全部讀完才截) ⚪ 資訊:門檻是平台管理員,而且已有 30 天上限 runner 實測(未經三人面板)

現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。

  • U10-1(遮罩只認少數寫法)=總表第 162 項剩餘(#173),✅ 已修(CM-2212,commit c8534bfcb/8f5755f1d,1.21.0 出貨)。
  • U10-3(時區比對當掉回 500)=總表第 206 項,✅ 已修(CM-2214,commit d39181f3b/046553756,1.21.0 出貨)。
  • U10-2(稽核事件只記意圖)、U10-4(畫面版同步佔工作程序):SUMMARY 無對應條目,查不到現況。

工具報的:零條(1 個候選,面板 0:3 否決,見下節)。

§4

工具報的逐條

工具派了兩位研究員:一位讀全部範圍,一位專找寫死在程式裡的密碼金鑰。後者交回空清單。前者提出一個候選:

  • 候選 C1:「請求內容原樣進包,會對原廠的 AI 分析造成間接提示注入」(研究員評低)。說法是:未登入的人送請求時,在瀏覽器識別字串(User-Agent)或網址裡寫一段假指令,這段文字會進存取紀錄,再原樣進診斷包的 db/api_logs.csv。原廠把包貼進 AI 對話時,AI 可能把這段文字當成指令。
  • 面板 0:3 否決,三票理由一致:這個程式庫裡沒有任何程式把診斷包送進 AI,貼進 AI 是原廠人員手動做的,發生在系統之外。把使用者送來的資料記進日誌、再匯出日誌,是這個功能本來就要做的事。
  • 我的看法:同意否決,但原廠要知道這個風險。 程式碼確實沒有危險的動作。不過「診斷包會被貼進 AI」是寫在程式說明裡的預期用法(infra/support/diag_masking.py:3-4,資料庫欄位說明也是專為 AI 寫的)。原廠分析診斷包用的 AI 若有執行指令的能力,應該把包內容當不可信資料看待。這是原廠內部作業規範的事,不是這個 repo 要修的。
§5

卡片點名的疑點逐條回答

① 環境變數與日誌有沒有遮罩密碼金鑰?——⚠️ 有遮,但認得的寫法不夠 → 成立(U10-1)

每一類收集物打包前走哪支遮罩(逐支開檔確認):

收集物 包內檔名 走哪支遮罩 強度
設定檔(指令版讀檔/畫面版讀當下環境變數) env/guidant.env.masked mask_env_text 只認「名稱像密碼」的 名稱=值
應用日誌 logs/app.log.slice mask_log_lines JSON 形狀+名稱: 值 形狀
存取紀錄、稽核與錯誤紀錄(資料庫) db/api_logs.csv、db/system_logs.csv mask_json_like 只認 JSON 形狀
錯誤收集站的事件 events/glitchtip-events.json mask_structure JSON 形狀+欄位名稱像密碼
支援頁「最近的系統錯誤」(瀏覽器顯示) — mask_json_like 只認 JSON 形狀
各容器的紀錄 logs/docker-*.log 完全沒過遮罩 — (=U2-2,U11 範圍,本棒不重報)
版號、授權開關、磁碟、容器狀態、資料表筆數、改版水位 env/*、db/row-counts.csv 不需要(本來就不含機密) —

實測:我寫了一支小程式,把 30 多種寫法的假密碼 SeCrEt123 分別丟進四支遮罩函式,看輸出還有沒有 SeCrEt123。結果(✅=遮掉,❌=原樣留下):

密碼的寫法 設定檔遮罩 日誌遮罩 資料庫紀錄/最近錯誤遮罩
DB_PASSWORD=值 ✅ ❌ ❌
export DB_PASSWORD=值(shell 常見寫法) ❌ ❌ ❌
DATABASE_URL=postgresql://帳號:密碼@主機/庫 ❌ ❌ ❌
DB_PASS=值、SMTP_PWD=值(縮寫名稱) ❌ — —
多行的值(例如 JSON 或憑證跨好幾行,只有第一行被遮) ❌ — —
Authorization: Bearer eyJ…(標準登入權杖標頭) — ✅ ❌
單獨出現的 Bearer eyJ…(例如錯誤訊息「token invalid Bearer eyJ…」) — ❌ ❌
cookie: session=值、X-Api-Key: 值 — ✅ ❌
{"password": "值"}(JSON) — ✅ ✅
{'password': '值'}(Python 印字典的寫法,單引號) — ❌ ❌
網址參數 ?token=值 — ❌ ❌
["docker","run","-e","ANTHROPIC_API_KEY=值"](=第 162 項的真實形狀) — ❌ ❌

錯誤收集站的事件也實測了:欄位名稱叫 password 的會遮,但字串裡寫著 Bearer 值 或 password=值 的不會。

白話說明:遮罩的做法是「看到長得像密碼的東西就換成 ***」,問題出在「長得像」的定義太窄:

  • 資料庫紀錄和「最近錯誤」用的是最弱的那支。 遮罩模組自己的說明寫著「四條路徑共用同一份判準,不分岔——分岔的症狀是『設定檔遮了、存取紀錄沒遮』」(infra/support/diag_masking.py:26-28),但實際上資料庫紀錄只過了 JSON 形狀那一道,沒過日誌那一道。db/system_logs.csv 的 message 欄裡是 ERROR 等級的日誌原文,跟 logs/app.log.slice 是同一種內容,遮罩卻比較弱。
  • 沒有任何一支認得 名稱=值 出現在一行文字中間。 第 162 項已經記錄了一個真實來源:AI 金鑰以 ANTHROPIC_API_KEY=明文 的形狀寫進日誌。本棒實測確認,這個形狀在日誌、資料庫紀錄兩條路都原樣留下。
  • 設定檔遮罩不認得網址裡的帳密、export 開頭、縮寫名稱、多行的值。 畫面版讀的是「當下進程的環境變數」,會用 名稱=值 一行一個串起來(infra/support/collector/container_collector.py:94)。值本身跨好幾行時(例如 JSON 格式的資料庫密鑰),只有第一行被遮。

出貨設定檔本身沒問題:我把安裝程式寫出的 guidant.env 範本(scripts/installer/install.sh:1379-1456、:1672-1690)逐欄丟進設定檔遮罩,11 個放機密(或機密檔路徑)的欄位全部遮掉。沒遮的都是主機名稱、埠號、開關、路徑這類本來就不是機密的值。出貨版 compose 也沒有注入帳密連線字串(docker/production/docker-compose.yml)。所以「今天出貨的設定檔直接外洩」不成立,風險在「日誌與資料庫紀錄裡混進機密」。

DEV 實際資料查證(2026-09-25 16:05 +08,全程唯讀連線,只下 SELECT):最近 30 天的 system_logs 共 1,491 筆,其中 10 筆含 password= 形狀,逐筆看過,全部是例外堆疊裡印出的程式原始碼那一行(例如 server.login(user=self.config.username, password=self.config.password)),值是變數名稱不是真密碼,所以 DEV 目前沒有真密碼進包。api_logs 7,723 筆裡沒有網址帶權杖參數的紀錄。打折處:DEV 資料量與使用樣態跟客戶現場不同,這只證明「DEV 今天沒有」,不證明「客戶那邊不會有」。

真的組一包:我在本機用指令版產了一包(python -m app.support.diag_cli --since 2h),設定檔換成一份含 8 個假密碼的測試檔,解開檢查:

  • DB_PASSWORD、GUIDANT_DB_ADMIN_PASSWORD、JWT_SECRET_KEY、AGENT_CA_KEY 遮掉了,也列在 manifest.json 的遮罩清單裡。
  • export REDIS_PASSWORD=、DATABASE_URL=postgresql://cmmgr:密碼@…、DB_PASS= 三個假密碼原樣出現在包裡,而且 manifest.json 的遮罩清單沒有列出它們。讀包的人看了清單,會以為這三行是無害設定。
  • 打折處:本機應用起不來(跟本棒無關的平行線錯誤:FlowControlJobRepoImpl 缺一支方法),所以這包走的是「降級產包」路徑,沒有資料庫紀錄那半。資料庫紀錄那半的結論來自直接呼叫遮罩函式實測,不是從真包裡看到的。

建議修法(遮罩規則在 U11 範圍的 diag_masking.py,這裡只給方向):

  1. 資料庫紀錄與「最近錯誤」改走日誌那支(mask_log_lines 的單行版),讓四條路徑真的共用同一套判準。這一步改動最小、效果最大。
  2. 日誌遮罩補認 名稱=值(第 162 項修法欄已寫「診斷遮罩補認 KEY=值」,同一件事)、單獨出現的 Bearer 值、網址參數、帳號:密碼@ 形狀的連線字串、單引號字典。
  3. 設定檔遮罩補認 export 開頭、縮寫名稱(PASS、PWD)、值裡含 ://帳號:密碼@ 的一律遮。畫面版不要用 名稱=值 串環境變數,改成逐個判斷。
  4. 組裝入口加一道最後檢查(diag_bundle_app_service.py:115-120 收齊之後、交給打包器之前):拿已知的機密值(程式啟動時讀到的資料庫密碼、權杖簽章金鑰等)逐檔搜一遍,搜到就遮,並在 manifest.json 記一筆。這是「不管寫法、只管值」的保底,能擋住所有目前沒想到的寫法。

② since/until 有沒有上限?——✅ 畫面版有,不成立;指令版刻意不設

  • 畫面版:最長 30 天(diag_bundle_export_service.py:53),超過就回 400 擋下,不會偷偷縮短範圍(:122-123)。起點比終點晚也擋。
  • 指令版:--since 沒有上限,而且有 --no-limit 可以關掉 100MB 包大小上限(diag_cli.py:88-91)。這是給到場工程師用的,要先有主機 root 權限,不算漏洞。
  • 各層還有第二道上限:日誌切片 40MB(infra/support/diag_log_reader.py:57)、資料庫每張表 20 萬筆(infra/support/diag_db_reader.py:45)、容器紀錄每支 5,000 行、整包 100MB。
  • 實測發現一個副作用 → U10-3:只送 since(帶時區)、不送 until 時,until 由伺服器補成「現在」(沒帶時區),兩個時間比大小時 Python 直接拋錯(TypeError: can't compare offset-naive and offset-aware datetimes),而且這一行在 try 外面,會冒成 500。兩個都送或都不送就正常。打包核心那層有處理混合時區(diag_bundle_app_service.py:53-66),但匯出服務在呼叫核心之前就先比了,沒走那道處理。
  • 資源用量 → U10-4:我造了一個 132MB 的日誌檔讓切片去讀,輸出正確截在 39MB,但記憶體峰值 169MB。原因是程式先把範圍內所有行讀進記憶體,最後才截尾端。畫面版產包在網頁請求裡同步執行,產完才回應,期間佔住一個工作程序(落地版預設只有 4 個)。門檻是平台管理員,不算漏洞,列出供知悉。

③ 「最近的系統錯誤」會不會帶出堆疊與內部路徑?——⚠️ 會,屬設計;但遮罩太弱(併入 U10-1)

  • 回傳的 message 是日誌原文,會含完整的例外堆疊和主機上的檔案路徑。DEV 查證:最近 200 筆 ERROR 裡有 10 筆含堆疊與路徑。source 欄另外給「檔名.函式:行號」。
  • 這是刻意的設計:這個頁面就是給平台管理員「使用者剛說壞了,我看一眼是什麼錯」用的(recent_error_app_service.py:1-11),需要看到堆疊。守門是平台管理員,不算洩漏。
  • 但遮罩只過了最弱的 mask_json_like(diag_slice_domain_service.py:245)。程式說明自己寫了「這支端點的內容會顯示在瀏覽器上、也會被貼進工單,與診斷包同一個外流路徑」(:229-230),卻只給了比診斷包日誌更弱的遮罩,所以錯誤訊息裡的 Bearer 值、名稱=值 會原樣顯示在畫面上。歸在 U10-1 一起修。
  • 筆數上限 200(domain/support/entity/diag_bundle.py:95),超過自動降到上限,不會一次撈爆。

④ 下載入口除了平台管理員,還有沒有別的路?——✅ 只有兩條,都守得住,不成立

逐一核對產包核心 build_bundle() 的所有呼叫者(全 repo 搜尋):

入口 誰能用 守門位置
畫面版 POST /api/1.0/support/diagnostic-bundle 登入+平台管理員 網址層 @jwt_required()(diagnostic_bundle_route.py:35);服務層第一行 require_platform_admin()(diag_bundle_export_service.py:74)
指令版 sudo guidant diag 主機 root scripts/installer/guidant:658-779:主機上以 root 收集,再 docker exec 進應用容器執行
開發機 python -m app.support.diag_cli 能登入開發機、讀得到 .env 的人 作業系統權限(出貨主機上沒有 Python)
  • 沒有簽章網址、沒有第二支路由、沒有背景排程會自動產包。 api/support/__init__.py 只登記兩支網址,DI 容器(di_containers/support/support_containers.py)也只接了這兩條。
  • RUN_MODE=diag(main.py:82-87)只在啟動時生效,產完包就結束,不會開任何網路埠。
  • 「最近錯誤」GET /support/recent-errors 同樣是網址層登入+服務層平台管理員(recent_error_app_service.py:46)。
  • 產出的包放在只有自己讀得到的暫存目錄(tempfile.mkdtemp,權限 0700),包內每個檔案權限 0600,回應送完就刪(diagnostic_bundle_route.py:51-57)。我確認了下載套件是先開檔、再在回應結束後刪,不會下載到空檔。小瑕疵:產包失敗時(diag_bundle_export_service.py:97-99)暫存目錄不會被刪,留下半成品(內容已遮罩)。影響很小,順手修即可。

額外核對平台管理員的判定:require_platform_admin() 判的是「你的公司是不是最上層公司」(身分套件 jedi_iam/authz/platform.py:15-32 → _session_paths_have_root),不看角色。落地版的客戶公司一律建在原廠系統殼(第 1 號公司)底下(app/setup/service/setup_wizard_service.py:14-16),所以客戶的管理員不是平台管理員,這扇門擋得住客戶。「最上層公司裡的非管理員角色也算平台管理員」是 U1(權限檢查共用層)的範圍,本棒不重報。

⑤ 環境變數、授權檔、資料庫摘要、事件逐項看——✅ 除 U10-1 外不成立

  • 授權檔:只收兩個開關的真假值,不含授權檔內容或任何金鑰(diag_bundle_app_service.py:197-233),開檔確認屬實。
  • 資料庫摘要:資料表筆數用的是估計值,只有數字,沒有資料內容;改版水位只有版號清單。都不含機密。
  • 資料庫讀取繞過客戶隔離:DiagDbReader 用 SET LOCAL app.is_super_admin = 't' 看全部客戶的紀錄(infra/support/diag_db_reader.py:63)。這是刻意的(診斷包要看整台機器),前提是入口只給平台管理員,已由 ④ 確認。
  • 降級產包:應用起不來時,錯誤原文會寫進 manifest.json(diag_cli.py:311-334),沒過遮罩。資料庫連線失敗的錯誤訊息一般帶主機、帳號,不帶密碼,所以風險低。建議順手過一次 mask_log_lines。

⑥ 「有人守了一半」八種樣式——除 U10-2 外不成立

樣式 結論
只驗身分、不驗資料歸屬 不適用:診斷包是整台機器的現場,不屬於任何客戶,平台管理員本來就該看全部
列表有查、單筆沒查 不適用:沒有單筆存取
守門寫在網址層,第二支路由沒掛 不成立:守門在服務層,兩支網址都經過
守門條件用 or 串起來 不成立:單一條件
「查不到」和「沒權限」混在一起 不適用
背景或系統身分執行時,假設呼叫者一定是自己人 指令版以系統身分執行,但入口是主機 root,本來就是自己人。成立但合理
防重放、計次只在單一進程內有效 不適用:沒有計次
註解說這裡刻意不檢查,上一層卻沒檢查 路由註解說「守門刻意不放在這裡」,服務層確實有檢查 ✅。但另一句註解說「成敗另記一筆結果,見 _audit_export」,這支函式不存在 → U10-2
§6

逐條細節

U10-1 — 遮罩只認少數幾種密碼寫法,四條收集路徑的遮罩強度不一樣(中)

  • 嚴重度:🟠 中
  • 位置(該補檢查的地方):
    • 組裝入口:app/support/service/diag_bundle_app_service.py:115-120(收齊所有收集物之後、交給打包器之前,沒有統一的最後一道遮罩)
    • 資料庫紀錄只走弱版:infra/support/diag_db_reader.py:237(mask_json_like)
    • 最近錯誤只走弱版:domain/support/service/diag_slice_domain_service.py:245(mask_json_like)
    • 遮罩規則:infra/support/diag_masking.py:48(額外名稱清單)、:57-61(設定檔規則)、:164-169(日誌規則)
  • 白話說明:見「卡片點名的疑點 ①」。簡單說,診斷包把密碼換成 *** 的方式是「比對長相」,而它認得的長相太少。四條收集路徑裡,有兩條用的是最窄的版本。
  • 影響:系統任何一處用常見寫法把機密寫進日誌或資料庫紀錄,都會跟著診斷包寄給原廠、貼進 AI 對話。manifest.json 的遮罩清單只列出「有被遮到的」,讀包的人會誤以為包是乾淨的。
  • 觸發前提:客戶產過診斷包,而且日誌或資料庫紀錄裡剛好有這些寫法的機密。已知來源:第 162 項(AI 金鑰以 名稱=值 進日誌,本棒實測確認兩條路徑都沒遮)。不需要攻擊。
  • 與既有項目的關係:
    • 第 162 項的修法欄已寫「主專案:診斷遮罩補認 KEY=值」,那只是本條的一部分(日誌路徑的 名稱=值)。本條新增的是:資料庫紀錄與「最近錯誤」根本沒走日誌遮罩、單獨出現的 Bearer、網址帶帳密、export、多行值、manifest 遮罩清單會誤導讀包的人。建議首腦判斷要併進第 162 項,還是另開一條。
    • U2-2(即時通訊權杖進容器紀錄)是容器紀錄「完全沒過遮罩」,屬 U11 的收集器,本條不含。
  • 實測:本機直接呼叫四支遮罩函式,30 多種寫法逐一比對;另在本機真的產一包,解開確認三個假密碼原樣留在 env/guidant.env.masked、且沒列進 manifest.json。DEV 資料庫唯讀查證,目前沒有真密碼符合這些寫法。
  • 建議修法:見「卡片點名的疑點 ①」末段四點。最優先的是第 1 點(資料庫紀錄與最近錯誤改走日誌那支),改兩行就能拉到跟日誌一樣的強度。

U10-2 — 稽核事件只記意圖,沒記成功或失敗(低)

  • 嚴重度:🟢 低
  • 位置:app/support/service/diag_bundle_export_service.py:90-99(產包成功與失敗兩個分支都沒有寫稽核);說明文字 :26-30
  • 白話說明:程式在開始產包前寫一筆「有人匯出診斷包」的稽核事件,這個設計很好(產包中途當掉也留得下紀錄)。說明文字接著寫「成敗另記一筆結果,見 _audit_export」,但全 repo 搜不到 _audit_export,程式裡也沒有第二筆稽核。
  • 影響:事後查「誰把系統現場帶走了」時,只知道誰按過按鈕,不知道他有沒有真的拿到包。應用日誌裡有一行「診斷包已產出」(:101-107),但那不是稽核事件,會隨日誌輪替被清掉。
  • 觸發前提:平台管理員匯出過診斷包。
  • 建議修法:產包成功後補一筆稽核(帶檔案大小與收錄檔數),失敗時也補一筆(帶錯誤代碼);或者把說明文字改成跟現況一致。

U10-3 — 只帶起點(帶時區)不帶終點時回 500(低)

  • 嚴重度:🟢 低
  • 位置:app/support/service/diag_bundle_export_service.py:120-122
  • 白話說明:網頁傳來的時間帶時區(+08:00),伺服器補的「現在」不帶時區,Python 不允許這兩種時間比大小,直接拋錯。這一行在 try 外面,所以錯誤冒到全站處理、回 500。
  • 影響:平台管理員看到「系統錯誤」而不是「時間範圍不對」;日誌多一筆 ERROR;有接錯誤收集站的話會多一筆假警報。不會放行、不會洩漏。
  • 觸發前提:平台管理員身分;請求只帶 since、不帶 until(前端正常是兩個都帶或都不帶,所以一般使用不會遇到)。
  • 實測:用正式的請求格式解析四種組合,逐一交給 _resolve_window:只帶 since(有時區)→ TypeError;只帶 until、兩個都帶、since 沒時區 → 正常。
  • 建議修法:比大小前先用打包核心已有的 _as_naive_local(diag_bundle_app_service.py:53-66)把兩個時間統一格式,或者在補「現在」時帶上時區。

U10-4 — 產包同步佔住工作程序;日誌切片記憶體用量偏高(資訊)

  • 嚴重度:⚪ 資訊
  • 位置:app/support/service/diag_bundle_export_service.py:91(同步產包);infra/support/diag_log_reader.py:128-150(先全部讀完才截)
  • 白話說明:畫面版按下匯出後,伺服器在同一個網頁請求裡把包產完才回應。範圍大時可能要幾十秒,期間這個工作程序不能處理別人的請求。日誌切片先把範圍內所有行讀進記憶體,最後才留尾端 40MB。
  • 實測:造一個 132MB 的日誌檔讓切片去讀,輸出 39MB、正確標示已截斷,記憶體峰值 169MB。
  • 影響:平台管理員選 30 天、日誌量大時,產包那段時間伺服器少一個工作程序、多吃數百 MB 記憶體。
  • 觸發前提:平台管理員身分。
  • 建議修法:不急。之後要改的話,切片改成邊讀邊丟最舊的行(固定大小的佇列),畫面版改背景工作+完成後下載。
§7

這份結果可信到什麼程度

第一層:工具正式報告(經三人面板驗證)

  • 驗證章:verified(stamp CLAUDE-SECURITY-REVISION-93c57739b77a-dirty.json)
  • 候選 1、面板 3 票(0:3 否決)、研究員派出 2/交回 2、failed 0、續跑 0、沒有遺失的候選。
  • 正式清單零條,不代表這 8 支乾淨。工具的研究員看到了「請求內容原樣進包」,但沒有去實測遮罩函式,所以沒發現遮罩本身的漏洞。

第二層:runner 自行開檔核對+實測(未經三人面板投票)

  • 範圍 8 支逐支通讀;卡片「重點看什麼」五條+「有人守了一半」八種樣式逐條答完。
  • 跨讀了範圍外的相關程式(只讀不報):infra/support/ 全部(遮罩、打包器、日誌切片、資料庫讀取、三支收集器)、scripts/installer/guidant 的 diag 子命令、install.sh 的設定檔範本、出貨版 compose、身分套件的平台管理員判定、werkzeug 的 send_file。
  • 實測:①四支遮罩函式 × 30 多種寫法;②安裝程式設定檔範本 35 個欄位逐欄過遮罩;③本機真的產一包診斷包、解開檢查;④時間範圍四種組合;⑤132MB 日誌切片的記憶體峰值。
  • DEV 資料庫唯讀查證(2026-09-25 約 16:05 +08,唯讀連線、只下 SELECT):system_logs 與 api_logs 最近 30 天內,符合各種機密寫法的筆數與內容;最近 200 筆 ERROR 裡含堆疊與路徑的筆數。
  • 打折處:
    • 本機應用起不來(平行線的無關錯誤),真包走的是降級路徑,沒有資料庫紀錄那半;資料庫紀錄的遮罩結論來自直接呼叫函式,不是從真包裡看到的。
    • 畫面版下載沒有對正在運行的服務實測(本機 BE 未啟動),守門結論來自讀碼。
    • DEV 資料只證明「DEV 今天沒有真密碼進包」,不代表客戶現場。
§8

執行概況

項目 值
掃描目標 BE repo compliance-manager-be,branch feature/review
版本 93c57739b(工作區有平行線未 commit 改動,範圍 8 支檔案本身無改動)
工具 Claude Code 官方 claude-security plugin 0.11.0
參數 mode scan/effort low/focus attack-surface/scope 8 檔
run ID wf_c03e5bb6-44b
報告目錄 CLAUDE-SECURITY-20260925-074607/(不入版控)
耗時 約 37 分鐘(2,220 秒)
研究員 2 派出/2 交回(全範圍 1+密碼金鑰專掃 1),failed 0
候選/面板票 1/3(0:3 否決)
驗證章 verified
runner 自行發現 4 條(中 1、低 2、資訊 1),皆未經面板