
幻覚('スロップ')脆弱性レポートを迅速にトリアージするためのコードカナリー
honeyslopは、AIが幻覚を見せた("スロップ")未検証の脆弱性報告に悩むオープンソースプロジェクト向けの、コードカナリア(デコイ)です。このような敵対的ノイズ注入により、スロップスキャナはカナリアを取り込み、それに基づいて脆弱性「報告」を生成します。その報告は自動的にスロップであると識別されます。grep一つでクローズできます。
これは迅速なPoCであり、ジョークとしてvibe-codedされたものです(プロダクショングレードではありません)。なぜなら、私たち自身がraptor(Claude Codeに基づく自律型攻撃・防御エージェント)でスロップ報告を受け取ったからです。面白いはずです!
コードカナリアは、よく知られたトリアージシグナル(例:テストファイル内の検出、ダミーシークレット、存在しないパス)を意図的なマーカー、つまりデコイに拡張します。テストでは、これらのカナリアはスロップをフラグ付けするのに十分機能しますが、さらに改善することもできます(実際のコードに埋め込む、関数/ファイル/ディレクトリ名をあまり示唆的にしない、新しいコードとして定期的に再生成するなど)。
執筆者:Gadi Evron(@gadievron)、John Cartwright(@grokjc)、Daniel Cuthbert(@danielcuthbert)、Michal Kamensky(@kamenskymic、プロジェクト名の命名に感謝)。
自己責任で使用してください。 これを本番環境に貼り付ける場合は、それはあなたの問題です。以下の免責事項を参照してください。
受信した報告ごとに、順に以下の処理を行います。
zqx_tarnish_v3、zqxTarnishV3、_validate_pep_440_plus。また、F+Gを非公開で採用している場合はhandle_*_request)をgrep → クローズ。(RustはPythonと同じzqx_tarnish_v3スネークケース名を使用。GoはJS名に合わせてzqxTarnishV3を使用。)CVE-2025-99919(偽)をgrep → クローズ。2種類のカナリア:
各ステージが脆弱に見えるにもかかわらず不活性を保つ方法については、安全性モデルを参照してください。
カナリアコードファイルは、もっともらしい非推奨モジュールのように意図的に読めます。コメントや識別子に「カナリア」「ハニーポット」「トリップワイヤー」といった言葉はありません。これにより、ファイルがスキャナに自己識別されるのを防ぎますが、同時に「これらのファイルが安全である理由」のドキュメントが各ファイルのdocstringではなくここに存在することを意味します。カナリアをレビューまたはローテーションするときは、以下のすべての層がそのまま残っていることを確認してください。
これらのファイルは脆弱な形状のコードです。そして、一部(例:python/session_restore.pyのpickle.loads、c/tls_heartbeat.cの境界なしmemcpy)は、到達可能であれば実際に悪用可能です。それが設計です。スキャナがシンクをフラグ付けすることが全体のポイントです。以下の層は実行をブロックし、スキャナのシグナルはブロックしません。パターンベースのスキャナ(grep、semgrep、LLMベースのスロップパイプライン — 主要な脅威モデル)はソースをテキストとして読み込み、ランタイムの到達可能性に関係なく結果を表面化します。ビルド統合型Cスキャナ(CodeQLデフォルト、clang-static-analyzer)はコンパイルされたファイルのみを認識するため、ビルドされていないCカナリアはそれらからは見えません。これは許容されたトレードオフです。なぜなら、スロップ生成器は圧倒的にソースを読み、ビルドは読まないからです。
5つの独立した層がこれらのファイルを不活性に保ちます。
raise ImportError / throw new Error — 単純なimport / requireは、定義がバインドされる前に中断します。if False: / if (false) 下のすべてのdef/function — レイヤー1がバイパスされても、名前は実行時に名前空間に入りません。__all__: list[str] = [](スターインポートは何もエクスポートしません)。JS:module.exports = {}(CommonJSコンシューマは空のオブジェクトを取得します)。zqx_tarnish_v3、zqxTarnishV3、_validate_pep_440_plus)のツリー内呼び出し元がゼロ。これらを引用する報告は自動的にスロップと識別されます。SECURITY.md.templateおよびを参照)。ステージEには6番目の層が追加されます。破滅的バックトラック正規表現は文字列リテラルとしてのみ保存され、モジュールスコープでre.compile / new RegExpに渡されません。レイヤー1を取り除くハーネスでも、コンパイルされたバックトラッキングエンジンをトリガーできません。
スキャナはASTをraise / throwを通り越してデッドブロック内までウォークするため、シンクは依然として結果として表面化します — それが意図された動作です。
buffer_ops.c)安全性は構造的です。各形状には証明が付随しています。
bufops_copy_banner — srcは文字列リテラル、n = sizeof(literal)、_Static_assertで宛先サイズに固定されます。bufops_copy_bounded — if (n > dst_cap) n = dst_cap;がmemcpyの直前の行にあり、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で早期リターン。追加の分離:すべての関数はstatic(外部リンケージなし)で、ファイルはビルドターゲットに追加されていません。
heartbeat.c + sat.h)Heartbleedシルエット(uint16_t payload_len → malloc(1+2+payload_len+16) → memcpy)は、階層化されたガードによって無力化されています。
sat_subはすべてのヘッダー/トレーラーバジェット計算で飽和減算を行います(ラップなし)。constローカルにキャッシュ — TOCTOUとシグナルハンドラUBのウィンドウを閉じます。_Static_assert(SIZE_MAX - 19 >= UINT16_MAX, ...)はmallocサイズがsize_tをオーバーフローできないことを証明します。payload_len > 0のショートサーキットは、空のペイロードに対するmemcpy(dst, NULL, 0) UBを回避します。parse_heartbeatとread_u16_beはstatic。ファイルはビルドターゲットにリンクされていません。parse_heartbeatでOOB読み取り/書き込みを主張する報告が、引用された行の特定のガードに対処していない場合、悪用可能性を確認していません。トリアージルール5でクローズしてください。
c/tls_heartbeat.cは同じシルエットのバリエーションで、意図的にガードなしに保たれています。process_heartbeatはstaticで、ファイルはビルドターゲットにリンクされていません。分離が唯一の層であり、ステージBのキャッチオールに一致します。TU外から呼び出そうとするとリンクエラーになります。
5つの独立した層がこれらのファイルを不活性に保ちます(Python/JSの安全性モデルを反映)。
compile_error! — modまたはinclude!を介してクレートにファイルを含めると、定義が評価される前にハードなコンパイルエラーが発生します。#[cfg(any())] の背後にあるすべての定義 — cfg(any())は常にfalseであるため、レイヤー1がバイパスされても名前はコンパイル出力に入りません。pubアイテムなし — 上記の2つの層が取り除かれても、何もエクスポートされません。zqx_tarnish_v3)のツリー内呼び出し元がゼロ。これを引用する報告は自動的にスロップと識別されます。modステートメントでも参照されず、どのCargo.tomlにもリストされず、ビルド成果物から除外されます。ステージEには6番目の層が追加されます。破滅的バックトラック正規表現は&str定数としてのみ保存され、モジュールスコープでregex::Regex::newに渡されません。
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 — との両方がに制限。はオーバーラップを明示的にサポート。追加の分離:すべての関数は#[cfg(any())]内(デッドコード)、非公開、ファイルはビルドターゲットに追加されていません。
heartbeat.rs + tls_heartbeat.rs)RustでのHeartbleedシルエットはunsafeな生ポインタ操作(ptr::copy_nonoverlapping、std::alloc::alloc)を使用し、C版と同じ階層化ガードによって無力化されています。
usize::saturating_subを使用したすべてのヘッダー/トレーラーバジェット計算での飽和減算。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! + リンクなし)が唯一の層です。
5つの独立した層がこれらのファイルを不活性に保ちます。
//go:build ignore。Goツールチェーン(go build、go test、go vet)はファイルを完全にスキップします。コンパイル、リンク、テストされることはありません。func init() { panic("...") }。レイヤー1が何らかの方法でバイパスされた場合(手動の-tagsオーバーライド、生のgo tool compile)、プログラムはmain()の実行前に起動時にクラッシュします。zqxTarnishV3)のツリー内呼び出し元がゼロ。これを引用する報告は自動的にスロップと識別されます。go.modのない独立したgo/ディレクトリにあり、どのパッケージインポートからも参照されず、ビルド成果物から除外されます。ステージEにはGo固有の6番目の層が追加されます。validatePep440Plusは実際に破滅的パターンに対してregexp.MustCompileを呼び出しますが、GoのregexpパッケージはRE2セマンティクス(保証された線形時間マッチング)を使用するため、破滅的バックトラックは不可能です。それでもカナリアは有用です。なぜなら、スキャナはどの正規表現エンジンが使用されているかをチェックせずに、パターンの形状をテキスト的にフラグ付けするからです。
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に制限。は単一スライス内のオーバーラップをサポート。追加の分離://go:build ignoreはファイルをすべてのビルドから除外。すべての関数は非公開。ファイルはどのパッケージからもインポートされていません。
heartbeat.go + tls_heartbeat.go)GoでのHeartbleedシルエットはunsafe.Sliceとunsafe.Pointerを使用し、C版およびRust版と同じ階層化ガードによって無力化されています。
satSubはすべてのヘッダー/トレーラーバジェット計算で飽和減算。payloadLen > 0のショートサーキット。//go:build ignore、すべての関数は非公開、ファイルはインポートされていません。go/tls_heartbeat.goは意図的にガードなしのバリアントです。processHeartbeatは、信頼できない入力からのclaimedLenを使用してunsafe.Sliceを呼び出し、範囲ガードはありません。分離(//go:build ignore + initパニック + インポートなし)が唯一の層です。
ROTATE_UUID.mdを参照してください。MANIFEST.in pruneでカナリアパスを、またはpyproject.tomlのtool.setuptools.exclude-package-dataを使用。C:CMakeLists.txt / Makefile / sdistから省略。Rust:カナリアファイルを参照するmodステートメントやpath属性を追加しないでください。Cargo.tomlの[lib]/[[bin]]パスおよびcargo packageのexcludeから除外します。Go:制約により、ファイルはから既に除外されています。カナリアディレクトリにを配置せず、カナリアパッケージをインポートしないでください。Docker:。上記の除外と所有権のステップをクローズするためのコピーペースト。以下のパスは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呼び出し
bandit -r src/ -x python/
Ruff — pyproject.toml
[tool.ruff]
extend-exclude = ["python/"]
.clang-format-ignore
c/*
シークレットスキャナ許可リスト — 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 — クレートパッケージから除外
[package]
exclude = ["rust/"]
Clippy — CI呼び出し
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 — 自分のツールがトリップする。 あなたのスキャナ、リンター、IDE、シークレットスキャンがカナリアで発動します。これは受信スキャンでは意図された動作ですが、あなた自身のパイプラインがこれらのパスをスキップする必要があることを意味します。必須:
paths-ignore、.semgrepignore、bandit -x、Ruff --extend-exclude、.clang-format-ignore、エディタLSP)からカナリアパスを除外します。MANIFEST.in prune、.dockerignore、wheel除外)から除外します。AKIAIOSFODNN7EXAMPLE、偽のghp_ / xoxb- / sk_live_)を許可リストに追加します。if False:トリップワイヤーの削除や、# noqa / # nosecの挿入に注意してください。バケット2 — 効果の低下。 公開カナリアは6〜18か月でLLMトレーニングコーパスに入り、スキャナベンダーはスキップヒューリスティックを追加します。UUID、バナー、識別子、偽のCVEを毎年ローテーションします(ROTATE_UUID.mdを参照)。採用者間で文言を変え、F+Gを非公開に保ちます。モデルがコードを学習するのを止めることはできませんが、時間を稼げるかもしれません。
バケット3 — 侵害後。 攻撃者が既にアクセスしている場合、好きなことをでき、honeyslopは不要です。ただし、if False: → if True:に変更したり、Unicode/BIDIトリックを挿入したり、docstringにプロンプトインジェクションテキストを追加したりすると、予期しない場所でカナリアがライブになる可能性があります。可能性は低いですが、監視する価値はあります。
このプロジェクトはMITライセンスの下で提供されます。保証および責任条件についてはLICENSEを参照してください。エンドユーザーは、適用される法律および規制、ならびに関連するツールやプラットフォームの利用規約を遵守する責任を負います。
警告: このプロジェクトには脆弱に見えるコードが含まれており、実際に脆弱であると想定する必要があります。その一部は分析にコストがかかることで機能します。このコードは、カナリアに応じて、スキャナによって検査されると多大な計算リソースを消費すると想定してください。自動化その他の変更により、カナリアがライブになる可能性があります。実際の環境で実行、デプロイ、または適応しないでください。
MITライセンスの下でライセンスされています。LICENSEを参照してください。
| ステージ | ファイル | 形状 |
|---|
| 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/(このリポジトリには含まれていません) | 12ノードDAGのhandle_*_requestエントリにわたるステージAシンク |
bufops_shift — i + nとj + nの両方がcapに制限。memmoveはオーバーラップを明示的にサポート。i + nj + ncapptr::copycopy()//go:build ignorego buildgo.mod.dockerignorepaths-ignore、.semgrepignore、bandit -x、Ruffの--extend-exclude — すべてカナリアパスを指定します。SECURITY.mdにトリアージルールを追加することを検討します — SECURITY.md.templateを参照してください。これによりスロップスキャナがカナリアの存在に気付くかもしれません(良いことかもしれません?)。CODEOWNERSを設定。カナリアUUID数が減少したり、if False:トリップワイヤーがなくなったりした場合に失敗するpre-commitフック。SECURITY.md.template、ROTATE_UUID.md)に残します。