# semgrep `p/security-audit` 預演 triage 紀錄（Checkmarx 送測前置）

**日期**：2026-07-25
**案號**：CM-903
**目的**：為 Checkmarx SAST 送測預演，先用 semgrep 的 audit 級 ruleset 自掃，把會被 SAST 報出來的項目**先定性、先備答辯材料**，避免送測當天才逐條解釋。

> 原始 JSON 依 `docs/security-reports/README.md` 規則存本機當日資料夾（不入版控）：
> `docs/security-reports/2026-07-25/semgrep-security-audit-preview-{backend-baseline,backend-final,frontend}.json`
> 本文件是**入版控的結論**（README 規則 3：「數字與判定要能離開本機存活」）。

---

## 1. 掃描參數

| 項目 | 值 |
|---|---|
| 工具 | semgrep 1.171.0（Community / OSS engine） |
| ruleset | `p/security-audit` + `p/python` |
| 指令 | `semgrep --config p/security-audit --config p/python --metrics=off --json -o <out> --timeout 120 .` |
| 範圍 | repo 根目錄（`--scan was limited to files tracked by git`） |
| BE 掃描規模 | 1144 條規則 × 5341 個 target |
| FE 掃描規模 | 40 條規則 × 1401 個 target |

註：本次刻意用 repo 根目錄全掃（非 `scripts/sonar_scan.sh` 的白名單制），因為送測時 Checkmarx 也是吃整包原始碼。

## 2. 結果總覽

| Repo | 修前 | 修後 |
|---|---|---|
| BE | **3 條**（ERROR 2 + WARNING 1） | **0 條** |
| FE | **0 條** | 0 條（未動任何 FE 檔） |

BE 三條的處置：真修 1 條、標記誤報 2 條（皆附中文說明與本文件連結）。

## 3. BE 三條逐條定性

### ① `python.lang.security.use-defused-xml` — ERROR → **誤報，標記**

- **位置**：`app/flow_engine/util/bpmn_generator.py:3`（`import xml.etree.ElementTree as ET`）
- **查證**（本 case 獨立複驗，非只採信既有註解）：
  - `grep` 全檔確認**零個** `ET.parse` / `ET.fromstring` / `ET.XMLParser` 呼叫 → 原生 ET 未用於任何解析路徑
  - 三個實際解析點（L1539 / L1561 / L1980）全部走 `safe_parse`，即 `defusedxml.ElementTree.parse`
  - 原生 ET 只用於**產生端**（`Element` / `SubElement` / `tostring` / `register_namespace`）——產生 XML 不讀外部實體，無 XXE 面
- **結論**：規則只看 import 行、看不到「解析走 defusedxml、產生走原生」的分工，屬誤報。檔案 L4-6 早有 A2 硬化註解說明此分工。
- **處置**：L3 行尾加 `# nosemgrep: python.lang.security.use-defused-xml.use-defused-xml`。既有 defusedxml 硬化**未動**。

### ② `dangerous-subprocess-use-tainted-env-args` — ERROR → **低風險真實項，已硬化**

- **位置**：`infra/evidence_classification/classifier_container_runner.py`（`subprocess.run(cmd, ...)`）
- **查證**：
  - `cmd` 是 **list 形式、無 `shell=True`** → **沒有 shell 命令注入面**（規則訊息建議的 `shlex.quote()` 在此不適用）
  - 但確認存在外部可控路徑：`api/evidence_classification/routes/evidence_classification_route.py:62`
    `evidence_folder_id_override = body.get("evidence_folder_id")` → 一路傳到
    `service:198-199` → `runner.run(evidence_folder_id=...)` → 進 docker argv。
    `framework_id` 同樣來自 HTTP body（`route:52`）
  - **真實風險**：值若以 `-` 開頭，docker CLI 會當成 flag 解析（例 `--privileged`、`-v /:/host`），
    即「docker flag 注入」而非命令注入。今天未爆，但面是真的
  - `model` 已有 `ALLOWED_MODELS` 白名單（`service:181`），`tenant_id` / `min_confidence` /
    `workers` / `limit` 是數值型經 `str()` 包過 → 無此面
- **處置（真修）**：在 runner 組 cmd **之前**加字元集白名單驗證
  `_SAFE_ARG_RE = ^[A-Za-z0-9_][A-Za-z0-9_.-]*$`，套用於 `evidence_folder_id` /
  `framework_id` / `model`（model 為 defense-in-depth 再驗一次）。
  不合法即 raise 既有的 `ContainerRunError`（domain 層例外，未自創新型別）。
  刻意「寧可嚴不可鬆」：**不允許開頭為 `-` 或 `.`**。
- **驗證**：8 個合法值（真實 Drive folder ID / shared drive ID / `cmmc-l1` / `iso27001` /
  各 model id）全通過；21 個攻擊/異常值全被拒，含 `--privileged`、`-v`、`--rm`、
  `-e ANTHROPIC_API_KEY=leak`、`--mount=type=bind,src=/,dst=/host`、`abc;rm -rf /`、
  `` a`id` ``、`a$(id)`、`../../etc/passwd`、空字串、含引號/換行者，以及非字串型別。
- **修後仍報 → 一併標記**：此規則屬 **audit 類**，只要有外部值流入 `subprocess`
  就報，看不到上游驗證。故在 `subprocess.run` 的 `cmd` 引數行加 nosemgrep 並附說明。
  ⚠️ 踩到既有已知坑（CM-889 handoff §2-3 記載）：多行呼叫的 finding **錨在引數行**
  （`cmd, capture_output=...`）而非 `subprocess.run(` 起始行，標錯行不生效——
  第一次標在起始行時複掃仍報，移到引數行才消。

### ③ `template-unescaped-with-safe` — WARNING → **誤報級，不改防護邏輯**

- **位置**：`api/project_summary_report/templates/project_summary_report_template.html:44`
  （`{{ report.summary|safe }}`）
- **查證**（雙重防護，皆實際開檔確認）：
  1. **render 前已消毒**：`project_summary_report_route.py:160-167` 在 `render_template`
     之前對 `report.summary` 跑 `bleach.clean(tags=白名單, attributes=..., protocols=..., strip=True)`。
     白名單（L33-41）只含排版標籤（p/br/b/strong/i/em/u/s/ul/ol/li/h1-h6/a/img/span/div/
     table 家族/blockquote/pre/code），**無 `script`、無 `on*` 事件屬性**。
     另有 CM-892 的 `img src` 限縮（只收 `data:image/*`，擋 WeasyPrint 伺服器端外抓 SSRF）
  2. **輸出不進瀏覽器**：`grep` 確認該 template 全 repo **唯一** consumer 是
     `route:168` → `weasyprint.HTML(string=html).write_pdf()` → `send_file` 下載 PDF。
     沒有任何路徑把它 render 給瀏覽器，**無 script 執行環境**
- **處置**：不改任何防護邏輯（case 紅線）。加 Jinja 註解形式的
  `{# nosemgrep: ... #}` + 中文說明。
  **實測此形式有效**（複掃後該條消失）——case 原本預期「HTML 檔標記可能無效」，
  實際上 semgrep 是把它當 Jinja/Python template 解析，`{# #}` 註解生效。

## 4. ⚠️ FE「0 條」的限制（送測時會被重新檢視，如實記載）

FE 掃描結果是 **0 findings**，但**不等於「FE 通過所有檢查」**，有兩點必須揭露：

1. **規則數落差極大**：FE 只跑了 **40 條**規則（BE 是 1144 條）。原因是 ruleset 依語言
   分派，`p/security-audit` + `p/python` 對 Vue/JS/TS 專案能匹配的規則本就少。
   **要有意義地掃 FE，需另外指定 JS/TS 導向的 ruleset**（如 `p/javascript`、
   `p/typescript`、`p/xss`）——本次預演未做，屬已知缺口。
2. **有 2 個檔案 parse 失敗**（`errors` 陣列，level=warn，type=`PartialParsing`）：
   - `figma/src/app/components/auditor/EvidenceAnalytics.tsx:238`
   - `figma/src/app/pages/ProjectCreate.tsx:401` / `:428`
   （皆為 `& 中文` / `& Confirm` 的語法解析失敗）
   這兩個檔在 `figma/` 目錄下（設計稿轉出的參考碼，非上線程式），但
   **parse 失敗代表那些行沒被任何規則檢查過**，「0 條」在這兩檔上不成立。

### 與 case 描述的落差（如實記錄）

case 頁原文寫「**171 條規則執行錯誤（3 個規則需 Pro engine——弱加密偵測/CORS 設定——在 77 檔上未執行）**」。

本 case 動工時**無法重現此數據**：
- 主 session 留下的基線 JSON（`/private/tmp/semgrep-preview-2607/be-security-audit.json`）
  本身即 `errors: 0`、`skipped_rules: 0`
- 我的 BE 重掃亦為 `errors: 0`、`skipped_rules: 0`
- FE 重掃的 errors 是上述 **2 條 PartialParsing**，與「171 條 / 3 規則需 Pro / 77 檔」不符

推測原因：該數字可能來自另一次不同參數的掃描（例如 `--config auto`，會拉更多需
Pro engine 的規則），或是 FE/BE 混記。**本文件記錄我實際量到的值**；
若「171 條」有其他來源，請補充來源指令後併入。

**Pro engine 限制本身仍然成立且需揭露**：本次全程使用 Community/OSS engine，
semgrep 的跨檔案資料流分析（interfile taint）與部分規則需 Pro 授權。
Checkmarx 具備跨檔案分析能力，**可能報出本次掃不到的跨檔 taint 路徑**。

## 5. 送測答辯要點速查

| Checkmarx 可能報的 | 我方立場 |
|---|---|
| XXE（`xml.etree` import） | 解析端全走 defusedxml（3 處），原生 ET 只用於產生 XML。見 §3① |
| 命令注入（`subprocess.run`） | list 形式無 `shell=True`，無 shell 注入面；docker flag 注入面已加字元集白名單擋掉（拒 `-` 開頭）。見 §3② |
| XSS（`\|safe`） | render 前 bleach 白名單消毒（無 script / 無 on* 事件），且輸出只進 WeasyPrint 轉 PDF、不進瀏覽器。見 §3③ |
| 硬編碼 IP | 內網 dev IP / 虛構 E2E 測試資料，已於 SonarQube 標 accept（CM-900） |

## 6. 未來反悔 / 重驗條件

- **FE 補掃 JS/TS ruleset**（§4-1 的已知缺口）→ 建議另開 case
- `figma/` 兩檔的 parse error 若要收乾淨，需修那兩行語法或把 `figma/` 加入
  `.semgrepignore`（該目錄非上線碼，後者較合理）
- 若 `evidence_folder_id` 之外**新增**任何進 docker argv 的字串參數，
  必須一併過 `_validate_cmd_arg`（規則寫在 runner 檔內註解）
- 若 `project_summary_report_template.html` 未來被 render 給瀏覽器（不只轉 PDF），
  §3③ 的「無 script 執行環境」前提即失效，`|safe` 需重新評估
