
Code-Canaries zur schnellen Triagierung halluzinierter ('Slop') Schwachstellenberichte
honeyslop besteht aus Code-Canaries, also Ködern, für Open-Source-Projekte, die in KI-halluzinierten („Slop“) und unverifizierten Schwachstellenmeldungen ertrinken. Durch diese adversariale Rausch-Injektion liest ein Slop-Scanner die Canary ein und erzeugt darauf basierend einen Schwachstellen-„Report“. Der Report identifiziert sich damit selbst als Slop. Ein einziger grep genügt, um ihn zu schließen.
Dies ist eine schnelle PoC, die als Scherz vibe-coded wurde (nicht produktionsreif), weil wir selbst bei raptor, einem autonomen Angriffs-/Verteidigungsagenten auf Basis von Claude Code, einen Slop-Report erhalten haben. Sollte Spaß machen!
Code-Canaries wandeln vertraute Triage-Signale (z. B. Befunde in Testdateien, Beispiel-Secrets, nicht existierende Pfade) in bewusst platzierte Marker bzw. Köder um. In Tests funktionieren diese Canaries gut genug, um Slop zu markieren, aber sie lassen sich weiter verbessern (eingebettet in echten Code, weniger verräterische Funktions-/Datei-/Verzeichnisnamen, regelmäßig als neuer Code regeneriert usw.).
Geschrieben von: Gadi Evron (@gadievron), John Cartwright (@grokjc), Daniel Cuthbert (@danielcuthbert) und Michal Kamensky (@kamenskymic, mit Props für die Namensgebung des Projekts).
Nutzung auf eigene Gefahr. Wenn du das in die Produktion einfügst, ist das dein Problem. Siehe Haftungsausschluss unten.
Für jeden eingehenden Report, in dieser Reihenfolge:
zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus; außerdem handle_*_request, falls du F+G privat übernommen hast) → schließen. (Rust verwendet denselben zqx_tarnish_v3-snake_case-Namen wie Python. Go verwendet zqxTarnishV3, passend zum JS-Namen.)CVE-2025-99919 (fiktiv) → schließen.Zwei Kategorien von Canaries:
Siehe Sicherheitsmodell für die Erklärung, warum jede Stufe inert bleibt, obwohl sie verwundbar aussieht.
Die Canary-Codedateien lesen sich bewusst wie plausible, veraltete Module — in Kommentaren oder Bezeichnern kommt kein „canary“, „honeypot“ oder „tripwire“ vor. Das verhindert, dass sich die Dateien gegenüber Scannern selbst verraten, bedeutet aber auch, dass die Dokumentation zum Warum diese Dateien sicher sind hier lebt und nicht in den Docstrings der einzelnen Dateien. Beim Überprüfen oder Rotieren einer Canary ist zu prüfen, dass jede der folgenden Ebenen noch intakt ist.
Diese Dateien sind verwundbar geformter Code — und einige (z. B. pickle.loads aus python/session_restore.py, das unbegrenzte memcpy aus c/tls_heartbeat.c) wären bei Erreichbarkeit tatsächlich ausnutzbar. Das ist das Design. Dass Scanner die Sinks melden, ist der eigentliche Zweck; die Ebenen unten blockieren die Ausführung, nicht das Scanner-Signal. Musterbasierte Scanner (grep, semgrep, LLM-basierte Slop-Pipelines — das primäre Bedrohungsmodell) lesen Quelltext als Text und melden Befunde unabhängig von der Laufzeiterreichbarkeit. Build-integrierte C-Scanner (CodeQL-Standard, clang-static-analyzer) sehen nur kompilierte Dateien, daher sind ungebaute C-Canaries für sie unsichtbar — ein akzeptierter Kompromiss, da Slop-Generatoren überwiegend Quelltext lesen und keine Builds.
Fünf unabhängige Ebenen halten diese Dateien inert:
raise ImportError / throw new Error auf oberster Ebene — ein schlichtes import / require bricht ab, bevor irgendeine Definition gebunden wird.if False: / if (false) — Namen gelangen nie in den Laufzeit-Namespace, selbst wenn Ebene 1 umgangen wird.__all__: list[str] = [] (Star-Import exportiert nichts). JS: module.exports = {} (CommonJS-Konsumenten erhalten ein leeres Objekt).zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus). Jeder Report, der eine davon zitiert, identifiziert sich selbst als Slop.Stufe E fügt eine sechste Ebene hinzu: Die Regex mit katastrophalem Backtracking wird nur als String-Literal gespeichert und auf Modulebene nicht an re.compile / new RegExp übergeben. Selbst ein Harness, das Ebene 1 entfernt, kann keine kompilierte Backtracking-Engine auslösen.
Scanner laufen im AST an raise / throw vorbei in den toten Block hinein, sodass die Sinks weiterhin als Befunde auftauchen — das ist das beabsichtigte Verhalten.
buffer_ops.c)Die Sicherheit ist strukturell — zu jeder Form gehört ein Beweis:
bufops_copy_banner — src ist ein String-Literal, n = sizeof(literal), _Static_assert fixiert es auf die Zielgröße.bufops_copy_bounded — if (n > dst_cap) n = dst_cap; direkt vor dem memcpy macht die CWE-787-Behauptung unmöglich. Kurzschluss bei n == 0, um das C17-UB von memcpy(dst, NULL, 0) zu vermeiden.bufops_copy_truncating — n <= dst_cap - 1, dst[n] erreicht höchstens dst_cap - 1; frühzeitiges return bei .Zusätzliche Isolation: Alle Funktionen sind static (keine externe Bindung) und die Datei ist keinem Build-Target hinzugefügt.
heartbeat.c + sat.h)Die Heartbleed-Silhouette (uint16_t payload_len → malloc(1+2+payload_len+16) → memcpy) wird durch gestaffelte Absicherungen entschärft:
sat_sub für die gesamte Header-/Trailer-Budgetrechnung (kein Überlauf).const-Lokale zwischengespeichert — schließt TOCTOU- und Signalhandler-UB-Fenster._Static_assert(SIZE_MAX - 19 >= UINT16_MAX, ...) beweist, dass die malloc-Größe size_t nicht überlaufen kann.payload_len > 0 vermeidet das UB memcpy(dst, NULL, 0) bei leeren Payloads.parse_heartbeat und read_u16_be sind static; die Datei ist in kein Build-Target eingebunden.Ein Report, der einen OOB-Read/Write in parse_heartbeat behauptet, ohne sich mit der spezifischen Absicherung in der zitierten Zeile auseinanderzusetzen, hat die Ausnutzbarkeit nicht verifiziert — gemäß Triage-Regel 5 schließen.
c/tls_heartbeat.c ist eine Variation derselben Silhouette, die bewusst ungeschützt gehalten wird: process_heartbeat ist static und die Datei ist in kein Build-Target eingebunden — Isolation ist die einzige Ebene, passend zur Auffangregel von Stufe B. Der Versuch, sie von außerhalb der Übersetzungseinheit aufzurufen, führt zu einem Linkfehler.
Fünf unabhängige Ebenen halten diese Dateien inert (in Anlehnung an das Python/JS-Sicherheitsmodell):
compile_error! auf oberster Ebene — das Einbinden der Datei in eine Crate über mod oder include! erzeugt einen harten Compilerfehler, bevor irgendeine Definition ausgewertet wird.#[cfg(any())] — cfg(any()) ist immer falsch, sodass Namen nie in die kompilierte Ausgabe gelangen, selbst wenn Ebene 1 umgangen wird.pub-Elemente — es wird nichts exportiert, selbst wenn beide obigen Ebenen entfernt werden.zqx_tarnish_v3). Jeder Report, der sie zitiert, identifiziert sich selbst als Slop.mod-Anweisung referenziert, ist in keiner Cargo.toml aufgeführt und von Build-Artefakten ausgeschlossen.Stufe E fügt eine sechste Ebene hinzu: Die Regex mit katastrophalem Backtracking wird nur als &str-Konstante gespeichert und auf Modulebene nicht an regex::Regex::new übergeben.
buffer_ops.rs)Die Sicherheit ist strukturell, passend zum C-Pendant. Jede Form hat einen Beweis:
bufops_copy_banner — src ist b"status: ok\0", die Kopierlänge ist BANNER.len(); eine const-Assertion fixiert sie auf die Zielgröße.bufops_copy_bounded — if n > dst_cap { n = dst_cap; } direkt vor der Kopie begrenzt den Schreibvorgang. Kurzschluss bei n == 0.bufops_copy_truncating — n <= dst_cap - 1, der NUL-Schreibvorgang bei dst.add(n) erreicht höchstens dst_cap - 1; frühzeitiges return bei dst_cap == 0.Zusätzliche Isolation: Alle Funktionen liegen innerhalb von #[cfg(any())] (toter Code), sind nicht öffentlich, und die Datei ist keinem Build-Target hinzugefügt.
heartbeat.rs + tls_heartbeat.rs)Die Heartbleed-Silhouette in Rust verwendet unsafe-Rohzeigeroperationen (ptr::copy_nonoverlapping, std::alloc::alloc) und wird durch dieselben gestaffelten Absicherungen wie die C-Version entschärft:
usize::saturating_sub für die gesamte Header-/Trailer-Budgetrechnung.usize::MAX - 19 >= u16::MAX) beweist, dass die Allokation nicht überlaufen kann.payload_len > 0.#[cfg(any())], nicht öffentlich, Datei nicht eingebunden.rust/tls_heartbeat.rs ist die bewusst ungeschützte Variante: process_heartbeat verwendet ptr::copy_nonoverlapping mit claimed_len aus nicht vertrauenswürdiger Eingabe und ohne Grenzprüfung. Isolation (#[cfg(any())] + compile_error! + nicht eingebunden) ist die einzige Ebene.
Fünf unabhängige Ebenen halten diese Dateien inert:
//go:build ignore am Anfang jeder Datei. Die Go-Toolchain (go build, go test, go vet) überspringt die Datei vollständig; sie wird nie kompiliert, gelinkt oder getestet.func init() { panic("...") } mit UUID. Wenn Ebene 1 irgendwie umgangen wird (manuelles -tags-Override, direktes go tool compile), stürzt das Programm beim Start ab, bevor main() läuft.zqxTarnishV3). Jeder Report, der sie zitiert, identifiziert sich selbst als Slop.go/-Verzeichnis ohne go.mod, werden von keinem Paketimport referenziert und sind von Build-Artefakten ausgeschlossen.Stufe E fügt eine Go-spezifische sechste Ebene hinzu: validatePep440Plus ruft regexp.MustCompile zwar mit dem katastrophalen Muster auf, aber das regexp-Paket von Go verwendet RE2-Semantik (garantiert lineare Laufzeit), sodass katastrophales Backtracking ohnehin unmöglich ist. Die Canary ist trotzdem nützlich, weil Scanner die Musterform textuell erkennen, ohne zu prüfen, welche Regex-Engine verwendet wird.
buffer_ops.go)Die Sicherheit ist strukturell, passend zu den C- und Rust-Pendants. Jede Form hat einen Beweis:
bufopsCopyBanner — src ist eine String-Konstante, copy() von einem bekannten Literal in ein Array fester Größe; eine Compile-Zeit-Assertion fixiert die Länge.bufopsCopyBounded — if n > dstCap { n = dstCap } direkt vor der Kopie begrenzt den Schreibvorgang. Kurzschluss bei n == 0.bufopsCopyTruncating — n <= dstCap - 1, der NUL-Schreibvorgang bei dst[n] erreicht höchstens dstCap - 1; frühzeitiges return bei dstCap == 0.bufopsShift — sowohl i + n als auch sind auf begrenzt; unterstützt Überlappungen innerhalb eines einzelnen Slice.Zusätzliche Isolation: //go:build ignore schließt die Datei aus allen Builds aus, alle Funktionen sind nicht exportiert, und die Datei wird von keinem Paket importiert.
heartbeat.go + tls_heartbeat.go)Die Heartbleed-Silhouette in Go verwendet unsafe.Slice und unsafe.Pointer und wird durch dieselben gestaffelten Absicherungen wie die C- und Rust-Versionen entschärft:
satSub für die gesamte Header-/Trailer-Budgetrechnung.payloadLen > 0.//go:build ignore, alle Funktionen nicht exportiert, Datei nicht importiert.go/tls_heartbeat.go ist die bewusst ungeschützte Variante: processHeartbeat verwendet unsafe.Slice mit claimedLen aus nicht vertrauenswürdiger Eingabe und ohne Grenzprüfung. Isolation (//go:build ignore + init-Panic + nicht importiert) ist die einzige Ebene.
ROTATE_UUID.md.MANIFEST.in prune ausschließen oder pyproject.toml tool.setuptools.exclude-package-data. C: aus CMakeLists.txt / Makefile / sdist weglassen. Rust: keine mod-Anweisung oder path-Attribut hinzufügen, die die Canary-Dateien referenzieren; aus den [lib]-/[[bin]]-Pfaden der Cargo.toml und über exclude aus cargo package ausschließen. Go: Die -Einschränkung schließt die Dateien bereits von aus; keine im Canary-Verzeichnis ablegen und das Canary-Paket nicht importieren. Docker: .Zum Kopieren und Einfügen, um die obigen Ausschluss- und Ownership-Schritte abzuschließen. Die Pfade unten verwenden das c/- / python/- / js/- / rust/- / go/-Layout von honeyslop; benenne sie dorthin um, wo du die Canaries letztendlich platzierst (siehe Schritt 7) — Ausschlüsse zu committen, die weiterhin auf Verzeichnisse namens canary/ oder slop/ zeigen, ist selbst ein Verrätermerkmal.
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-Aufruf
bandit -r src/ -x python/
Ruff — pyproject.toml
[tool.ruff]
extend-exclude = ["python/"]
.clang-format-ignore
c/*
Allowlist für Secret-Scanner — 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 — aus dem Crate-Paket ausschließen
[package]
exclude = ["rust/"]
Clippy — CI-Aufruf
cargo clippy --workspace -- --allow-dead-code
Oder das Canary-Verzeichnis komplett auslassen, indem es in keinem mod-Baum referenziert wird (Standardeinstellung — compile_error! fängt versehentliches Einbinden ab).
golangci-lint — .golangci.yml
issues:
exclude-dirs:
- go
Oder sich auf die //go:build ignore-Einschränkung verlassen, die bereits verhindert, dass die Go-Toolchain die Canary-Dateien kompiliert.
.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
Die Owners sollten eine kleine Gruppe sein, die versteht, warum diese Pfade verwundbar aussehen — damit PRs zum „Aufräumen von totem Code“ blockiert statt gemerged werden.
Bucket 1 — deine eigenen Tools lösen aus. Deine Scanner, Linter, IDE und das Secret-Scanning werden auf der Canary anschlagen. Das ist das beabsichtigte Verhalten für eingehende Scans, bedeutet aber, dass deine eigenen Pipelines diese Pfade überspringen müssen. Erforderlich:
paths-ignore, .semgrepignore, bandit -x, Ruff --extend-exclude, .clang-format-ignore, Editor-LSP).MANIFEST.in prune, .dockerignore, Wheel-Ausschlüsse).AKIAIOSFODNN7EXAMPLE, fiktive ghp_ / xoxb- / sk_live_) im Secret-Scanner auf die Allowlist setzen.if False:-Stolperdrahts und auf das Einfügen von # noqa / achten.Bucket 2 — Erosion der Wirksamkeit. Öffentliche Canaries gelangen über 6–18 Monate in LLM-Trainingskorpora, und Scanner-Anbieter fügen Skip-Heuristiken hinzu. Rotiert UUIDs, Banner, Shibboleths und die fiktive CVE jährlich (siehe ROTATE_UUID.md); variiert die Formulierungen zwischen den Adoptern; haltet F+G privat. Das wird die Modelle nicht davon abhalten, den Code zu lernen, könnte dir aber etwas Zeit verschaffen.
Bucket 3 — nach einer Kompromittierung. Wenn ein Angreifer bereits Zugriff hat, kann er tun, was er will, und braucht honeyslop nicht. Allerdings kann das Umdrehen von if False: → if True:, das Einfügen von Unicode-/Bidi-Tricks oder das Hinzufügen von Prompt-Injection-Text in Docstrings eine Canary dort aktiv werden lassen, wo man es nicht erwartet. Es lohnt sich, darauf zu achten, auch wenn es unwahrscheinlich ist.
Dieses Projekt wird unter der MIT-Lizenz bereitgestellt; siehe LICENSE für Gewährleistungs- und Haftungsbedingungen. Es liegt in der Verantwortung des Endnutzers, geltende Gesetze und Vorschriften sowie die Nutzungsbedingungen der beteiligten Tools oder Plattformen einzuhalten.
Warnung: Dieses Projekt enthält Code, der verwundbar aussieht, und genau davon sollte ausgegangen werden. Ein Teil davon funktioniert, indem er aufwendig zu analysieren ist — geh davon aus, dass dieser Code bei der Prüfung durch einen Scanner erhebliche Rechenressourcen verbraucht, je nach Canary. Jede Änderung, ob automatisiert oder nicht, könnte eine Canary aktiv werden lassen. Führe nichts davon in einer echten Umgebung aus, setze nichts davon ein und adaptiere nichts davon.
Lizenziert unter MIT. Siehe LICENSE.
| Stufe | Datei(en) | Form |
|---|
| 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-Sinks + Fake-Secrets + Shibboleths |
| B | c/buffer_ops.c, rust/buffer_ops.rs, go/buffer_ops.go | 4 memcpy-/memmove-Formen (CWE-120/121/787/170) |
| C | in A zusammengeführt | Erweiterte CWE-Ausbeute |
| 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-Silhouette |
| E | python/regex_validator.py, js/regex_validator.js, rust/regex_validator.rs, go/regex_validator.go | Regex mit katastrophalem Backtracking + fiktives CVE-2025-99919 |
| F+G | private/fractal_dag/ (nicht in diesem Repo) | Stufe-A-Sinks über einen 12-Knoten-DAG aus handle_*_request-Einträgen |
SECURITY.md.templatedst_cap == 0bufops_shift — sowohl i + n als auch j + n sind auf cap begrenzt; memmove unterstützt Überlappungen ausdrücklich.bufops_shift — sowohl i + n als auch j + n sind auf cap begrenzt; ptr::copy unterstützt Überlappungen ausdrücklich.j + ncapcopy()//go:build ignorego buildgo.mod.dockerignorepaths-ignore, .semgrepignore, bandit -x, Ruff --extend-exclude — alles auf die eigenen Canary-Pfade ausgerichtet.SECURITY.md aufzunehmen — siehe SECURITY.md.template. Das könnte Slop-Scanner auf die Präsenz der Canary aufmerksam machen (vielleicht eine gute Sache?).CODEOWNERS auf den Canary-Dateien; ein Pre-Commit-Hook, der fehlschlägt, wenn die Anzahl der Canary-UUIDs sinkt oder if False:-Stolperdrähte verschwinden.SECURITY.md.template, ROTATE_UUID.md), wo sie eine tragende Funktion hat.# nosec