SuperMarioYL/promptshield

GitHub: SuperMarioYL/promptshield

PromptShield 是一款离线扫描工具,用于在 AI 编程助手读取代码前检测并拦截隐藏在代码注释、commit 和文档中的提示词注入攻击。

Stars: 1 | Forks: 0

[English](./README.en.md) | **简体中文**

PromptShield

PromptShield 是在 Claude Code / Cursor 读取代码前,拦截隐藏注入指令的扫描器。

License: MIT Python 3.12+ Release v0.3.0 CI PRs welcome
Agentic coding security Offline, no API key

## 目录 - [为什么需要它](#为什么需要它) - [60 秒上手](#60-秒上手) - [演示](#演示) - [它扫描什么](#它扫描什么) - [集成到 CI](#集成到-ci) - [基线工作流](#基线工作流) - [对比同类方案](#对比同类方案) - [配置项](#配置项) - [付费 · PromptShield Cloud](#付费--promptshield-cloud) - [路线图](#路线图) - [许可证与贡献](#许可证与贡献) - [分享](#share-this) ## 为什么需要它 Claude Code、Cursor 这类 coding agent 会把第三方仓库、PR、代码注释、commit message **直接读进上下文**,然后照着读到的内容去执行——但在仓库和 agent 之间,没有任何一层把这些代码**当作提示词**来扫描。 这正是 [r/LocalLLaMA 那条 "dev sneaks data-nuking prompt injection into their code"](https://www.reddit.com/r/LocalLLaMA/comments/1trdnap/fed_up_with_vibe_coders_dev_sneaks_datanuking/) 帖子揭露的攻击面:有人把一句 `rm -rf /` 的销毁指令藏进注释里,等着别人的 agent 读到并执行。人工 review 的眼睛追不上 agent 的阅读吞吐——这是一种**结构性的吞吐量不对称**,不是"再仔细一点"能解决的。 **为什么是现在:** agentic coding 的普及,把"agent 读到的代码"变成了"agent 会照做的代码"。Cursor 已有上百万开发者,[affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code) 这类 Claude Code 实战仓库让 agent 例行拉取外部 PR 与 vendored OSS。两年前主流还是人把片段复制进聊天框,agent 没有对外部仓库自主读取并执行的回路;MCP 与工具调用的扩张才让注入有了通往"动作"的路径。PromptShield 就是在 agent 读取之前,把这条注入面卡在 CI 里——离线、零 API key、几秒出结果。 PromptShield 引入的新原语是 **Finding over a code-as-prompt surface**:把 agent 将要*读取*的源文本视为攻击向量,而不是当作可执行代码或要做 CVE 扫描的依赖包。这与聊天越狱扫描(Garak)和依赖漏洞扫描(Socket / Snyk)是不同的威胁类别。 ## 架构

架构:仓库 / git diff / PR JSON 经 Collectors 拆成代码即提示词的 Surface,YAML 规则引擎把每段映射成五类注入 Finding,基线抑制已接受项,Report 打印发现表并给出 CI 退出码

一次扫描接收三种 **Source**——整个仓库、一段 `git diff`,或一份 `gh api` 的 PR-files JSON——**Collectors** 把它们拆解成 agent 真正会*读到*的 **Surface**(注释、docstring、commit message、markdown、配置、字符串字面量)。**Rule Engine** 用 YAML 规则集把每段 Surface 映射成横跨五类注入的 **Finding**,高噪声规则用 `requires` 二次门控压低误报;**基线**抑制已接受的历史 Finding。最后 **Report** 打印发现表(或 JSON),只要出现任何 HIGH 就 `exit 1`——整条链路离线、零 API key,卡在 agent 读取之前的 CI 门里。 ## 60 秒上手 ``` pip install promptshield # 或: uvx promptshield promptshield scan . # 扫描当前仓库,打印发现表 echo "退出码: $?" # 出现任何 HIGH 即为 1,可直接接入 CI promptshield scan . --format sarif # 输出 SARIF 2.1.0,上传到 GitHub Security 标签页 promptshield rules list # 查看当前生效的合并规则集 ```
示例输出(扫描内置的恶意 PR 复现样本) ``` $ promptshield scan ./tests/fixtures/malicious_pr PromptShield findings ┏━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━┓ ┃ Sev ┃ Rule ┃ Category ┃ Location ┃ Surf ┃ ┡━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━┩ │ HIGH │ PS001-instruction-override-d… │ instruction_ov… │ utils.py:11 │ com… │ │ HIGH │ PS010-destructive-shell │ data_destructi… │ utils.py:12 │ com… │ │ HIGH │ PS011-destructive-instruction │ data_destructi… │ utils.py:17 │ doc… │ │ HIGH │ PS020-exfil-secrets │ exfiltration │ utils.py:17 │ doc… │ └──────┴───────────────────────────────┴─────────────────┴─────────────┴──────┘ Scanned 31 surfaces · 8 HIGH · 0 MED · 0 LOW ✗ HIGH findings present — exit 1 (CI gate failed). ``` 每一条 HIGH 都会附带一句 `why`,说明它为什么是注入——精度优先于召回,让你信得过每一个告警。
`promptshield scan .` 不需要 `git` 或 `gh`,也不发任何网络请求。只有用 `--diff` / `--pr` 时才会把它们当子进程调用。 ## 演示 脚本:扫描 `malicious_pr` → 发现表 → `--pr` 退出码 1 让 CI 变红 → 现场命中真实的 Reddit 数据销毁注入。 ![演示](https://static.pigsec.cn/wp-content/uploads/repos/cas/70/703f347a4d9868cc26c210b99628a10d134b328559af6f19017df7ca2c2b7954.gif) ## 它扫描什么 PromptShield 把仓库或 diff 拆解成 **Surface** 记录(agent 会读到的每一段文本),再用 YAML 规则引擎把每段映射成零或多个 **Finding**。 **扫描的 Surface(代码即提示词的面):** 代码注释 · docstring · commit message · markdown / README · 配置文件 · 字符串字面量。 **五类威胁(seed 规则集,v0.1):** | 类别 | 含义 | 代表规则 | | --- | --- | --- | | `instruction_override` | 直接对 agent 喊话、让它忽略此前指令 | `PS001` `PS002` `PS003` | | `data_destructive` | 删库 / 销毁数据(Reddit 那类攻击) | `PS010` `PS011` `PS012` | | `exfiltration` | 读取密钥并外发 | `PS020` `PS021` `PS022` | | `tool_abuse` | 诱导 agent 跳过确认、滥用工具自主权 | `PS030` `PS031` | | `obfuscation` | 零宽 / 双向 Unicode、编码载荷等规避人工 review 的手法 | `PS040` `PS041` | 严重度分三档:**HIGH**(令 CI 失败)· **MED** · **LOW**。为压低误报,部分高噪声动词规则用 `requires` 二次门控——只有同时出现 agent 喊话或"全部 / 递归 / 生产库"这类措辞时才会触发,避免一句"删掉临时缓存"的普通注释就被误报。 **混淆解码 pass(v0.2):** 每段 Surface 还会过一遍解码——base64 / hex / 零宽字符剥离 / 同形字(homoglyph)归一化后的变体都会被重新扫描,所以一条藏在编码层背后的注入仍会被命中;`--no-decode` 可关掉。扫描结果可输出为 **SARIF 2.1.0**(`--format sarif`)供 GitHub code-scanning 摄取,规则集也可用 **可堆叠规则包**(`--rules`,详见[配置项](#配置项))按团队策略扩展。 ## 集成到 CI 把仓库自带的 [`.github/workflows/promptshield.yml`](./.github/workflows/promptshield.yml) 复制进任意仓库的 `.github/workflows/`,每个 PR 就会被自动门控: - **PR 上**:`promptshield scan --diff origin/` 只扫改动过的 Surface(新增行 + 新 commit message)。 - **push 到 main**:`promptshield scan .` 扫整棵树。 - 出现任何 **HIGH** 即退出 1,让 check 变红。 - **SARIF 上报**:Action 还会以 `--format sarif` 再跑一遍并把结果上传到仓库的 Security → Code scanning 标签页——即便 HIGH 让扫描 exit 1,SARIF 照样上传,所以发现项会出现在 Security 面板里,而不只是一个红 check。 也可以手动接入: ``` # 只扫本次相对 main 的改动 promptshield scan --diff origin/main # 扫一份 gh api PR-files JSON(CI 里无需 checkout 全量历史) gh api repos/OWNER/REPO/pulls/123/files > pr.json promptshield scan --pr pr.json # 有 HIGH 即 exit 1 ``` ## 基线工作流 在有历史包袱的老仓库上,先把当前所有 Finding 记成"已接受",之后只对**新增**注入告警: ``` promptshield scan . --update-baseline # 写入 .promptshield-baseline.yaml,并 exit 0 git add .promptshield-baseline.yaml promptshield scan . # 旧 Finding 被抑制,只报新出现的 ``` 基线按指纹(`rule_id` + 文件 + excerpt 哈希)抑制,所以一旦真有新注入潜入,它仍会浮出水面。 ## 对比同类方案 定位,不是吹嘘——在该认输的地方如实认输。 vs [affaan-m/ECC](https://github.com/affaan-m/ECC)(Claude Code / Cursor 实战配置仓库,代表"agent 例行吞入外部代码"的工作流): | 能力轴 | PromptShield | Garak / PromptBench | Socket / Snyk | | --- | :---: | :---: | :---: | | 扫描**源码注释 / commit / markdown** 中的注入 | ✓ | ✗ | ✗ | | 离线、零 API key、几秒出结果 | ✓ | 部分 | ✓ | | 依赖包 CVE / 供应链漏洞 | ✗ | ✗ | ✓ | | 聊天越狱 / 红队语料成熟度 | 部分 | ✓ | ✗ | | 一行接入 PR 门控(GitHub Action) | ✓ | 部分 | ✓ | Garak 在**聊天提示词**红队上更成熟,Socket / Snyk 在**依赖供应链**上无可替代——它们和 PromptShield 是互补关系。PromptShield 只专注一件它们都没碰的事:把你的 coding agent *会读进上下文并照做*的源文本,当作提示词来扫描。 ## 配置项 `scan` 命令的主要选项: | 选项 | 类型 | 默认值 | 含义 | | --- | --- | --- | --- | | `PATH` | 路径 | `.` | 要扫描的目录或文件 | | `--diff REF` | 字符串 | 无 | 只扫 `git diff REF` 的新增行 + 新 commit message | | `--pr FILE.json` | 路径 | 无 | 扫一份 `gh api .../files` 的 PR-files JSON | | `--baseline FILE` | 路径 | `.promptshield-baseline.yaml` | 用于抑制已接受 Finding 的基线文件 | | `--update-baseline` | 开关 | `false` | 把当前所有 Finding 写入基线并 exit 0 | | `--rules FILE / DIR` | 路径(可重复) | 内置规则集 | 自定义 `rules.yaml` 或目录;可重复,包按加载顺序堆叠(同 id 后者覆盖前者) | | `--format FORMAT` | 枚举 | `table` | 输出格式:`table` / `json` / `sarif`(SARIF 2.1.0) | | `--no-decode` | 开关 | `false` | 关闭混淆解码 pass(base64 / hex / 零宽 / 同形字) | | `--json` | 开关 | `false` | 输出机器可读 JSON 而非 Rich 表格(`--format json` 的别名) | | `--no-color` | 开关 | `false` | 关闭彩色输出 | | `--repo DIR` | 路径 | `.` | `--diff` 使用的仓库目录 | ### 规则包 `--rules` 可重复、也可指向一个目录——内置 seed 规则集先加载,每个自定义包按顺序堆叠其上,**同 id 的规则后者覆盖前者**(last-wins)。这样团队无需 fork 内置规则,就能叠加自己的策略包、收窄某条规则,或直接关掉它: ``` # my-team-rules.yaml — 叠加在内置规则集之上 rules: - id: PS050-custom-exfil # 新增一条团队私有规则 severity: HIGH category: exfiltration why: 内部约定的密钥外发措辞,内置规则未覆盖。 patterns: - "dump (env|secrets|\\.env) to .* curl" - id: PS012-credential-deletion # 直接关掉某条内置规则 enabled: false ``` ``` promptshield scan . --rules my-team-rules.yaml --rules policies/ # 多包堆叠 promptshield rules list --rules my-team-rules.yaml # 查看合并后的生效规则集 ``` ## 付费 · PromptShield Cloud **CLI 与 GitHub Action 永久开源免费**,它们是漏斗的入口;营收来自托管的 **PromptShield Cloud** 团队版——把 CLI 之上的团队协作能力变成订阅。 | | 开源 CLI / Action | PromptShield Cloud(团队版) | | --- | --- | --- | | 本地 / CI 扫描 | ✓ | ✓ | | 五类 seed 规则集 | ✓ | ✓ | | 单仓库基线 | ✓ | ✓ | | **组织级 PR 门控**(多仓库统一策略) | ✗ | ✓ | | **共享 / 中心化基线**(跨仓库) | ✗ | ✓ | | **统一 Finding 看板** | ✗ | ✓ | | **Slack / 飞书 告警**(命中 HIGH 即推送) | ✗ | ✓ | | 托管的攻击签名 / 规则订阅源 | ✗ | ✓ | **定价:** **$8 / 席位 / 月**(年付),或 **$99 / 月**封顶团队版(含 15 席)——比一个 Snyk 席位更省,同时是免费 CLI 之上清晰的"团队协作"升级。 **最短付费路径:** Cloud 看板用一个 token 接入团队现有的 Action 输出 → 展示全组织 Finding + 跨仓库共享基线 → 14 天试用 → Stripe Checkout 自助下单。最有说服力的演示:**你的 6 个仓库,一块看板,一份基线,HIGH 命中即 Slack 告警。** ## 路线图 - [x] **m1 — `scan `**遍历仓库,抽取注释 / docstring / markdown / 字符串字面量为 Surface,跑 YAML 规则引擎,打印 Rich 发现表 + 严重度计数。 - [x] **m2 — diff 与 CI**:`scan --diff `(git diff 新增行)+ `scan --pr `(gh PR-files JSON),HIGH → exit 1,附带 `promptshield.yml` Action。 - [x] **m3 — 基线与演示**:`.promptshield-baseline.yaml` 抑制;复现真实 Reddit 数据销毁注入的 `tests/fixtures/malicious_pr/`;asciinema 演示;双语 README。 - [x] **m4 — SARIF 输出**:`scan --format sarif` 输出 SARIF 2.1.0;GitHub Action 通过 `upload-sarif` 把发现项上报到 Security → Code scanning 标签页。 - [x] **m5 — 可堆叠规则包**:`--rules` 可重复、可指向目录,包按加载顺序堆叠(同 id 后者覆盖前者);规则带 `enabled` 开关;`rules list` 查看合并后的规则集。 - [x] **m6 — 混淆解码 pass**:base64 / hex / 零宽 / 同形字变体重新扫描,`--no-decode` 可关。 - [x] **v0.3 — 检测正确性 + 抗绕过加固**:5 项已复现缺陷修复——(m8) 修掉带撇号字符串前缀导致整条注释被跳过的漏报 / 绕过向量(`msg = "don't" # <注入>` 曾返回 0 findings);(m9) 停止扫描 `.promptshield-baseline.yaml` 本身,避免基线文件反复自我命中;(m10/m11) 修正 diff 解析对 `++` 开头新增行的文件归属,以及 `\ No newline at end of file` 造成的行号 +1 漂移;(m12) 把 `decoded_from`(编码层)透传到 Finding,解码命中的发现项会标注 `[base64]` 而非被当成误报。 - [ ] 语义检测(可选、需开关)——在 regex / 启发式之上叠加,提升对绕过手法的召回。 - [ ] 托管攻击签名 / 规则订阅源(PromptShield Cloud)。 - [ ] 上架 GitHub Marketplace,进入 awesome-claude-code / awesome-ai-coding 等清单。 ## 许可证与贡献 [MIT](./LICENSE)。欢迎 PR 与 Issue——发现误报或漏报?带上能复现的最小样本,到 [Issues](https://github.com/SuperMarioYL/promptshield/issues) 开一条,我们会据此收紧规则。 变更记录见 [CHANGELOG.md](./CHANGELOG.md)。 ## 分享 ``` PromptShield — 在你的 Claude Code / Cursor agent 读取代码之前,扫描其中隐藏的提示词注入。 离线、零 API key、一行接入 CI。现场拦下 Reddit 那条 rm -rf 数据销毁注入。 https://github.com/SuperMarioYL/promptshield ```
标签:AI安全, Chat Copilot, GitHub Action, SOC Prime, 云安全监控, 开发工具, 提示词注入防护, 逆向工具, 静态分析