
Code canaries pour trier rapidement les rapports de vulnérabilités hallucinées ('slop')
honeyslop est un ensemble de canaris de code, des leurres, destinés aux projets open-source submergés par des rapports de vulnérabilité hallucinés par l'IA (« slop ») et non vérifiés. Avec une telle injection de bruit adversarial, un scanner slop ingère le canari, puis génère un « rapport » de vulnérabilité basé sur celui-ci. Le rapport s'identifie lui-même comme du slop. Clôturez-le en un seul grep.
Ceci est un PoC rapide, codé au feeling comme une blague (pas de qualité production), car nous avons nous-mêmes reçu un rapport slop sur raptor, un agent autonome d'attaque/défense basé sur Claude Code. Ce devrait être amusant !
Les canaris de code étendent les signaux de triage familiers (par exemple, les détections dans les fichiers de test, les secrets d'exemple, les chemins inexistants) en marqueurs délibérés, ou leurres. Lors des tests, ces canaris fonctionnent suffisamment bien pour signaler du slop, mais ils peuvent être encore améliorés (intégrés dans du code réel, noms de fonctions/fichiers/répertoires moins indicateurs, régénérés régulièrement comme nouveau code, etc.).
Écrit par : Gadi Evron (@gadievron), John Cartwright (@grokjc), Daniel Cuthbert (@danielcuthbert) et Michal Kamensky (@kamenskymic, avec des remerciements pour avoir nommé le projet).
Utilisation à vos propres risques. Si vous collez ceci en production, c'est votre problème. Voir Avertissement ci-dessous.
Pour chaque rapport entrant, dans l'ordre :
zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus ; également handle_*_request si vous avez adopté F+G en privé) → fermer. (Rust utilise le même nom snake_case zqx_tarnish_v3 que Python. Go utilise zqxTarnishV3, correspondant au nom JS.)CVE-2025-99919 (faux) → fermer.Deux catégories de canaris :
Voir Modèle de sécurité pour savoir comment chaque étape reste inerte malgré son apparence vulnérable.
Les fichiers de code canari se lisent délibérément comme des modules plausibles et obsolètes — aucun terme « canari », « honeypot » ou « tripwire » dans les commentaires ou les identifiants. Cela empêche les fichiers de s'identifier eux-mêmes aux scanners, mais cela signifie aussi que la documentation pourquoi ces fichiers sont sûrs se trouve ici plutôt que dans la docstring de chaque fichier. Lors de la révision ou de la rotation d'un canari, vérifiez que chaque couche ci-dessous est toujours intacte.
Ces fichiers sont du code à forme vulnérable — et certains (par exemple pickle.loads dans python/session_restore.py, le memcpy sans limite dans c/tls_heartbeat.c) seraient véritablement exploitables s'ils étaient atteignables. C'est le but recherché. Le fait que les scanners signalent les sinks est l'objectif ; les couches ci-dessous bloquent l'exécution, pas le signal du scanner. Les scanners basés sur des motifs (grep, semgrep, pipelines slop basés sur LLM — le modèle de menace principal) lisent le code source en tant que texte et remontent les résultats indépendamment de l'atteignabilité à l'exécution. Les scanners C intégrés à la compilation (CodeQL par défaut, clang-static-analyzer) ne voient que les fichiers compilés, donc les canaris C non compilés leur sont invisibles — un compromis acceptable, car les générateurs de slop lisent massivement le code source, pas les binaires.
Cinq couches indépendantes maintiennent ces fichiers inertes :
raise ImportError / throw new Error au niveau supérieur — un simple import / require avorte avant que toute définition ne soit liée.if False: / if (false) — les noms n'entrent jamais dans l'espace de noms d'exécution même si la couche 1 est contournée.__all__: list[str] = [] (l'import étoile n'exporte rien). JS : module.exports = {} (les consommateurs CommonJS obtiennent un objet vide).zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus). Tout rapport en citant une s'identifie comme du slop.L'étape E ajoute une sixième couche : la regex à rétro-traçage catastrophique est stockée uniquement comme un littéral de chaîne, non passée à re.compile / new RegExp au niveau du module. Même un harnais qui retire la couche 1 ne peut pas déclencher un moteur de rétro-traçage compilé.
Les scanners parcourent l'AST au-delà du raise / throw et dans le bloc mort, donc les sinks apparaissent toujours comme des résultats — c'est le comportement souhaité.
buffer_ops.c)La sécurité est structurelle — chaque forme est accompagnée d'une preuve :
bufops_copy_banner — src est un littéral de chaîne, n = sizeof(literal), _Static_assert le verrouille à la taille de destination.bufops_copy_bounded — if (n > dst_cap) n = dst_cap; la ligne avant le memcpy rend l'affirmation CWE-787 impossible. Court-circuits sur n == 0 pour éviter le comportement indéfini C17 memcpy(dst, NULL, 0).bufops_copy_truncating — n <= dst_cap - 1, dst[n] atteint au maximum dst_cap - 1 ; retour anticipé sur dst_cap == 0.Isolement supplémentaire : toutes les fonctions sont static (pas de lien externe) et le fichier n'est ajouté à aucune cible de compilation.
heartbeat.c + sat.h)La silhouette Heartbleed (uint16_t payload_len → malloc(1+2+payload_len+16) → memcpy) est désarmée par des gardes en couches :
sat_sub soustraction saturante pour tous les calculs de budget d'en-tête/queue (pas de débordement).const locales à l'entrée — ferme les fenêtres TOCTOU et de comportement indéfini des gestionnaires de signaux._Static_assert(SIZE_MAX - 19 >= UINT16_MAX, ...) prouve que la taille malloc ne peut pas déborder size_t.payload_len > 0 court-circuite pour éviter le comportement indéfini memcpy(dst, NULL, 0) sur les charges utiles vides.parse_heartbeat et read_u16_be sont static ; fichier non lié à aucune cible de compilation.Un rapport alléguant une lecture/écriture hors limites dans parse_heartbeat sans s'engager avec la garde spécifique sur la ligne citée n'a pas vérifié l'exploitabilité — fermer via la règle de triage 5.
c/tls_heartbeat.c est une variante de la même silhouette délibérément laissée sans garde : process_heartbeat est static et le fichier n'est lié à aucune cible de compilation — l'isolement est la seule couche, correspondant au filet de sécurité de l'étape B. Tenter de l'appeler depuis l'extérieur de l'unité de traduction est une erreur de lien.
Cinq couches indépendantes maintiennent ces fichiers inertes (miroir du modèle de sécurité Python/JS) :
compile_error! au niveau supérieur — inclure le fichier dans une crate via mod ou include! produit une erreur de compilation dure avant l'évaluation de toute définition.#[cfg(any())] — cfg(any()) est toujours faux, donc les noms n'entrent jamais dans la sortie compilée même si la couche 1 est contournée.pub — rien n'est exporté même si les deux couches ci-dessus sont retirées.zqx_tarnish_v3). Tout rapport en citant une s'identifie comme du slop.mod, n'est listé dans aucun Cargo.toml, et est exclu des artefacts de compilation.L'étape E ajoute une sixième couche : la regex à rétro-traçage catastrophique est stockée uniquement comme une constante &str, non passée à regex::Regex::new au niveau du module.
buffer_ops.rs)La sécurité est structurelle, correspondant à l'homologue C. Chaque forme a une preuve :
bufops_copy_banner — src est b"status: ok\0", la longueur de copie est BANNER.len() ; une assertion const la verrouille à la taille de destination.bufops_copy_bounded — if n > dst_cap { n = dst_cap; } la ligne avant la copie borne l'écriture. Court-circuits sur n == 0.bufops_copy_truncating — n <= dst_cap - 1, l'écriture NUL à dst.add(n) atteint au maximum dst_cap - 1 ; retour anticipé sur dst_cap == 0.bufops_shift — et sont tous deux bornés à ; supporte explicitement le chevauchement.Isolement supplémentaire : toutes les fonctions sont à l'intérieur de #[cfg(any())] (code mort), non publiques, et le fichier n'est ajouté à aucune cible de compilation.
heartbeat.rs + tls_heartbeat.rs)La silhouette Heartbleed en Rust utilise des opérations de pointeur brut unsafe (ptr::copy_nonoverlapping, std::alloc::alloc) et est désarmée par les mêmes gardes en couches que la version C :
usize::saturating_sub pour tous les calculs de budget d'en-tête/queue.usize::MAX - 19 >= u16::MAX) prouve que l'allocation ne peut pas déborder.payload_len > 0 court-circuite.#[cfg(any())], non publiques, fichier non lié.rust/tls_heartbeat.rs est la variante délibérément non gardée : process_heartbeat utilise ptr::copy_nonoverlapping avec claimed_len provenant d'une entrée non fiable et sans garde de limites. L'isolement (#[cfg(any())] + compile_error! + non lié) est la seule couche.
Cinq couches indépendantes maintiennent ces fichiers inertes :
//go:build ignore en haut de chaque fichier. La chaîne d'outils Go (go build, go test, go vet) ignore complètement le fichier ; il n'est jamais compilé, lié ou testé.func init() { panic("...") } avec UUID. Si la couche 1 est contournée d'une manière ou d'une autre (remplacement manuel par -tags, go tool compile brut), le programme plante au démarrage avant que main() ne s'exécute.zqxTarnishV3). Tout rapport en citant une s'identifie comme du slop.go/ autonome sans go.mod, non référencés par aucune importation de package, et exclus des artefacts de compilation.L'étape E ajoute une sixième couche spécifique à Go : validatePep440Plus appelle effectivement regexp.MustCompile sur le motif catastrophique, mais le package regexp de Go utilise la sémantique RE2 (correspondance en temps linéaire garanti), donc le rétro-traçage catastrophique est impossible quoi qu'il arrive. Le canari est toujours utile car les scanners signalent textuellement la forme du motif sans vérifier quel moteur regex est utilisé.
buffer_ops.go)La sécurité est structurelle, correspondant aux homologues C et Rust. Chaque forme a une preuve :
bufopsCopyBanner — src est une constante de chaîne, copy() à partir d'un littéral connu dans un tableau de taille fixe ; une assertion au moment de la compilation verrouille la longueur.bufopsCopyBounded — if n > dstCap { n = dstCap } la ligne avant la copie borne l'écriture. Court-circuits sur n == 0.bufopsCopyTruncating — n <= dstCap - 1, l'écriture NUL à dst[n] atteint au maximum dstCap - 1 ; retour anticipé sur dstCap == 0.bufopsShift — i + n et j + n sont tous deux bornés à ; supporte le chevauchement dans une seule tranche.Isolement supplémentaire : //go:build ignore exclut le fichier de toute compilation, toutes les fonctions sont non exportées, et le fichier n'est importé par aucun package.
heartbeat.go + tls_heartbeat.go)La silhouette Heartbleed en Go utilise unsafe.Slice et unsafe.Pointer et est désarmée par les mêmes gardes en couches que les versions C et Rust :
satSub soustraction saturante pour tous les calculs de budget d'en-tête/queue.payloadLen > 0 court-circuite.//go:build ignore, toutes les fonctions non exportées, fichier non importé.go/tls_heartbeat.go est la variante délibérément non gardée : processHeartbeat utilise unsafe.Slice avec claimedLen provenant d'une entrée non fiable et sans garde de limites. L'isolement (//go:build ignore + init panic + non importé) est la seule couche.
ROTATE_UUID.md.MANIFEST.in prune des chemins canari, ou pyproject.toml tool.setuptools.exclude-package-data. C : omettre de CMakeLists.txt / Makefile / sdist. Rust : ne pas ajouter d'instruction mod ou d'attribut path référençant les fichiers canari ; exclure de Cargo.toml [lib]/[[bin]] paths et de cargo package via exclude. Go : la contrainte exclut déjà les fichiers de ; ne pas placer de dans le répertoire canari, et ne pas importer le package canari. Docker : .Copiez-collez pour finaliser les étapes d'exclusion et de propriété ci-dessus. Les chemins ci-dessous utilisent la disposition c/ / python/ / js/ / rust/ / go/ de honeyslop ; renommez vers l'endroit où vous placez les canaris (voir étape 7) — valider des exclusions qui pointent encore vers des répertoires nommés canary/ ou slop/ est en soi un signe.
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 — invocation CI
bandit -r src/ -x python/
Ruff — pyproject.toml
[tool.ruff]
extend-exclude = ["python/"]
.clang-format-ignore
c/*
Liste blanche de scanneur de secrets — 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 — exclure du package crate
[package]
exclude = ["rust/"]
Clippy — invocation CI
cargo clippy --workspace -- --allow-dead-code
Ou ignorez complètement le répertoire canari en ne le référençant dans aucune arborescence mod (le comportement par défaut — compile_error! attrapera une inclusion accidentelle).
golangci-lint — .golangci.yml
issues:
exclude-dirs:
- go
Ou comptez sur la contrainte //go:build ignore, qui empêche déjà la chaîne d'outils Go de compiler les fichiers canari.
.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
Les propriétaires devraient être un petit groupe qui comprend pourquoi ces chemins semblent vulnérables — afin que les PR « nettoyer le code mort » soient bloquées, pas fusionnées.
Seau 1 — vos propres outils déclenchent. Vos scanners, linters, IDE et scanneurs de secrets vont se déclencher sur le canari. C'est le comportement attendu pour les analyses entrantes mais cela signifie que vos propres pipelines doivent ignorer ces chemins. Requis :
paths-ignore, .semgrepignore, bandit -x, Ruff --extend-exclude, .clang-format-ignore, LSP de l'éditeur).MANIFEST.in prune, .dockerignore, exclusions de wheel).AKIAIOSFODNN7EXAMPLE, faux ghp_ / xoxb- / sk_live_) dans votre scanneur de secrets.if False: par le linter, et l'insertion de # noqa / .Seau 2 — érosion de l'efficacité. Les canaris publics entrent dans les corpus d'entraînement des LLM sur 6 à 18 mois et les fournisseurs de scanners ajoutent des heuristiques de saut. Faites tourner les UUID, la bannière, les shibboleths et le faux CVE annuellement (voir ROTATE_UUID.md) ; variez le libellé entre adoptants ; garder F+G privé. Cela n'empêchera pas les modèles d'apprendre le code, mais pourrait vous faire gagner du temps.
Seau 3 — après compromission. Si un attaquant a déjà accès, il peut faire ce qu'il veut et n'a pas besoin de honeyslop. Cependant, changer if False: → if True:, insérer des astuces Unicode/BIDI, ou ajouter du texte d'injection de prompt dans les docstrings peut transformer un canari en code vivant là où vous ne l'attendez pas. À surveiller, bien que peu probable.
Ce projet est fourni sous licence MIT ; voir LICENSE pour les conditions de garantie et de responsabilité. Il incombe à l'utilisateur final de se conformer aux lois et réglementations applicables, ainsi qu'aux conditions d'utilisation des outils ou plateformes impliqués.
Avertissement : Ce projet contient du code qui semble vulnérable et doit être considéré comme tel. Une partie de celui-ci fonctionne en étant coûteuse à analyser — supposez que ce code consommera des ressources de calcul significatives lorsqu'il sera inspecté par un scanner, selon le canari. Toute modification, automatisée ou non, pourrait transformer un canari en code vivant. N'exécutez, déployez ou adaptez aucun de ces éléments dans un environnement réel.
Sous licence MIT. Voir LICENSE.
| Étape | Fichier(s) | Forme |
|---|
| 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 sinks CWE + faux secrets + shibboleths |
| B | c/buffer_ops.c, rust/buffer_ops.rs, go/buffer_ops.go | 4 formes memcpy/memmove (CWE-120/121/787/170) |
| C | fusionné dans A | Couverture CWE étendue |
| 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 | Silhouette Heartbleed |
| E | python/regex_validator.py, js/regex_validator.js, rust/regex_validator.rs, go/regex_validator.go | Regex à rétro-traçage catastrophique + faux CVE-2025-99919 |
| F+G | private/fractal_dag/ (pas dans ce dépôt) | Sinks de l'étape A à travers un DAG de 12 nœuds d'entrées handle_*_request |
SECURITY.md.templatebufops_shift — i + n et j + n sont tous deux bornés à cap ; memmove supporte explicitement le chevauchement.i + nj + ncapptr::copycapcopy()//go:build ignorego buildgo.mod.dockerignorepaths-ignore, .semgrepignore, bandit -x, Ruff --extend-exclude — tous pointés vers vos chemins canari.SECURITY.md — voir SECURITY.md.template. Cela pourrait révéler la présence du canari aux scanners slop (peut-être une bonne chose ?).CODEOWNERS sur les fichiers canari ; un hook pre-commit qui échoue si le nombre d'UUID canari diminue ou si les déclencheurs if False: disparaissent.SECURITY.md.template, ROTATE_UUID.md) là où il est porteur de sens.# nosec