
Canary di codice per smistare rapidamente le segnalazioni di vulnerabilità allucinate ('slop')
honeyslop è un insieme di canary di codice, esche, per progetti open-source sommersi da report di vulnerabilità allucinati dall'IA ("slop") e non verificati. Con questa iniezione di rumore avversario, uno scanner slop ingerisce il canary e poi genera un "report" di vulnerabilità basato su di esso. Il report si auto-identifica come slop. Lo chiudi con una sola grep.
Questo è un PoC veloce, vibe-coded come scherzo (non di livello produzione), perché anche noi abbiamo ricevuto un report slop su raptor, un agente autonomo di attacco/difesa basato su Claude Code. Dovrebbe essere divertente!
I canary di codice estendono i segnali di triage familiari (ad es. rilevamenti in file di test, segreti di esempio, percorsi inesistenti) trasformandoli in marcatori deliberati, o esche. Nei test, questi canary funzionano abbastanza bene da segnalare lo slop, ma possono essere ulteriormente migliorati (incorporati nel codice reale, con nomi di funzioni/file/directory meno indicativi, rigenerati regolarmente come nuovo codice, ecc.).
Scritto da: Gadi Evron (@gadievron), John Cartwright (@grokjc), Daniel Cuthbert (@danielcuthbert) e Michal Kamensky (@kamenskymic, con merito per aver dato il nome al progetto).
Usalo a tuo rischio. Se lo incolli in produzione, è un problema tuo. Vedi Disclaimer sotto.
Per ogni report in arrivo, in ordine:
zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus; anche handle_*_request se hai adottato F+G privatamente) → chiudi. (Rust usa lo stesso nome snake_case zqx_tarnish_v3 di Python. Go usa zqxTarnishV3, come il nome JS.)CVE-2025-99919 (fittizio) → chiudi.Due categorie di canary:
| Fase | File(s) | Forma |
|---|---|---|
| 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 sink CWE + segreti fittizi + shibboleth |
| B | c/buffer_ops.c, rust/buffer_ops.rs, go/buffer_ops.go | 4 forme memcpy/memmove (CWE-120/121/787/170) |
| C | fusa in A | Resa CWE estesa |
| 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 | Sagoma Heartbleed |
| E | python/regex_validator.py, js/regex_validator.js, rust/regex_validator.rs, go/regex_validator.go | Regex con backtracking catastrofico + falso CVE-2025-99919 |
| F+G | private/fractal_dag/ (non in questo repo) | Sink di Fase A distribuiti su un DAG a 12 nodi di voci handle_*_request |
Vedi Safety model per come ogni fase rimane inerte pur apparendo vulnerabile.
I file di codice canary sono volutamente leggibili come plausibili moduli deprecati — nessun linguaggio "canary", "honeypot" o "tripwire" nei commenti o negli identificatori. Questo impedisce ai file di auto-identificarsi davanti agli scanner, ma significa anche che la documentazione sul perché questi file sono sicuri vive qui invece che nel docstring di ciascun file. Quando rivedi o ruoti un canary, verifica che ogni livello sottostante sia ancora intatto.
Questi file sono codice dalla forma vulnerabile — e alcuni (ad es. pickle.loads in python/session_restore.py, la memcpy senza limiti in c/tls_heartbeat.c) sarebbero davvero sfruttabili se raggiungibili. È il design. Il fatto che gli scanner segnalino i sink è il punto centrale; i livelli sottostanti bloccano l'esecuzione, non il segnale per lo scanner. Gli scanner basati su pattern (grep, semgrep, pipeline slop basate su LLM — il modello di minaccia principale) leggono il sorgente come testo e mostrano i risultati indipendentemente dalla raggiungibilità a runtime. Gli scanner C integrati nella build (CodeQL di default, clang-static-analyzer) vedono solo i file compilati, quindi i canary C non compilati sono invisibili per loro — un compromesso accettato, dato che i generatori di slop leggono in prevalenza il sorgente, non le build.
Cinque livelli indipendenti mantengono inerti questi file:
raise ImportError / throw new Error di livello top-level — una semplice import / require si interrompe prima che qualsiasi definizione venga vincolata.if False: / if (false) — i nomi non entrano mai nel namespace runtime anche se il livello 1 viene bypassato.__all__: list[str] = [] (lo star-import non esporta nulla). JS: module.exports = {} (i consumer CommonJS ricevono un oggetto vuoto).zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus). Qualsiasi report che ne citi una si auto-identifica come slop.SECURITY.md.template e How to try).La Fase E aggiunge un sesto livello: la regex con backtracking catastrofico è conservata solo come stringa letterale, non passata a re.compile / new RegExp a livello di modulo. Anche un harness che rimuove il livello 1 non può innescare un motore di backtracking compilato.
Gli scanner attraversano l'AST oltre raise / throw e dentro il blocco morto, quindi i sink emergono comunque come segnalazioni — è il comportamento previsto.
buffer_ops.c)La sicurezza è strutturale — ogni forma ha una dimostrazione accanto:
bufops_copy_banner — src è un literal stringa, n = sizeof(literal), _Static_assert lo fissa alla dimensione della destinazione.bufops_copy_bounded — if (n > dst_cap) n = dst_cap; la riga prima della memcpy rende impossibile l'affermazione CWE-787. Si interrompe su n == 0 per evitare la UB memcpy(dst, NULL, 0) del C17.bufops_copy_truncating — n <= dst_cap - 1, dst[n] arriva al massimo a dst_cap - 1; return anticipato su dst_cap == 0.bufops_shift — sia i + n che j + n sono limitati a cap; memmove supporta esplicitamente la sovrapposizione.Isolamento aggiuntivo: tutte le funzioni sono static (nessun collegamento esterno) e il file non è aggiunto ad alcun target di build.
heartbeat.c + sat.h)