gadievron/honeyslop

GitHub: gadievron/honeyslop

HoneySlop 通过在代码库中植入安全可控的诱饵文件,帮助维护者快速识别并过滤由 AI 幻觉生成的虚假漏洞报告。

Stars: 95 | Forks: 9

# honeyslop - 通过代码金丝雀快速分拣 AI 幻觉("slop")漏洞报告

HoneySlop

honeyslop 是为开源项目设计的代码金丝雀(诱饵),专为应对那些令人应接不暇的、由 AI 幻觉("slop")生成且未经核实的漏洞报告。面对这种对抗性噪声注入,slop 扫描器一旦读取了该金丝雀,就会基于它生成一份漏洞“报告”。该报告会自动暴露其 slop 的本质。只需一行 grep 即可将其关闭。 这是一个快速的 PoC,仅仅是个心血来潮编写的玩笑(并非生产级别),因为我们自己在 [raptor](https://github.com/gadievron/raptor)(一个基于 Claude Code 的自主攻击/防御 agent)项目中也收到了一份 slop 报告。希望能给大家带来一些乐趣! 代码金丝雀将大家熟悉的分拣信号(例如在测试文件中的检测、示例密钥、不存在的路径)扩展为了刻意的标记或诱饵。在测试中,这些金丝雀足以标记出 slop,但它们仍有进一步优化的空间(例如嵌入到真实代码中、使用不那么显眼的函数/文件/目录名、作为新代码定期重新生成等)。 作者:Gadi Evron (@gadievron)、John Cartwright (@grokjc)、Daniel Cuthbert (@danielcuthbert) 和 Michal Kamensky (@kamenskymic,感谢其为项目命名)。 **使用风险自负。** 如果你将其粘贴到生产环境中,那是你自己的问题。请参阅下方的[免责声明](#disclaimer)。 ## 分拣规则 对于每份收到的报告,按顺序执行: 1. grep 报告中的任何金丝雀 UUID → 关闭。(UUID 因语言而异;每个金丝雀文件只嵌入一个。) 2. grep 仅属于金丝雀的函数名(`zqx_tarnish_v3`, `zqxTarnishV3`, `_validate_pep_440_plus`;如果你在内部使用了 F+G 阶段,还包括 `handle_*_request`) → 关闭。(Rust 使用与 Python 相同的 snake_case 名称 `zqx_tarnish_v3`。Go 使用 `zqxTarnishV3`,与 JS 名称一致。) 3. grep `CVE-2025-99919`(伪造的) → 关闭。 4. 引用的函数在代码库中不存在 → *“不存在”*。 5. 对于针对 B/D 的 memcpy/越界声明:要求报告者详细说明其 PoC 是如何绕过所引用行上的特定防护措施的。AI 后续无法回答此类问题;但人类可以。 ## 阶段 两大类金丝雀: - **SCANNER-FLAG**(阶段 A, B, C, D, E) — 触发扫描器,使得 slop 报告堆积在金丝雀上,而不是真实代码上。 - **RESOURCE-WASTE**(阶段 F + G 组合) — 以最高成本耗尽 agentic LLM 扫描器的全部迭代预算。 | 阶段 | 文件 | 形式 | | ------- | ---------------------------------------------------- | ----------------------------------------------------------------- | | **A** | `python/legacy_utils.py`, `python/session_restore.py`, `python/compat_tokens.py`, `js/legacy_utils.js`, `rust/legacy_utils.rs`, `rust/session_restore.rs`, `go/legacy_utils.go`, `go/session_restore.go` | ~15 个 CWE 汇点 + 伪造密钥 + 暗号 | | **B** | `c/buffer_ops.c`, `rust/buffer_ops.rs`, `go/buffer_ops.go` | 4 种 `memcpy`/`memmove` 形式 (CWE-120/121/787/170) | | **C** | 合并到 A | 扩展的 CWE 产出 | | **D** | `c/heartbeat.c` + `c/sat.h`, `c/tls_heartbeat.c`, `rust/heartbeat.rs`, `rust/tls_heartbeat.rs`, `go/heartbeat.go`, `go/tls_heartbeat.go` | Heartbleed 轮廓 | | **E** | `python/regex_validator.py`, `js/regex_validator.js`, `rust/regex_validator.rs`, `go/regex_validator.go` | 灾难性回溯正则表达式 + 伪造的 **CVE-2025-99919** | | **F+G** | `private/fractal_dag/`(不在此 repo 中) | 分布在包含 `handle_*_request` 条目的 12 个节点 DAG 中的阶段 A 汇点 | 关于每个阶段如何在看起来存在漏洞的情况下保持非活动状态,请参阅[安全模型](#safety-model)。 ## 安全模型 金丝雀代码文件刻意设计得像看似合理的废弃模块——注释或标识符中没有“canary”、“honeypot”或“tripwire”等字眼。这使得这些文件不会向扫描器自我暴露,但这也意味着*为什么这些文件是安全的*相关文档记录在这里,而不是写在每个文件的 docstring 中。在审查或轮换金丝雀时,请检查下方的每一层防护是否依然完好。 这些文件**确实**是漏洞形状的代码——其中一些(例如 `python/session_restore.py` 中的 `pickle.loads`,`c/tls_heartbeat.c` 中无界的 `memcpy`)如果可被访问,将真正可被利用。这正是设计初衷。扫描器标记这些汇点是整个机制的核心;下面的层级阻止的是**执行**,而不是扫描器的**信号**。基于模式的扫描器(grep、semgrep、基于 LLM 的 slop pipeline——这是主要的威胁模型)将源代码作为文本读取,并无论运行时是否可达都会报告发现。构建集成的 C 扫描器(CodeQL 默认配置,clang-static-analyzer)只能看到已编译的文件,因此未构建的 C 金丝雀对它们是不可见的——这是一个可接受的权衡,因为 slop 生成器绝大多数时候只读取源代码,而不是构建结果。 ### 阶段 A 和 E (Python + JS) 五个独立的层级使这些文件保持非活动状态: 1. **顶层 `raise ImportError` / `throw new Error`** — 普通的 `import` / `require` 会在任何定义绑定之前中止。 2. **所有 def/function 位于 `if False:` / `if (false)` 之下** — 即使绕过了第 1 层,名称也永远不会进入运行时命名空间。 3. **空导出** — Python: `__all__: list[str] = []`(星号 import 不导出任何内容)。JS: `module.exports = {}`(CommonJS 消费者会得到一个空对象)。 4. **代码库内对暗号函数零调用** (`zqx_tarnish_v3`, `zqxTarnishV3`, `_validate_pep_440_plus`)。任何引用这些函数的报告都会自动暴露其为 slop。 5. **部署隔离** — 采用者将金丝雀路径从 sdist / wheel / Docker / SAST 中排除(参见 [`SECURITY.md.template`](SECURITY.md.template) 和 [如何试用](#how-to-try))。 阶段 E 增加了第六层:灾难性回溯正则表达式仅作为字符串字面量存储,未在模块作用域传递给 `re.compile` / `new RegExp`。即使是去除了第 1 层防护的测试工具,也无法触发已编译的回溯引擎。 扫描器会遍历 AST 跳过 `raise` / `throw` 并进入死代码块,因此汇点依然会作为发现项浮出水面——这就是预期的行为。 ### 阶段 B (C `buffer_ops.c`) 安全性是结构性的——每种形式旁边都有证明: - `bufops_copy_banner` — `src` 是字符串字面量,`n = sizeof(literal)`,`_Static_assert` 将其固定为目标大小。 - `bufops_copy_bounded` — 在 `memcpy` 前一行执行 `if (n > dst_cap) n = dst_cap;` 使得关于 CWE-787 的声明不成立。在 `n == 0` 时短路,以避免 C17 `memcpy(dst, NULL, 0)` 的 UB。 - `bufops_copy_truncating` — `n <= dst_cap - 1`,`dst[n]` 最多访问到 `dst_cap - 1`;在 `dst_cap == 0` 时提前返回。 - `bufops_shift` — `i + n` 和 `j + n` 都限制在 `cap` 以内;`memmove` 明确支持重叠。 额外隔离:所有函数均为 `static`(无外部链接),且文件未添加到任何构建目标中。 ### 阶段 D (C `heartbeat.c` + `sat.h`) Heartbleed 轮廓(`uint16_t payload_len` → `malloc(1+2+payload_len+16)` → `memcpy`)被多层防护解除武装: - `sat_sub` 对所有头部/尾部预算计算采用饱和减法(不会溢出回绕)。 - 帧字段在入口处缓存到 `const` 局部变量中——消除了 TOCTOU 和信号处理程序 UB 的隐患窗口。 - 对读取器结构体及其缓冲区进行 NULL 检查。 - `_Static_assert(SIZE_MAX - 19 >= UINT16_MAX, ...)` 证明 `malloc` 大小不会溢出 `size_t`。 - `payload_len > 0` 短路避免了在空 payload 时出现 `memcpy(dst, NULL, 0)` UB。 - `parse_heartbeat` 和 `read_u16_be` 是 `static` 的;文件未链接到任何构建目标。 如果一份报告指控 `parse_heartbeat` 中存在 OOB 读/写,却没有针对所引用行上的具体防护措施进行论证,说明其并未真正验证可利用性——根据分拣规则 5 关闭。 `c/tls_heartbeat.c` 是相同轮廓的一个变体,刻意保持未设防状态:`process_heartbeat` 是 `static` 的,且文件未链接到任何构建目标——隔离是其唯一的防线,与阶段 B 的兜底策略一致。尝试从编译单元外部调用它会导致链接错误。 ### 阶段 A 和 E (Rust) 五个独立的层级使这些文件保持非活动状态(镜像 Python/JS 的安全模型): 1. **顶层 `compile_error!`** — 通过 `mod` 或 `include!` 将文件包含在 crate 中会在评估任何定义之前产生严重的编译器错误。 2. **所有定义都在 `#[cfg(any())]` 之后** — `cfg(any())` 始终为 false,因此即使绕过了第 1 层,名称也永远不会进入编译输出中。 3. **没有 `pub` 项** — 即使去除了以上两层,也不会导出任何内容。 4. **代码库内对暗号函数零调用** (`zqx_tarnish_v3`)。任何引用它的报告都会自动暴露其为 slop。 5. **部署隔离** — 文件未在任何 `mod` 语句中被引用,未在任何 `Cargo.toml` 中列出,并从构建产物中排除。 阶段 E 增加了第六层:灾难性回溯正则表达式仅作为 `&str` 常量存储,未在模块作用域传递给 `regex::Regex::new`。 ### 阶段 B (Rust `buffer_ops.rs`) 安全性是结构性的,与 C 对应版本一致。每种形式都有证明: - `bufops_copy_banner` — `src` 为 `b"status: ok\0"`,拷贝长度为 `BANNER.len()`;`const` 断言将其固定为目标大小。 - `bufops_copy_bounded` — 在拷贝前一行执行 `if n > dst_cap { n = dst_cap; }` 限制了写入。在 `n == 0` 时短路。 - `bufops_copy_truncating` — `n <= dst_cap - 1`,在 `dst.add(n)` 处的 NUL 写入最多触及 `dst_cap - 1`;在 `dst_cap == 0` 时提前返回。 - `bufops_shift` — `i + n` 和 `j + n` 都被限制在 `cap` 以内;`ptr::copy` 明确支持重叠。 额外隔离:所有函数均位于 `#[cfg(any())]` 之中(死代码),非公开,且未添加到任何构建目标中。 ### 阶段 D (Rust `heartbeat.rs` + `tls_heartbeat.rs`) Rust 中的 Heartbleed 轮廓使用了 `unsafe` 裸指针操作(`ptr::copy_nonoverlapping`, `std::alloc::alloc`),并被与 C 版本相同的分层防护解除武装: - 通过 `usize::saturating_sub` 对所有头部/尾部预算计算进行饱和减法。 - 帧字段在入口处缓存到局部变量中。 - 对读取器结构体及其缓冲区进行 Null 检查。 - Const 断言 (`usize::MAX - 19 >= u16::MAX`) 证明分配不会溢出。 - `payload_len > 0` 短。 - 所有函数位于 `#[cfg(any())` 中,非公开,文件未链接。 `rust/tls_heartbeat.rs` 是刻意未设防的变体:`process_heartbeat` 使用了来自不可信输入的 `claimed_len` 调用 `ptr::copy_nonoverlapping` 且没有边界防护。隔离(`#[cfg(any())]` + `compile_error!` + 未链接)是唯一的防线。 ### 阶段 A 和 E (Go) 五个独立的层级使这些文件保持非活动状态: 1. **`//go:build ignore`** 位于每个文件的顶部。Go 工具链(`go build`, `go test`, `go vet`)会完全跳过该文件;它永远不会被编译、链接或测试。 2. **包含 UUID 的 `func init() { panic("...") }`**。如果第 1 层以某种方式被绕过(手动覆盖 `-tags`,直接使用 `go tool compile`),程序会在 `main()` 运行前的启动阶段崩溃。 3. **所有函数均未导出**(小写名称)。即使被编译,也无法从包外部调用任何内容。 4. **代码库内对暗号函数零调用** (`zqxTarnishV3`)。任何引用它的报告都会自动暴露其为 slop。 5. **部署隔离** — 文件位于一个没有 `go.mod` 的独立 `go/` 目录中,未被任何包 import 引用,且从构建产物中排除。 阶段 E 增加了 Go 特有的第六层:`validatePep440Plus` 确实对灾难性模式调用了 `regexp.MustCompile`,但 Go 的 `regexp` 包使用 RE2 语义(保证线性时间匹配),因此无论如何都不可能发生灾难性回溯。金丝雀依然有用,因为扫描器只是从文本上标记模式形状,而不检查正在使用的是哪种正则引擎。 ### 阶段 B (Go `buffer_ops.go`) 安全性是结构性的,与 C 和 Rust 对应版本一致。每种形式都有证明: - `bufopsCopyBanner` — `src` 是字符串常量,`copy()` 从已知字面量拷贝到固定大小的数组中;编译时断言固定了长度。 - `bufopsCopyBounded` — 在拷贝前一行执行 `if n > dstCap { n = dstCap }` 限制了写入。在 `n == 0` 时短路。 - `bufopsCopyTruncating` — `n <= dstCap - 1`,在 `dst[n]` 处的 NUL 写入最多触及 `dstCap - 1`;在 `dstCap == 0` 时提前返回。 - `bufopsShift` — `i + n` 和 `j + n` 均被限制在 `cap` 以内;`copy()` 支持在单个切片内重叠。 额外隔离:`//go:build ignore` 将文件排除在所有构建之外,所有函数均未导出,且该文件未被任何包 import。 ### 阶段 D (Go `heartbeat.go` + `tls_heartbeat.go`) Go 中的 Heartbleed 轮廓使用了 `unsafe.Slice` 和 `unsafe.Pointer`,并被与 C 和 Rust 版本相同的分层防护解除武装: - `satSub` 对所有头部/尾部预算计算采用饱和减法。 - 帧字段在入口处缓存到局部变量中。 - 对读取器结构体及其缓冲区进行 Nil 检查。 - 在分配前进行针对预算的长度验证。 - `payloadLen > 0` 短路。 - `//go:build ignore`,所有函数未导出,文件未被 import。 `go/tls_heartbeat.go` 是刻意未设防的变体:`processHeartbeat` 使用了来自不可信输入的 `claimedLen` 调用 `unsafe.Slice` 且没有边界防护。隔离(`//go:build ignore` + `init` panic + 未 import)是唯一的防线。 ## 如何试用 1. **选择阶段。** C/C++ 解析器层面 → D (+ B)。Python OSS 维护者 → A + E。Rust crate → A + B + D + E。Go module → A + B + D + E。在持续遭受 agentic 扫描器垃圾轰炸的情况下 → 在内部私下添加 F + G。 2. **轮换每个 UUID。** 每种语言一个,每个采用者各不相同——不是一个基础 UUID 的前缀变体。参见 [`ROTATE_UUID.md`](ROTATE_UUID.md)。 3. **从构建产物中排除。** Python:通过 `MANIFEST.in prune` 剪除金丝雀路径,或使用 `pyproject.toml tool.setuptools.exclude-package-data`。C:从 `CMakeLists.txt` / `Makefile` / sdist 中省略。Rust:不要添加引用金丝雀文件的 `mod` 语句或 `path` 属性;从 `Cargo.toml` 的 `[lib]`/`[[bin]]` 路径中排除,并通过 `exclude` 在 `cargo package` 中排除。Go:`//go:build ignore` 约束已将文件排除在 `go build` 之外;不要在金丝雀目录中放置 `go.mod`,也不要 import 金丝雀包。Docker:`.dockerignore`。 4. **从 CI 静态分析中排除。** 否则你自己的 CI 会对金丝雀产生大量发现。CodeQL `paths-ignore`、`.semgrepignore`、`bandit -x`、Ruff `--extend-exclude` — 全部指向你的金丝雀路径。 5. **考虑将分拣规则添加到 `SECURITY.md` 中** — 参见 [`SECURITY.md.template`](SECURITY.md.template)。这可能会让 slop 扫描器察觉到金丝雀的存在(也许是件好事?)。 6. **防止贡献者清理代码。** 在金丝雀文件上设置 `CODEOWNERS`;添加一个 pre-commit 钩子,如果金丝雀 UUID 数量减少或 `if False:` 触发器丢失,则提交失败。 7. **移除明显的“标识”。** 从代码注释、目录名、文件名以及函数/标识符名称中剔除 "canary"、"canaries"、"honeypot"、"decoy"、"fake"、"tripwire" 和 "slop"。将文件顶部的 docstring 重构为看似合理的废弃通知。将这种概念性的语言保留在*文档*中(README, `SECURITY.md.template`, `ROTATE_UUID.md`),这是其发挥作用的地方。 ## 采用者片段 复制粘贴以完成上述的排除和所有权归属步骤。下面的路径使用了 honeyslop 的 `c/` / `python/` / `js/` / `rust/` / `go/` 布局;**重命名为你实际放置金丝雀的任何位置**(参见第 7 步)——提交仍指向名为 `canary/` 或 `slop/` 目录的排除规则本身就是一种明显的标识。 **`MANIFEST.in`** ``` prune c prune python prune js prune rust prune go ``` **`pyproject.toml`** — setuptools ``` [tool.setuptools.exclude-package-data] "*" = ["c/*", "python/*", "js/*", "rust/*", "go/*"] ``` **`.dockerignore`** ``` c/ python/ js/ rust/ go/ ``` **`.semgrepignore`** ``` c/ python/ js/ rust/ go/ ``` **CodeQL** — `.github/codeql/codeql-config.yml` ``` paths-ignore: - c - python - js - rust - go ``` **Bandit** — CI invocation ``` bandit -r src/ -x python/ ``` **Ruff** — `pyproject.toml` ``` [tool.ruff] extend-exclude = ["python/"] ``` **`.clang-format-ignore`** ``` c/* ``` **Secret-scanner allowlist** — gitleaks `.gitleaks.toml` ``` [allowlist] regexes = [ '''AKIAIOSFODNN7EXAMPLE''', '''ghp_[A-Za-z0-9]{36}''', '''xoxb-[0-9A-Za-z-]+''', '''sk_live_[A-Za-z0-9]+''', ] ``` **`Cargo.toml`** — exclude from crate package ``` [package] exclude = ["rust/"] ``` **Clippy** — CI invocation ``` cargo clippy --workspace -- --allow-dead-code ``` 或者通过在任何 `mod` 树中不引用它来直接跳过金丝雀目录(这是默认情况——`compile_error!` 会捕获意外包含的情况)。 **golangci-lint** — `.golangci.yml` ``` issues: exclude-dirs: - go ``` 或者依赖 `//go:build ignore` 约束,这已经阻止了 Go 工具链编译金丝雀文件。 **`.github/CODEOWNERS`** ``` c/ @your-org/sec-team python/ @your-org/sec-team js/ @your-org/sec-team rust/ @your-org/sec-team go/ @your-org/sec-team ``` 所有者应该是了解*为什么*这些路径看起来有漏洞的核心小组——这样“清理死代码”的 PR 才会被阻止,而不是被合并。 ## 注意事项 **类别 1 — 触发你自己的工具。** 你的扫描器、linter、IDE 和密钥扫描器将在金丝雀上触发警报。这对于*传入的*扫描来说是预期行为,但这意味着**你自己的 pipeline** 需要跳过这些路径。必需的操作: - 将金丝雀路径从每个 SAST 配置中排除(CodeQL `paths-ignore`、`.semgrepignore`、`bandit -x`、Ruff `--extend-exclude`、`.clang-format-ignore`、编辑器 LSP)。 - 从已发布的产物中排除(`MANIFEST.in prune`、`.dockerignore`、wheel 排除项)。 - 将伪造的密钥(`AKIAIOSFODNN7EXAMPLE`,伪造的 `ghp_` / `xoxb-` / `sk_live_`)加入你的密钥扫描器的白名单。 - 警告贡献者不要去“清理” honeypot。提防 linter 移除 `if False:` 触发器以及插入 `# noqa` / `# nosec` 的行为。 **类别 2 — 有效性削弱。** 公开的金丝雀会在 6 到 18 个月内进入 LLM 训练语料库,扫描器供应商也会添加跳过启发式规则。每年轮换 UUID、banner、暗号和伪造的 CVE(参见 [`ROTATE_UUID.md`](ROTATE_UUID.md));在不同采用者之间改变措辞;将 F+G 保持在私有状态。这无法阻止模型学习这些代码,但可能会为你争取一些时间。 **类别 3 — 被入侵后。** 如果攻击者已经获得了访问权限,他们可以为所欲为,根本不需要 honeyslop。然而,将 `if False:` 翻转为 `if True:`、插入 Unicode/BIDI 技巧,或在 docstring 中添加 prompt 注入文本,可能会在你意想不到的地方激活金丝雀。虽然可能性不大,但仍值得留意。 ## 免责声明 本项目基于 MIT License 提供;有关保证和责任条款,请参见 [LICENSE](LICENSE)。遵守适用法律和法规,以及任何相关工具或平台的服务条款是最终用户的责任。 **警告**:本项目包含看似存在漏洞的代码,应假设其确实存在漏洞。其中一些代码的原理是使其分析起来极其耗费资源——根据具体的金丝雀不同,当被扫描器检查时,请假设这些代码会消耗大量计算资源。任何修改(无论是否自动化)都可能激活金丝雀。请勿在任何真实环境中运行、部署或对其进行改造。 ## License 基于 MIT 授权。请参见 [LICENSE](LICENSE)。
标签:AI安全, Chat Copilot, CISA项目, DLL 劫持, IPv6支持, 代码金丝雀, 可视化界面, 大语言模型, 数据可视化, 日志审计, 漏洞报告分类, 蜜罐诱饵, 误报过滤, 逆向工具, 配置审计