别把 Semgrep 当高级 grep:用污点规则抓住命令注入,并把结果送进 CI

别把 Semgrep 当高级 grep:用污点规则抓住命令注入,并把结果送进 CI

代码里出现 subprocess.run(),不等于存在命令注入;用户输入经过了三次变量赋值,也不等于已经安全。真正需要回答的是:不可信数据有没有流进危险调用,而且危险调用是否启用了 Shell?

这类问题只靠字符串搜索很难说清。rg "subprocess.run" 能列出调用点,却不知道参数来自常量、白名单还是 HTTP 请求。人工逐条追踪在小仓库里可行,到了多语言代码库就容易漏。今天从《AI Skills 每日精选》的 5 个候选中选择 Trail of Bits 的 semgrep Skill,实战目标不是跑一条"万能扫描命令",而是搭出一条可复核的最小流水线:

  1. 用带正例和反例的测试定义想抓什么;
  2. 用 taint mode 追踪 request.args.get()subprocess.run(..., shell=True)
  3. 同时输出 JSON 与 SARIF;
  4. 让发现漏洞时的退出码能真正挡住 CI;
  5. 修复后再次扫描,证明门禁恢复为绿色。

本文没有向正式 Codex 环境安装任何第三方 Skill。上游内容只读核对自 Trail of Bits 官方仓库semgrep Skill 源文件static-analysis 插件说明。演示只在当前项目的隔离临时目录中运行,代码、请求参数和扫描结果均为合成数据。

为什么选 semgrep

今天的另外 4 个候选是 skill-installerpdftest-driven-developmentgithubsemgrep 最值得深入,是因为它把 Agent 的"安全建议"变成了可以重复执行的工程证据。

官方 Skill 不是简单包装 semgrep scan,而是规定了完整的编排流程:先识别语言并检查 OSS/Pro 能力,再选择扫描模式和精确规则集;向用户展示目标、引擎、模式和全部规则后,必须经过明确批准;扫描任务可以按语言与规则集并行,原始结果和过滤结果分开保存,最后合并为 SARIF。它还要求每条 Semgrep 命令都使用 --metrics=off,并明确反对用 --config auto 代替可审查的规则清单。

这几个约束比"扫出多少条"更重要:扫描目标可能包含私有源码,远程规则集会产生网络访问,SARIF 也可能携带源码片段和绝对路径。未经确认就把整个仓库交给若干动态规则集,不是自动化,而是把风险藏进自动化。

本文只实现完整 Skill 的最小子集:单语言、单个本地规则、合成目标、无登录、无远程规则集。它足够解释核心机制,也不会把教程变成一次不透明的大规模安全扫描。

安装:把 CLI 留在项目隔离目录

Trail of Bits 为 Claude Code 提供的插件安装命令是:

text 复制代码
/plugin install trailofbits/skills/plugins/static-analysis

Codex 也可按仓库的 Codex 安装说明接入 sidecar Skills。不过这两种方式都会改变正式 Agent 环境,安装前必须审查目标提交、SKILL.md、引用文件、脚本和许可证。本文不执行它们。

真正运行扫描还需要 Semgrep CLI。Semgrep 官方已支持 Windows 原生运行;官方更新文档推荐 pipxuv 管理 CLI。为了让验证可删除、可复现,本文把它装进项目临时目录自己的虚拟环境:

powershell 复制代码
New-Item -ItemType Directory -Force `
  .tmp\ai-skill-2026-07-20\semgrep-demo | Out-Null

Set-Location .tmp\ai-skill-2026-07-20\semgrep-demo
uv venv .venv
uv pip install --python .venv\Scripts\python.exe "semgrep==1.170.0"

.\.venv\Scripts\semgrep.exe --metrics=off --version

本文实测环境是 Windows、Python 3.12.9、Semgrep 1.170.0。macOS/Linux 的可执行文件路径通常是 .venv/bin/semgrep。固定版本是为了让今天的结果可复核;正式项目应先在测试分支验证升级,再更新固定版本。

最小可复现实例:HTTP 参数进入 Shell

先创建 rules/python-shell-injection.yaml

yaml 复制代码
rules:
  - id: python-shell-injection-from-query
    message: Untrusted query parameter reaches subprocess.run(..., shell=True)
    languages: [python]
    severity: ERROR
    metadata:
      category: security
      confidence: HIGH
      impact: HIGH
      cwe: "CWE-78: Improper Neutralization of Special Elements used in an OS Command"
    mode: taint
    pattern-sources:
      - pattern: request.args.get(...)
    pattern-sinks:
      - patterns:
          - pattern: subprocess.run($CMD, ..., shell=True, ...)
          - focus-metavariable: $CMD

这条规则包含三个关键点:

  • mode: taint 表示追踪数据流,而不是只找一段固定语法;
  • pattern-sources 把 Flask 查询参数视为不可信来源;
  • pattern-sinks 只关注传给 subprocess.run 的命令参数,并要求调用显式使用 shell=True

Semgrep 官方把 taint rule 定义为 source、sink,以及可选的 sanitizer 和 propagator。Community Edition 的边界也要说清:它主要做单文件分析;跨文件追踪属于更强的商业引擎能力。本文故意把来源和危险调用放在同一个文件里,不假装 OSS 扫描已经覆盖跨文件调用链。可参考 Semgrep taint mode 文档官方术语说明

先给规则写测试

创建同名测试文件 rules/python-shell-injection.py

python 复制代码
from flask import request
import subprocess


def vulnerable():
    host = request.args.get("host")
    command = "ping -n 1 " + host
    # ruleid: python-shell-injection-from-query
    subprocess.run(command, shell=True, check=False)


def safe():
    host = request.args.get("host")
    if host not in {"127.0.0.1", "localhost"}:
        raise ValueError("unsupported host")
    # ok: python-shell-injection-from-query
    subprocess.run(["ping", "-n", "1", host], check=True)

ruleid 是应当命中的正例,防止规则未来产生漏报;ok 是不应命中的反例,防止规则变宽后把安全写法一起报出来。Semgrep 官方规则测试文档要求注释放在目标行正上方,并按规则文件名寻找对应语言的测试文件。

运行测试:

powershell 复制代码
.\.venv\Scripts\semgrep.exe --metrics=off --test rules

实测结果:

text 复制代码
1/1: ✓ All tests passed
No tests for fixes found.

这里甚至不需要安装 Flask,因为我们只分析源码,不导入或启动应用。把"扫描源码"和"执行被扫项目"分开,是处理不可信仓库时很实用的安全边界。

真实问题与扫描效果:HTTP 参数进入 Shell

把下面代码保存为 src/app.py

python 复制代码
from flask import request
import subprocess


def ping_host():
    host = request.args.get("host")
    command = "ping -n 1 " + host
    subprocess.run(command, shell=True, check=False)

如果 Web 应用真的执行这段代码,攻击者可能把 host 构造成带 Shell 元字符的字符串,使服务器执行预期之外的命令。本文不会启动它,也不会构造攻击载荷;静态数据流已经足够复现检测目标。

执行扫描,同时保存机器可读结果:

powershell 复制代码
.\.venv\Scripts\semgrep.exe scan `
  --metrics=off `
  --config rules\python-shell-injection.yaml `
  --error `
  --json-output=results.json `
  --sarif-output=results.sarif `
  src

核心输出是:

text 复制代码
1 Code Finding

src\app.py
rules.python-shell-injection-from-query
Untrusted query parameter reaches subprocess.run(..., shell=True)

8┆ subprocess.run(command, shell=True, check=False)

本次实测同时得到:

text 复制代码
规则测试退出码:0
扫描退出码:1
JSON findings:1
SARIF version:2.1.0
SARIF results:1

--error 是 CI 门禁的关键。根据 Semgrep CLI 官方参考semgrep scan 默认即使命中问题也可以返回 0;加上 --error 后,命中返回 1,工具本身失败返回 2,规则或配置错误还会使用其他退出码。CI 脚本不应把所有非零退出码都粗暴写成"发现漏洞",否则 YAML 写坏也会被误报为代码风险。

SARIF 则解决"下一步交给谁"的问题。终端输出适合本地阅读,JSON 适合自定义统计,SARIF 2.1.0 可交给兼容的代码扫描平台或后续聚合工具。官方 Skill 进一步要求保留每个规则集的原始输出,再合并最终 SARIF,避免过滤之后无法回溯证据。

修复与复扫:不要只做字符串转义

这个案例最直接的修复是停止拼接 Shell 命令,改用参数数组,并对业务允许的主机做白名单:

python 复制代码
from flask import request
import subprocess


ALLOWED_HOSTS = {"127.0.0.1", "localhost"}


def ping_host():
    host = request.args.get("host")
    if host not in ALLOWED_HOSTS:
        raise ValueError("unsupported host")
    subprocess.run(["ping", "-n", "1", host], check=True)

不要把"调用 shlex.quote() 后继续 shell=True"当成所有场景的万能修复。转义规则与 Shell、平台和命令上下文有关;能不用 Shell 时,参数数组的边界更清晰。

只扫描修复版:

powershell 复制代码
.\.venv\Scripts\semgrep.exe scan `
  --metrics=off `
  --config rules\python-shell-injection.yaml `
  --error `
  --json-output=fixed-results.json `
  src\app_fixed.py

实测结果:

text 复制代码
Findings: 0
退出码:0

至此证据链闭合:测试证明规则知道什么该报、什么不该报;脆弱版产生 1 条发现并阻断;修复版 0 命中并恢复绿色。

把它接进 CI 前,先理解 Skill 的审批门

如果不是本文这种单规则合成演示,而是让 semgrep Skill 扫描真实仓库,建议给 Agent 的任务写成:

text 复制代码
使用 semgrep Skill 为当前仓库准备安全扫描计划。
第一阶段只读取文件类型和数量,列出:
1. 扫描的绝对路径与排除目录;
2. OSS 或 Pro 引擎;
3. run all 或 important only;
4. 每一个本地或远程规则集的来源与固定版本;
5. 输出目录和可能的网络访问。

未经我对这份精确计划明确批准,不要开始扫描。
每条命令必须关闭 metrics;不要使用 --config auto;
不要执行仓库的构建、测试、安装或启动脚本;
原始结果与过滤后的 SARIF 分开保存。

"请扫描这个仓库"在官方 Skill 里不等于批准任意规则集和任意目标。它把计划批准设为硬门,是因为完整模式还可能使用 Trail of Bits、0xdea、Decurity 等第三方规则。第三方规则能提高覆盖面,也引入规则质量、版本漂移与供应链风险;使用前应列出并固定来源,而不是让 Agent 临场静默添加。

常见坑

1. 把 API 出现次数当漏洞数

rg "subprocess.run" 可以帮助盘点 sink,却无法判断第一个参数是否可控,也不知道 shell 的值。搜索结果是候选,不是安全结论。反过来,taint rule 也可能因为未建模的框架封装漏掉 source 或 propagator,不能替代人工复核。

2. 规则只有正例,没有反例

只写 ruleid,很容易得到一条"凡是 subprocess.run 都报"的规则。ok 用例应覆盖参数数组、白名单、常量参数以及团队真实的安全封装。规则库越重要,越应该像业务代码一样做回归测试。

3. 命中了,但 CI 仍然是绿的

本地 semgrep scan 默认不一定因为发现问题而失败。需要 --error,并在流水线里区分退出码 1(发现)和 2+(工具、规则或配置失败)。同时保存结果文件,别只依赖易丢失的控制台日志。

4. 用 --config auto 省掉规则治理

auto 会根据项目请求 Semgrep Registry,规则内容和网络行为不再完全由本地文件决定。官方 CLI 文档还说明:从 Registry 拉配置时,默认 metrics 策略可能发送伪匿名使用数据。安全审计中应列出精确规则、固定版本,并显式使用 --metrics=off

5. 以为 OSS 已经追完跨文件调用链

Community Edition 适合快速模式匹配和单文件数据流,但跨文件分析是另一层能力。若漏洞来源在控制器、净化逻辑在服务层、sink 在基础设施封装里,先确认引擎能力;没有 Pro 时可考虑 CodeQL,或把结论写成"当前规则覆盖到的单文件路径",不要夸大覆盖率。

6. SARIF 当成无害报告直接公开

SARIF 可能包含源码片段、规则说明、绝对路径、仓库结构和漏洞位置。上传到 CI 制品或第三方平台前,要检查可见范围、保留周期、脱敏策略和访问控制。真实 Secret 不应因为"方便复现"被复制进规则测试或报告。

7. 为了扫描,先执行不可信仓库

Semgrep 扫描源码通常不要求安装项目依赖,更不要求运行构建脚本。对陌生仓库执行 npm installpip install -r requirements.txt 或测试命令,会把静态分析扩大成供应链代码执行。只有确有需要且经过审查时,才在隔离环境中授予这类权限。

权限与安全边界

semgrep Skill 用在真实项目时,至少守住这些边界:

  • 只读起步:第一阶段只读源码、配置和必要 Git 历史,不修改业务代码,不自动应用 autofix;
  • 目标可见 :使用绝对扫描路径,明确排除 .git、依赖缓存、构建产物、大型生成文件和不在授权范围内的目录;
  • 网络可见:本地规则可离线运行;远程 Registry、第三方规则仓库、Pro 登录和结果上传都属于额外网络行为,必须单独说明;
  • 版本可追溯:Skill、Semgrep CLI 和远程规则集都固定到已审查版本,升级前看 diff 并重跑规则测试;
  • 数据最小化 :不给 Agent 生产 .env、Cookie、云凭据、客户数据、未脱敏日志或其他项目源码;
  • 输出受控:原始 JSON/SARIF 放在受限目录,设置访问控制与保留期限,公开分享前删除不必要的路径和代码片段;
  • 执行隔离:扫描陌生代码时不运行其安装、构建、测试、容器或启动脚本;如必须运行,使用临时副本、无生产凭据、受限网络和最低文件权限;
  • 人审结论:发现是待验证证据,不是自动定罪;高风险修改、PR 评论、Issue 创建和 CI 阻断策略仍由有权限的人批准。

适合与不适合的场景

适合

  • 把已经发生过的漏洞模式沉淀成可回归的组织规则;
  • 在 PR 中快速发现危险 API、注入链路和错误配置;
  • 给多语言仓库做第一轮安全筛查,并用 SARIF 汇总结果;
  • ruleid / ok 为自定义规则建立测试资产;
  • 在人工审计前缩小候选范围,或在修复后验证同类问题没有重新出现。

不适合

  • 需要证明"仓库绝对没有漏洞";静态分析存在误报、漏报和建模边界;
  • 主要目标是二进制逆向、运行时竞态、性能退化或复杂业务越权;
  • 关键数据流跨越多个文件,但只有 OSS 引擎且不能接受覆盖限制;
  • 没有权限读取目标源码,或扫描结果不能被妥善保护;
  • 已有成熟 Semgrep CI,而任务只是查看现有流水线结果;此时应复用既有配置,不要另起一套规则和基线。

结论

semgrep Skill 最值得学的不是某个规则集名字,而是它给静态分析加上的工程纪律:先把目标、引擎、规则和网络行为说清楚,经过批准再扫描;保留原始证据,生成标准结果;最后由人复核,而不是让"工具命中"自动升级成"漏洞定案"。

最小实战也遵循同一思路:正反例先定义规则边界,taint mode 追踪真正的数据流,--error 把发现变成门禁,SARIF 把结果交给后续系统,修复后再扫到 0。这样,安全扫描才从一条临时命令变成可测试、可审查、可重复的交付能力。

相关推荐
我要割麦子9 小时前
从零到一手撸 Agent 系列 — 第 4 篇:工具的契约 — Tool 接口与注册表
agent·ai编程
怕浪猫10 小时前
# 第3章 记忆系统:构建Agent的长短期记忆
openai·agent·ai编程
uccs10 小时前
Agent 工具系统怎么设计:注册、并发锁与截断
agent·ai编程·claude
love530love19 小时前
【笔记】AutoClaw NSIS 安装器卡死、进程杀不掉、目录删不了?我是这么解决的
人工智能·windows·笔记·agent
新知图书21 小时前
7.4 测试与发布(一键生成PPT智能体开发)
人工智能·agent·ai agent·智能体·扣子
wWYy.1 天前
Agent推理模式有哪些?
agent
辰4701 天前
从第一性原理出发看待 Codex 的学习
agent·ai编程·codex
阿里云云原生1 天前
阿里云 Agent Native Cloud:让智能体成为企业原生的能力
云原生·agent