---
title: U11 檢查結果：系統診斷包的資料收集器
---

# U11 檢查結果：系統診斷包的資料收集器

> 檢查日期 2026-09-25｜對應卡片 CM-2163（母卡 CM-2152）｜檢查範圍 7 個檔案 1,548 行｜工具 run ID `wf_408b5225-5ee`

## 🔴 一句話結論

**收集器取主機資料的 `docker` 指令都是寫死的，資料庫讀取也只看兩張紀錄表，這兩件沒問題。問題出在「收進包之前遮得乾不乾淨」和「指令版在主機上用的暫存資料夾」。**

工具正式清單 2 條，三人面板都是 3:0 通過，兩條都是中：

1. **（中）寄信失敗時，寄信伺服器的密碼會以 `password='明文'` 的寫法進日誌，診斷包原樣帶走。** 遮罩只認 JSON 的 `"password":"值"` 和 `password: 值` 兩種寫法，不認這一種。我實測確認兩支遮罩都漏。
2. **（中）指令版 `sudo guidant diag` 在「容器使用者寫得動的資料夾」裡，用一個猜得到的名字建暫存資料夾，而且不檢查那是不是捷徑（符號連結）。** 已經在容器裡拿到一般權限的人可以預先放捷徑，讓主機的最高權限帳號把檔案寫到別的地方去。我在自己的暫存資料夾裡照同樣手法重現成功。

我照卡片逐支開檔，另外找到 3 條（未經三人面板投票）：

3. **（中）畫面版的設定快照遇到跨多行的值，只遮第一行。** 資料庫密鑰 JSON 的第二行、私鑰檔的內容會原樣進包。
4. **（低）預蒐模式讀檔時，完全照索引檔寫的路徑去讀，沒限制在暫存資料夾裡。** 這一條是第 2 條在容器內的那一半。工具把它併進第 2 條，這裡單獨列出來，是因為它要改的是另一支檔。
5. **（低）存取紀錄的「網址」欄完全沒過遮罩。** 網址裡帶權杖參數（`?access_token=…`）時會原樣進包。DEV 查到 1 筆，是測試資料。

**不重報的**：容器紀錄完全沒過遮罩（總表第 173 項已登記，即 U2-2）。這一棒順帶確認，指令版的「預蒐模式」同樣沒過遮罩，兩條路是同一個洞，修的時候要一起修（見下方疑點 ①）。

## 這一棒在檢查什麼

「系統診斷包」是客戶系統出問題時，把整台機器的現場打成一個壓縮檔寄給原廠分析用的。這個包**不加密**，會經過客戶的電腦、郵件或隨身碟、原廠的電腦，最後被貼進 AI 對話。所以包裡的密碼有沒有遮乾淨，就是這個功能唯一的防線。

U10 查的是「誰能按下產包」和「組裝流程」，這一棒 U11 查的是**真正去拿資料的那七支程式**：它們去哪裡拿、拿的時候怎麼組指令、拿到後有沒有遮。

| 檔案 | 行數 | 做什麼 |
|------|-----:|------|
| `infra/support/collector/host_collector.py` | 201 | 開發機／原生部署用：直接跑 `docker ps`、`docker logs`、`df`，讀設定檔 |
| `infra/support/collector/staged_collector.py` | 178 | 出貨主機用：主機端的腳本先把資料存進暫存資料夾，這支再讀進來 |
| `infra/support/collector/container_collector.py` | 105 | 畫面版用：在容器裡面，拿不到 docker 就誠實寫「取不到」，設定改讀當下的環境變數 |
| `infra/support/diag_db_reader.py` | 242 | 讀資料庫的存取紀錄、錯誤紀錄、改版水位、各表筆數 |
| `infra/support/diag_log_reader.py` | 176 | 依時間範圍切應用日誌檔 |
| `infra/support/diag_event_fetcher.py` | 303 | 從錯誤收集站撈錯誤清單 |
| `infra/support/diag_packer.py` | 343 | 全部打成 tar.gz，超過大小就從最舊的開始砍 |

## 掃到什麼（總覽）

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度＋為什麼 | 來源 |
|---|---|---|---|---|---|---|
| U11-1 | 遮罩不認得 `password='值'` 這種寫法，而寄信套件寄信失敗時就是這樣把整組寄信設定寫進日誌的 | 客戶的寄信伺服器帳號密碼跟著診斷包寄到原廠、貼進 AI 對話；包裡的說明檔還寫著「密碼都已遮罩」 | 客戶有設寄信帳密，時間範圍內寄信失敗過一次（封閉網路很常見），然後有人產診斷包。**不需要攻擊** | 遮罩規則 `infra/support/diag_masking.py:164-169`（補認 `名稱=值`、`名稱='值'`）；資料庫紀錄改走完整日誌遮罩 `infra/support/diag_db_reader.py:237`；源頭在寄信套件 `jedi_notification/infra/smtp_mail/smtp_mail_adapter.py:52`、`:57` | 🟠 中：正常維運就會發生，但外洩對象是拿到包的人，不是任意網路使用者 | 工具（面板 3:0） |
| U11-2 | 指令版在容器寫得動的資料夾裡，用猜得到的名字建暫存資料夾，不檢查是不是捷徑 | 已經在容器裡拿到一般權限的人，可以讓主機最高權限帳號改掉主機上任意檔案的權限，或把內容寫進去 | 先在容器裡拿到一般使用者的執行權（要另一個漏洞），而且主機管理員之後跑了 `sudo guidant diag` | `scripts/installer/guidant:700-706`（改用 `mktemp -d` 建在只有 root 能寫的地方；或先檢查路徑不存在、不是捷徑） | 🟠 中：後果是從容器跨到主機，但要先有另一個漏洞，還要等管理員跑診斷 | 工具（面板 3:0） |
| U11-3 | 畫面版的設定快照遇到跨多行的值，只遮第一行 | 資料庫密鑰 JSON 的第二行、私鑰檔的內容（如果環境變數直接放內容而不是路徑）原樣進包 | 平台管理員從畫面產包，而且當下的環境變數裡有跨多行的機密值 | `infra/support/collector/container_collector.py:94`（不要把環境變數串成 `名稱=值` 的文字再遮，改成逐個判斷名稱） | 🟠 中：正常產包就會發生，前提是有多行機密值；出貨設定檔裡的 `DB_SECRET` 是單行，所以今天的落地版沒中 | runner 實測（未經三人面板） |
| U11-4 | 預蒐模式讀檔時，完全照索引檔寫的路徑去讀，沒限制在暫存資料夾裡 | 能改索引檔的人，可以讓容器內用最高權限跑的打包程式把容器裡任何檔案讀進包 | 要能寫暫存資料夾，而這其實就是 U11-2 的前提 | `infra/support/collector/staged_collector.py:90`（取實際路徑後，確認還在 `staging_dir` 底下）；`:140`（服務名稱也要驗，不然包內檔名會出現 `../`） | 🟢 低：前提跟 U11-2 相同，後果比它小 | runner 實測（未經三人面板；工具在 U11-2 裡提到這一半） |
| U11-5 | 存取紀錄的「網址」欄完全不遮 | 網址裡帶的權杖參數原樣進包 | 有人用網址參數傳權杖（本產品的正常端點不這樣做），加上有人產包 | `domain/support/entity/diag_bundle.py:30`（`MASKED_API_LOG_COLUMNS` 補上 `url`）；遮罩也要認得網址參數 | 🟢 低：產品本身不用網址傳權杖，DEV 只有 1 筆測試資料 | runner 開檔＋DEV 查證（未經三人面板） |

> **現況（2026-10-01）**：本棒各條後來的處理結果如下（過程紀錄保留，不改）。
>
> - U11-1（遮罩不認 `password='值'`）＝總表第 207 項，✅ 已修（寄信套件 CM-2111；遮罩規則 CM-2212，1.21.0 出貨）。
> - U11-2（診斷指令暫存資料夾）＝總表第 208 項，✅ 已修（CM-2214，commit `d39181f3b`／`046553756`，1.21.0 出貨）。
> - U11-3（設定快照多行只遮第一行）＝總表第 209 項，✅ 已修（CM-2212，1.21.0 出貨）。
> - U11-4（預蒐模式路徑未限制）＝總表第 210 項，✅ 已修（CM-2214，1.21.0 出貨）。
> - U11-5（網址欄不遮）＝總表第 211 項，✅ 已修（CM-2212，1.21.0 出貨）。

## 工具報的逐條

工具派了一位研究員讀整個範圍，另一位專找寫死在程式裡的密碼金鑰。第一位研究員跑了約 90 分鐘被中斷，工具自動重派一次，這次跑完。兩個候選都送進三人面板，6 票全投完，兩條都是 3:0 通過。

### U11-1（工具 F1）— 寄信密碼以 `password='值'` 寫法進日誌，遮罩漏掉（中）

- **嚴重度**：🟠 中（面板三票都判中）
- **該補檢查的位置**：
  - 遮罩規則：`infra/support/diag_masking.py:164-169`（日誌那一道），要補認 `名稱=值` 和 `名稱='值'`
  - 資料庫紀錄只走最弱的那道：`infra/support/diag_db_reader.py:237`
  - 源頭（套件，範圍外）：`jedi_notification/infra/smtp_mail/smtp_mail_adapter.py:52`、`:57`
- **白話說明**：寄信套件寄信失敗時，會把「寄信設定」整包印進錯誤日誌：`Error sending email smtp info: … username='u' password='明文' …`。這個寫法是 Python 印物件的預設格式。診斷包的遮罩只認兩種寫法：JSON 的 `"password":"值"`，和日誌裡的 `password: 值`，**不認 `password='值'`**。所以這行會原樣進 `logs/app.log.slice`，也會進 `db/system_logs.csv`。
- **我自己核對**（開檔＋實測）：
  - 開檔確認寄信套件 `smtp_mail_adapter.py:52`、`:57` 真的是 `logger.error(f"... smtp info: {self.config}")`。設定物件的 `password` 是一般字串（`smtp_email_config_dto.py:13`），不是會自動隱藏的密碼型別。
  - 主專案 `app/notification/service/notification_service.py:67` 把資料庫裡存的寄信密碼放進這個欄位。
  - 用真的設定物件造一行假日誌丟進兩支遮罩：`mask_json_like` 和 `mask_log_lines` **都原樣留下**假密碼。
  - DEV 唯讀查證（2026-09-25 18:10 +08）：`system_logs` 與本機 `log/app.log*` 目前都沒有 `smtp info` 這一行，因為 DEV 沒有寄信失敗過。**所以這只證明「一失敗就會外洩」，不代表 DEV 已經外洩。**
- **與既有項目的關係**：
  - **CM-1606**（FR-078 N1）已經登記「寄信套件把密碼寫進日誌」，那是**源頭**。
  - 這一條是**下游**：就算源頭沒修，診斷包也不該把它帶出去。
  - **U10-1** 已經報過「遮罩認得的寫法太少、資料庫紀錄走最弱那支」，並列了 `名稱=值` 的缺口。這一條是那個缺口的**第一個確認的真實來源**。建議首腦判斷要併進 U10-1（與第 162 項一起），還是單獨一條。
- **建議修法**：遮罩補認 `名稱=值` 和 `名稱='值'`（鍵名清單沿用同一份）；資料庫紀錄改走 `mask_log_lines`。寄信套件那半（不要印整個設定，或把密碼改成隱藏型別）照外部套件異動規範先提醒、決策者點頭才動。

### U11-2（工具 F2）— 指令版在容器寫得動的資料夾裡建暫存資料夾，跟著捷徑走（中）

- **嚴重度**：🟠 中（面板三票都判中）
- **該補檢查的位置**：`scripts/installer/guidant:700-706`（範圍外，但資料是流進本範圍的 `staged_collector.py`）
- **白話說明**：
  - 出貨主機上的 `sudo guidant diag` 會以主機最高權限（root）建一個暫存資料夾，路徑是 `/srv/guidant-ai/home/.diag-stage-<程序編號>`，然後往裡面寫檔、改權限。
  - 問題是 `home` 這個資料夾**同時掛進容器、而且屬於容器裡的一般使用者（uid 1000）**，所以容器裡的程式也寫得動它。
  - 程序編號猜得到。容器裡的人可以預先放一個同名的「捷徑」（符號連結）指到主機別的地方，主機的 root 跑診斷時不檢查就跟著捷徑走，改權限、寫檔都落到那個地方。
- **我自己核對**：
  - 開檔確認 `guidant:700` 路徑的組法、`:705-706` 的 `mkdir -p` 和 `chmod 700`、之後的 `>` 寫檔與 `cp`，一路都沒檢查是不是捷徑。
  - 出貨 compose `docker/production/docker-compose.yml:152` 把 `home` 掛進容器；`install.sh:1319` 和 entrypoint 把它設成 uid 1000 所有。
  - 在自己的暫存資料夾照同樣手法重現：先放捷徑，再用腳本同樣的 `mkdir -p`、`chmod`、`>` 寫檔 → **權限改到捷徑指的資料夾，檔案內容也寫進捷徑指的檔案**。
  - **打折處**：沒有在真的出貨主機上跑，是照腳本的指令在本機資料夾重現。
- **前提與為什麼是中**：攻擊者要**先**在容器裡拿到一般使用者的執行權，這要靠另一個漏洞；還要等主機管理員跑診斷。但得手後是從容器跨到主機，這正是容器隔離要擋的事。
- **建議修法**：暫存資料夾改用 `mktemp -d` 建在只有 root 能寫的地方，再用唯讀方式掛給容器；至少先檢查路徑不存在、也不是捷徑。容器內的打包程式改用 uid 1000 跑（`docker exec -u 1000`），除非真的需要 root。

## 卡片點名的疑點逐條回答

### ① 每一類收集物在打包前有沒有過遮罩？——⚠️ 大部分有，兩類沒有 → 成立（U11-1、U11-3、U11-5，另有第 173 項）

逐支開檔核對的結果：

| 收集物 | 從哪支來 | 打包前的遮罩 | 結論 |
|---|---|---|---|
| 容器狀態（`docker ps`）、資源用量、磁碟 | 三支收集器 | 沒有 | 不需要：內容是容器名稱、狀態、用量，不含機密 |
| 設定檔（指令版） | `host_collector.py:197`、`staged_collector.py:174` | `mask_env_text` | 有遮，但規則窄（見 U10-1） |
| 設定快照（畫面版） | `container_collector.py:94-95` | `mask_env_text` | 有遮，但**跨多行的值只遮第一行 → U11-3** |
| **容器紀錄（開發機直接收）** | `host_collector.py:136-156` | **沒有** | 總表第 173 項（U2-2），不重報 |
| **容器紀錄（出貨主機預蒐）** | `staged_collector.py:128-153` | **沒有** | 同一個洞的另一條路。實測：假權杖三種寫法全部原樣留下、遮罩清單空白。**修第 173 項時這支要一起改** |
| 應用日誌 | `diag_log_reader.py:152` | `mask_log_lines` | 有遮，但不認 `名稱=值`、`名稱='值'` → U11-1 |
| 存取紀錄、錯誤紀錄（資料庫） | `diag_db_reader.py:237` | `mask_json_like`（最弱） | 只遮四個欄位；`url` 欄沒遮 → U11-5 |
| 錯誤收集站事件 | `diag_event_fetcher.py:275-287` | `mask_structure` | 有遮；欄位名稱像密碼的整個換掉 |
| 各表筆數、改版水位、分區 | `diag_db_reader.py:175-222` | 不需要 | 只有數字與版號 |

### ② 遮罩規則認不認得 `KEY=值`／`Bearer`／連線字串？——❌ 認得的不夠 → 成立（併入 U10-1，新增一個真實來源 U11-1）

我寫了一支小程式，拿假密碼逐一丟進三支遮罩。跟 U10 的實測結果一致，另外補了三種：

| 寫法 | 設定檔遮罩 | 日誌遮罩 | 資料庫紀錄遮罩 |
|---|:-:|:-:|:-:|
| `password='值'`（寄信套件的真實寫法） | — | ❌ | ❌ |
| `Authorization: Bearer …` | — | ✅ | ❌ |
| 行中間的 `DB_PASSWORD=值` | — | ❌ | ❌ |
| `postgresql://帳號:密碼@主機`、`redis://:密碼@主機` | ❌ | ❌ | ❌ |
| `SMTP_PASS=值`、`JWT=值`（名稱不含 password／secret／token／key） | ❌ | — | — |
| `{'password': '值'}`（單引號字典） | — | ❌ | ❌ |
| 跨多行的值（第二行起） | ❌ | — | — |

設定檔的部分：出貨設定檔裡**真的放機密**的欄位（資料庫、Redis、S3、JWT、加密金鑰、AI 金鑰）名稱全部含 password／secret／key，都遮得到，**今天出貨的設定檔本身不會外洩**（U10 已實測，本棒再以 `install.sh` 寫出的欄位名單核對一次）。問題在日誌與資料庫紀錄裡混進來的機密。

### ③ 讀主機檔案、跑 docker 指令時，路徑與參數是寫死的還是拼進來的？——✅ 指令寫死；⚠️ 預蒐讀檔的路徑是拼進來的 → 部分成立（U11-2、U11-4）

- **docker 指令（`host_collector.py`）**：`docker ps`、`docker stats`、`docker logs`、`df`、`du` 全部用清單形式呼叫（不經過 shell），服務名稱來自檔案裡寫死的清單 `KNOWN_SERVICES`，時間參數是程式算出來的時間字串。**沒有任何外部輸入進得了指令。不成立。**
- **設定檔路徑**：`host_collector.py:68` 是 `資料目錄/guidant.env`，資料目錄來自指令參數 `--data-dir`，只有能在主機下指令的人才給得了。不成立。
- **應用日誌**：`diag_log_reader.py:85-100` 只列出日誌資料夾裡、檔名以 `app.log.` 開頭的檔，路徑來自日誌設定。不成立。
- **預蒐模式讀檔（`staged_collector.py:90`）**：`staging_dir / 索引檔裡寫的檔名`，沒檢查是否跑出暫存資料夾。實測：索引檔寫 `../outside/secret.txt`，就把暫存資料夾外面的檔讀進包；服務名稱寫 `../../evil`，包內檔名就變成 `logs/docker-../../evil.log`。→ **成立（U11-4）**，前提是能寫暫存資料夾，也就是 U11-2 的前提。
- **錯誤收集站網址**：`diag_event_fetcher.py:71-93` 來自環境變數，不是使用者輸入。下一頁的網址取自對方回傳的 `Link` 標頭，最多 10 頁、每次 10 秒逾時，而且跳去別的主機時 Python 會把權杖標頭一起帶過去。但要做到這一步得先控制錯誤收集站本身，所以不列。

### ④ 資料庫讀取器用什麼身分讀？會不會把全租戶資料撈進包？——⚠️ 會看全部，但這是設計 → 不成立

- 連線用的是應用程式本身的資料庫帳號（出貨版是 `cm_app`，`install.sh:1386`），每次查詢前設 `SET LOCAL app.is_super_admin = 't'`（`diag_db_reader.py:63`）。
- 查的四樣東西裡，只有兩張紀錄表有內容：`api_logs`、`system_logs`。DEV 唯讀查證（2026-09-25 18:10 +08）這兩張表**都沒有客戶欄、也沒開資料庫隔離**（`relrowsecurity=false`），本來就是整台機器共用的紀錄。另外兩樣是改版水位，和各表筆數（估計值，只有數字）。
- **結論**：會把所有客戶的操作紀錄帶進包，但診斷包的用途就是「整台機器的現場」，而產包的兩個入口已由 U10 確認只有平台管理員和主機 root 用得到。**不成立。**
- 每張表最多 20 萬列（`:45`），查詢的表名與欄名是檔案裡寫死的常數，值都走參數綁定，沒有注入的空間。

### ⑤ 打包的檔名與壓縮大小上限？——✅ 有上限 → 不成立

- 整包預設 100MB（`domain/support/entity/diag_bundle.py:21`），用未壓縮大小的 3 倍當預算先砍，寫完再以實際大小核對，還超過就寫進包裡的說明檔（`diag_packer.py:163-181`）。
- 各層還有自己的上限：日誌切片 40MB、每張表 20 萬列、每個容器 5,000 行、錯誤事件 10 頁×100 筆。
- 包內檔名來自程式裡寫死的字串。只有預蒐模式的服務名稱是從索引檔拼進來的 → 已列在 U11-4。
- 包內每個檔權限 0600（`:203`），不是還原到別人機器時才要擔心的那種路徑。
- 指令版可以用 `--no-limit` 關掉上限，但那要主機 root，不算漏洞。

### ⑥ 「有人守了一半」八種樣式——除 ① 的遮罩外不成立

| 樣式 | 結論 |
|---|---|
| 只驗身分、不驗資料歸屬 | 不適用：收集器不做守門，守門在 U10 的入口，已確認只有平台管理員／主機 root |
| 列表有查、單筆沒查 | 不適用 |
| 守門寫在網址層，第二支路由沒掛 | 不適用：收集器沒有路由 |
| 守門條件用 `or` 串起來 | 不適用 |
| 「查不到」和「沒權限」混在一起 | 不適用 |
| 背景或系統身分執行時，假設呼叫者一定是自己人 | **成立一半**：預蒐模式假設「暫存資料夾裡的東西一定是主機端腳本放的」，所以照索引檔讀檔（U11-4）。但那個資料夾容器使用者也寫得動（U11-2） |
| 防重放、計次只在單一進程內有效 | 不適用 |
| 註解說這裡刻意不檢查，上一層卻沒檢查 | **成立一種變形**：`diag_masking.py:26-28` 寫「四條路徑共用同一份判準，不分岔」，但資料庫紀錄只走最弱那道、容器紀錄一道都沒走（U10-1、第 173 項、U11-1） |

## 逐條細節（runner 自行發現，未經三人面板投票）

### U11-3 — 畫面版設定快照遇到跨多行的值只遮第一行（中）

- **嚴重度**：🟠 中
- **該補檢查的位置**：`infra/support/collector/container_collector.py:94`
- **白話說明**：畫面版在容器裡拿不到設定檔，改讀「當下生效的環境變數」，做法是把每個變數串成 `名稱=值` 一行一個，再用設定檔那支遮罩去遮。遮罩是**一行一行**比對的。如果某個值本身跨好幾行，只有第一行開頭是 `名稱=`，會被遮到；後面幾行看起來只是普通文字，原樣留下。
- **實測**：造一組假環境變數丟進去：
  - 跨兩行的 `DB_SECRET` JSON → 第二行的假密碼**留下**
  - 私鑰內容跨三行的 `AGENT_JWT_PRIVATE_KEY` → 中間那行**留下**
  - `CELERY_BROKER_URL=redis://:假密碼@…` → 名稱不像密碼，整行**留下**
  - `REDIS_SECRET`（單行）、`SENTRY_DSN`、AI 金鑰 → 遮掉

  更麻煩的是，遮罩清單會列出 `DB_SECRET`、`AGENT_JWT_PRIVATE_KEY`「已遮罩」，讀包的人會以為它們遮乾淨了。
- **影響**：今天的落地版不會中。`install.sh` 寫出的 `DB_SECRET`、`REDIS_SECRET` 是單行；`AGENT_*_KEY` 放的是檔案路徑，不是內容。**但只要部署端把機密以多行值或連線網址放進環境變數，就會原樣進包。** 我沒有在 DEV 或出貨設定裡找到這種現況。
- **建議修法**：不要先串成文字再遮。直接逐個看環境變數的名稱，像密碼就整個值換成 `***`；值裡有 `://帳號:密碼@` 的也遮。

### U11-4 — 預蒐模式讀檔不限制在暫存資料夾內（低）

- **嚴重度**：🟢 低
- **該補檢查的位置**：`infra/support/collector/staged_collector.py:90`（讀檔路徑）、`:140`（包內檔名用的服務名稱）
- **白話說明**：指令版的流程是主機端腳本先把資料存進暫存資料夾，再附一份索引檔 `staged.json` 說明每個檔是什麼。容器裡的打包程式照索引檔寫的檔名去讀，但沒有檢查檔名裡有沒有 `../` 這種「往上一層」的寫法。
- **實測**：造一份假索引檔，`file` 寫成 `../outside/secret.txt` → 把暫存資料夾外面的檔讀進包，而且因為被歸類成「容器狀態」，**完全沒過遮罩**；`service` 寫成 `../../evil` → 包內檔名出現 `logs/docker-../../evil.log`。
- **影響**：打包程式在容器裡用 root 執行（`docker exec` 沒指定使用者、映像檔也沒設 `USER`），所以讀得到容器裡任何檔案。
- **前提與為什麼是低**：要能改索引檔，而那個資料夾只有主機 root 和容器使用者寫得動。能做到的人已經符合 U11-2 的前提，U11-2 的後果更大。修 U11-2 時順手改這支即可。
- **建議修法**：取實際路徑後確認還在 `staging_dir` 底下，不在就回「取不到」；服務名稱只允許 `KNOWN_SERVICES` 裡的名字。

### U11-5 — 存取紀錄的「網址」欄不遮（低）

- **嚴重度**：🟢 低
- **該補檢查的位置**：`domain/support/entity/diag_bundle.py:30`（`MASKED_API_LOG_COLUMNS` 只有 `params`、`request`、`response`、`message`）
- **白話說明**：存取紀錄有一欄 `url` 存完整網址（含 `?` 後面的參數，`common/middleware/app_mw.py:188`）。這欄不在遮罩名單裡，就算在，現在的遮罩也不認網址參數的寫法（`?token=值`）。
- **DEV 查證**（2026-09-25 18:10 +08，唯讀）：`api_logs` 約 2 萬筆裡有 895 筆網址帶參數；名稱像權杖／密鑰的只有 1 筆 `?access_token=qs-…`，看起來是測試打的。Google 雲端硬碟回呼帶的 `code=` 參數目前 0 筆。
- **前提與為什麼是低**：本產品正常的 API 不用網址參數傳權杖（登入權杖走標頭），唯一會在網址帶一次性代碼的是 Google 回呼，而那個代碼一用就失效。
- **建議修法**：`url` 加進遮罩欄位名單，遮罩補認網址參數。和 U10-1 一起改。

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

**第一層：工具正式報告（經三人面板驗證）**

- 驗證章：`verified`（stamp `CLAUDE-SECURITY-REVISION-1bafa3bfe2e7-dirty.json`）
- 候選 2、面板 6 票（2 條都是 3:0 通過）、研究員派出 2／交回 2、`failed` 0、續跑 0、沒有遺失的候選。
- 第一位研究員跑約 90 分鐘被中斷，工具自動重派一次（journal 顯示 `research:repository:all` 啟動兩次），`failed` 計數仍是 0。
- 第 2 條的主要位置在範圍外的安裝腳本，是研究員順著資料流追出去的。
- 兩條我都自己開檔核對屬實，也都各自實測過（第 1 條用真的設定物件丟進遮罩；第 2 條在自己的資料夾照同樣指令重現）。

**第二層：runner 自行開檔核對＋實測（未經三人面板投票）**

- 範圍 7 支逐支通讀；卡片「重點看什麼」五條，加上派工要求的四問、「有人守了一半」八種樣式，逐條答完。
- 跨讀的範圍外程式（只讀不報）：`infra/support/diag_masking.py`、`scripts/installer/guidant` 的 `diag` 段、`install.sh` 寫設定檔那段、出貨版 compose、Dockerfile、entrypoint、`app/support/diag_cli.py`、打包核心、寄信套件的 adapter 與設定物件。
- 實測：
  1. 三支遮罩 × 十幾種寫法
  2. 容器收集器的多行值與網址帳密
  3. 預蒐收集器的 `../` 路徑與容器紀錄遮罩
  4. 暫存資料夾的捷徑（在本機暫存區照腳本指令重現）
  5. 寄信設定物件的真實日誌寫法
- DEV 資料庫唯讀查證（2026-09-25 約 16:40 與 18:10 +08，只下 `SELECT`）：兩張紀錄表的隔離設定、`system_logs` 的密碼形狀（10 筆都是堆疊裡印出的程式碼、值是變數名）、`smtp info` 筆數（0）、`api_logs` 網址參數。
- **打折處**：
  - U11-2 沒在真的出貨主機上跑，是在本機照腳本同樣的指令重現。
  - U11-1 在 DEV 沒有實際外洩的紀錄（DEV 沒寄信失敗過），結論來自直接呼叫遮罩函式。
  - 沒有起完整服務產真包。U10 已經產過一包（走降級路徑），本棒沒有重做。

## 執行概況

| 項目 | 值 |
|---|---|
| 掃描目標 | BE repo `compliance-manager-be`，branch `feature/review` |
| 版本 | `1bafa3bfe`（工作區有平行線未 commit 改動，範圍 7 支檔案本身無改動） |
| 工具 | Claude Code 官方 `claude-security` plugin 0.11.0 |
| 參數 | mode `scan`／effort `low`／focus `attack-surface`／scope 7 檔 |
| run ID | `wf_408b5225-5ee` |
| 報告目錄 | `CLAUDE-SECURITY-20260925-082706/`（不入版控） |
| 耗時 | 約 181 分鐘（10,858 秒，含第一位研究員被中斷後重派） |
| 研究員 | 2 派出／2 交回（全範圍 1＋密碼金鑰專掃 1；全範圍那位被中斷後重派 1 次），`failed` 0 |
| 候選／面板票 | 2／6（兩條都是 3:0 通過） |
| 驗證章 | `verified` |
| 工具發現 | 2 條（中 2） |
| runner 自行發現 | 3 條（中 1、低 2），皆未經面板 |
