檢查日期 2026-09-25|對應卡片 CM-2162(母卡 CM-2152)|檢查範圍 8 個檔案 1,295 行|工具 run ID
wf_c03e5bb6-44b
下載入口只有兩條,都守得住。但診斷包的「遮罩」(把密碼換成 *** 的那道手續)認得的密碼寫法太少,而且三種收集物用的遮罩強度不一樣。 實測有十幾種常見寫法的密碼會原樣進包。
掃描工具提出 1 個候選,三人面板 0:3 否決,正式清單零條。我照卡片逐支開檔,也真的在本機組了一包診斷包打開檢查,另外找到四件事:
Authorization: Bearer … 這種最典型的登入權杖寫法都不遮。四條路徑都不認得 password=值、網址裡的帳密、export 名稱=值。_audit_export」,但這支函式根本不存在。⚠️ 給首腦:派工提到「總表第 173 項也是診斷包遮罩」,但總表第 173 項實際是「SSP Word 檔卡死解析」。 診斷包遮罩的既有項目是第 162 項(AI 金鑰以 KEY=值 形狀進日誌,診斷遮罩不認得這種寫法)。
「系統診斷包」是客戶系統出問題時,平台管理員把整台機器的現場(設定、日誌、資料庫紀錄、錯誤事件)打成一個 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 不必重報。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度+為什麼 | 來源 |
|---|---|---|---|---|---|---|
| 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 否決,見下節)。
工具派了兩位研究員:一位讀全部範圍,一位專找寫死在程式裡的密碼金鑰。後者交回空清單。前者提出一個候選:
db/api_logs.csv。原廠把包貼進 AI 對話時,AI 可能把這段文字當成指令。infra/support/diag_masking.py:3-4,資料庫欄位說明也是專為 AI 寫的)。原廠分析診斷包用的 AI 若有執行指令的能力,應該把包內容當不可信資料看待。這是原廠內部作業規範的事,不是這個 repo 要修的。每一類收集物打包前走哪支遮罩(逐支開檔確認):
| 收集物 | 包內檔名 | 走哪支遮罩 | 強度 |
|---|---|---|---|
| 設定檔(指令版讀檔/畫面版讀當下環境變數) | 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,這裡只給方向):
mask_log_lines 的單行版),讓四條路徑真的共用同一套判準。這一步改動最小、效果最大。名稱=值(第 162 項修法欄已寫「診斷遮罩補認 KEY=值」,同一件事)、單獨出現的 Bearer 值、網址參數、帳號:密碼@ 形狀的連線字串、單引號字典。export 開頭、縮寫名稱(PASS、PWD)、值裡含 ://帳號:密碼@ 的一律遮。畫面版不要用 名稱=值 串環境變數,改成逐個判斷。diag_bundle_app_service.py:115-120 收齊之後、交給打包器之前):拿已知的機密值(程式啟動時讀到的資料庫密碼、權杖簽章金鑰等)逐檔搜一遍,搜到就遮,並在 manifest.json 記一筆。這是「不管寫法、只管值」的保底,能擋住所有目前沒想到的寫法。since/until 有沒有上限?——✅ 畫面版有,不成立;指令版刻意不設diag_bundle_export_service.py:53),超過就回 400 擋下,不會偷偷縮短範圍(:122-123)。起點比終點晚也擋。--since 沒有上限,而且有 --no-limit 可以關掉 100MB 包大小上限(diag_cli.py:88-91)。這是給到場工程師用的,要先有主機 root 權限,不算漏洞。infra/support/diag_log_reader.py:57)、資料庫每張表 20 萬筆(infra/support/diag_db_reader.py:45)、容器紀錄每支 5,000 行、整包 100MB。since(帶時區)、不送 until 時,until 由伺服器補成「現在」(沒帶時區),兩個時間比大小時 Python 直接拋錯(TypeError: can't compare offset-naive and offset-aware datetimes),而且這一行在 try 外面,會冒成 500。兩個都送或都不送就正常。打包核心那層有處理混合時區(diag_bundle_app_service.py:53-66),但匯出服務在呼叫核心之前就先比了,沒走那道處理。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 一起修。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(權限檢查共用層)的範圍,本棒不重報。
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。| 樣式 | 結論 |
|---|---|
| 只驗身分、不驗資料歸屬 | 不適用:診斷包是整台機器的現場,不屬於任何客戶,平台管理員本來就該看全部 |
| 列表有查、單筆沒查 | 不適用:沒有單筆存取 |
| 守門寫在網址層,第二支路由沒掛 | 不成立:守門在服務層,兩支網址都經過 |
守門條件用 or 串起來 |
不成立:單一條件 |
| 「查不到」和「沒權限」混在一起 | 不適用 |
| 背景或系統身分執行時,假設呼叫者一定是自己人 | 指令版以系統身分執行,但入口是主機 root,本來就是自己人。成立但合理 |
| 防重放、計次只在單一進程內有效 | 不適用:沒有計次 |
| 註解說這裡刻意不檢查,上一層卻沒檢查 | 路由註解說「守門刻意不放在這裡」,服務層確實有檢查 ✅。但另一句註解說「成敗另記一筆結果,見 _audit_export」,這支函式不存在 → U10-2 |
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(日誌規則)*** 的方式是「比對長相」,而它認得的長相太少。四條收集路徑裡,有兩條用的是最窄的版本。manifest.json 的遮罩清單只列出「有被遮到的」,讀包的人會誤以為包是乾淨的。名稱=值 進日誌,本棒實測確認兩條路徑都沒遮)。不需要攻擊。名稱=值)。本條新增的是:資料庫紀錄與「最近錯誤」根本沒走日誌遮罩、單獨出現的 Bearer、網址帶帳密、export、多行值、manifest 遮罩清單會誤導讀包的人。建議首腦判斷要併進第 162 項,還是另開一條。env/guidant.env.masked、且沒列進 manifest.json。DEV 資料庫唯讀查證,目前沒有真密碼符合這些寫法。app/support/service/diag_bundle_export_service.py:90-99(產包成功與失敗兩個分支都沒有寫稽核);說明文字 :26-30_audit_export」,但全 repo 搜不到 _audit_export,程式裡也沒有第二筆稽核。:101-107),但那不是稽核事件,會隨日誌輪替被清掉。app/support/service/diag_bundle_export_service.py:120-122+08:00),伺服器補的「現在」不帶時區,Python 不允許這兩種時間比大小,直接拋錯。這一行在 try 外面,所以錯誤冒到全站處理、回 500。since、不帶 until(前端正常是兩個都帶或都不帶,所以一般使用不會遇到)。_resolve_window:只帶 since(有時區)→ TypeError;只帶 until、兩個都帶、since 沒時區 → 正常。_as_naive_local(diag_bundle_app_service.py:53-66)把兩個時間統一格式,或者在補「現在」時帶上時區。app/support/service/diag_bundle_export_service.py:91(同步產包);infra/support/diag_log_reader.py:128-150(先全部讀完才截)第一層:工具正式報告(經三人面板驗證)
verified(stamp CLAUDE-SECURITY-REVISION-93c57739b77a-dirty.json)failed 0、續跑 0、沒有遺失的候選。第二層:runner 自行開檔核對+實測(未經三人面板投票)
infra/support/ 全部(遮罩、打包器、日誌切片、資料庫讀取、三支收集器)、scripts/installer/guidant 的 diag 子命令、install.sh 的設定檔範本、出貨版 compose、身分套件的平台管理員判定、werkzeug 的 send_file。SELECT):system_logs 與 api_logs 最近 30 天內,符合各種機密寫法的筆數與內容;最近 200 筆 ERROR 裡含堆疊與路徑的筆數。| 項目 | 值 |
|---|---|
| 掃描目標 | 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),皆未經面板 |