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:3import 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.pysubprocess.run(cmd, ...)
  • 查證
    • cmdlist 形式、無 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-199runner.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=/hostabc;rm -rf /a`id`a$(id)../../etc/passwd、空字串、含引號/換行者,以及非字串型別。
  • 修後仍報 → 一併標記:此規則屬 audit 類,只要有外部值流入 subprocess 就報,看不到上游驗證。故在 subprocess.runcmd 引數行加 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-167render_template 之前對 report.summarybleach.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:168weasyprint.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/javascriptp/typescriptp/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: 0skipped_rules: 0
  • 我的 BE 重掃亦為 errors: 0skipped_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 需重新評估