
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:
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.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.Isolamento aggiuntivo: tutte le funzioni sono static (nessun collegamento esterno) e il file non è aggiunto ad alcun target di build.
heartbeat.c + sat.h)La sagoma Heartbleed (uint16_t payload_len → malloc(1+2+payload_len+16) → memcpy) è resa innocua da guardie a livelli:
sat_sub sottrazione saturante per tutti i calcoli di budget header/trailer (niente wrap).const all'ingresso — chiude le finestre TOCTOU e di UB da signal handler._Static_assert(SIZE_MAX - 19 >= UINT16_MAX, ...) dimostra che la dimensione della malloc non può far overflow di size_t.payload_len > 0 evita la UB memcpy(dst, NULL, 0) su payload vuoti.parse_heartbeat e read_u16_be sono static; il file non è collegato ad alcun target di build.Un report che sostiene una lettura/scrittura OOB in parse_heartbeat senza confrontarsi con la guardia specifica sulla riga citata non ha verificato lo sfruttamento — chiudi tramite la regola di triage 5.
c/tls_heartbeat.c è una variazione sulla stessa sagoma, volutamente lasciata senza guardie: process_heartbeat è static e il file non è collegato ad alcun target di build — l'isolamento è l'unico livello, come nel catch-all della Fase B. Qualsiasi tentativo di chiamarla dall'esterno della TU è un errore di link.
Cinque livelli indipendenti mantengono inerti questi file (rispecchiando il modello di sicurezza di Python/JS):
compile_error! di livello top-level — includere il file in una crate tramite mod o include! produce un errore del compilatore prima che qualsiasi definizione venga valutata.#[cfg(any())] — cfg(any()) è sempre falso, quindi i nomi non entrano mai nell'output compilato anche se il livello 1 viene bypassato.pub — nulla viene esportato anche se entrambi i livelli sopra vengono rimossi.zqx_tarnish_v3). Qualsiasi report che la citi si auto-identifica come slop.mod, non è elencato in alcun Cargo.toml ed è escluso dagli artefatti di build.La Fase E aggiunge un sesto livello: la regex con backtracking catastrofico è conservata solo come costante &str, non passata a regex::Regex::new a livello di modulo.
buffer_ops.rs)La sicurezza è strutturale, come nella controparte C. Ogni forma ha una dimostrazione:
bufops_copy_banner — src è b"status: ok\0", la lunghezza della copia è BANNER.len(); un'asserzione const la fissa alla dimensione della destinazione.bufops_copy_bounded — if n > dst_cap { n = dst_cap; } la riga prima della copia limita la scrittura. Si interrompe su n == 0.bufops_copy_truncating — n <= dst_cap - 1, la scrittura del NUL in dst.add(n) arriva al massimo a dst_cap - 1; return anticipato su dst_cap == 0.bufops_shift — sia che sono limitati a ; supporta esplicitamente la sovrapposizione.Isolamento aggiuntivo: tutte le funzioni sono dentro #[cfg(any())] (codice morto), non pubbliche, e il file non è aggiunto ad alcun target di build.
heartbeat.rs + tls_heartbeat.rs)La sagoma Heartbleed in Rust usa operazioni su puntatori grezzi unsafe (ptr::copy_nonoverlapping, std::alloc::alloc) ed è resa innocua dalle stesse guardie a livelli della versione C:
usize::saturating_sub per tutti i calcoli di budget header/trailer.usize::MAX - 19 >= u16::MAX) dimostra che l'allocazione non può andare in overflow.payload_len > 0.#[cfg(any())], non pubbliche, file non collegato.rust/tls_heartbeat.rs è la variante volutamente senza guardie: process_heartbeat usa ptr::copy_nonoverlapping con claimed_len proveniente da input non attendibile e senza guardia sui limiti. L'isolamento (#[cfg(any())] + compile_error! + non collegato) è l'unico livello.
Cinque livelli indipendenti mantengono inerti questi file:
//go:build ignore in cima a ogni file. La toolchain Go (go build, go test, go vet) salta completamente il file; non viene mai compilato, collegato o testato.func init() { panic("...") } con UUID. Se il livello 1 viene in qualche modo bypassato (override manuale di -tags, go tool compile diretto), il programma va in crash all'avvio prima che main() venga eseguito.zqxTarnishV3). Qualsiasi report che la citi si auto-identifica come slop.go/ autonoma senza go.mod, non referenziati da alcun import di pacchetto ed esclusi dagli artefatti di build.La Fase E aggiunge un sesto livello specifico di Go: validatePep440Plus chiama effettivamente regexp.MustCompile sul pattern catastrofico, ma il pacchetto regexp di Go usa la semantica RE2 (matching a tempo lineare garantito), quindi il backtracking catastrofico è comunque impossibile. Il canary è comunque utile perché gli scanner segnalano la forma del pattern testualmente senza controllare quale motore regex è in uso.
buffer_ops.go)La sicurezza è strutturale, come nelle controparti C e Rust. Ogni forma ha una dimostrazione:
bufopsCopyBanner — src è una costante stringa, copy() da un literal noto in un array a dimensione fissa; un'asserzione a compile-time fissa la lunghezza.bufopsCopyBounded — if n > dstCap { n = dstCap } la riga prima della copia limita la scrittura. Si interrompe su n == 0.bufopsCopyTruncating — n <= dstCap - 1, la scrittura del NUL in dst[n] arriva al massimo a dstCap - 1; return anticipato su dstCap == 0.bufopsShift — sia i + n che j + n sono limitati a ; supporta la sovrapposizione all'interno di un'unica slice.Isolamento aggiuntivo: //go:build ignore esclude il file da tutte le build, tutte le funzioni sono non esportate e il file non è importato da alcun pacchetto.
heartbeat.go + tls_heartbeat.go)La sagoma Heartbleed in Go usa unsafe.Slice e unsafe.Pointer ed è resa innocua dalle stesse guardie a livelli delle versioni C e Rust:
satSub sottrazione saturante per tutti i calcoli di budget header/trailer.payloadLen > 0.//go:build ignore, tutte le funzioni non esportate, file non importato.go/tls_heartbeat.go è la variante volutamente senza guardie: processHeartbeat usa unsafe.Slice con claimedLen proveniente da input non attendibile e senza guardia sui limiti. L'isolamento (//go:build ignore + panic in init + non importato) è l'unico livello.
ROTATE_UUID.md.MANIFEST.in prune sui percorsi dei canary, oppure pyproject.toml tool.setuptools.exclude-package-data. C: ometti da CMakeLists.txt / Makefile / sdist. Rust: non aggiungere una dichiarazione mod o un attributo path che referenzi i file canary; escludi dai percorsi [lib]/[[bin]] di Cargo.toml e da cargo package tramite . Go: il vincolo esclude già i file da ; non inserire un nella directory dei canary e non importare il pacchetto canary. Docker: .Copia-incolla per chiudere i passaggi di esclusione e proprietà sopra. I percorsi seguenti usano la struttura c/ / python/ / js/ / rust/ / go/ di honeyslop; rinomina in base al punto in cui collochi i canary (vedi passaggio 7) — committare esclusioni che puntano ancora a directory chiamate canary/ o slop/ è di per sé un indizio.
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 — invocazione CI
bandit -r src/ -x python/
Ruff — pyproject.toml
[tool.ruff]
extend-exclude = ["python/"]
.clang-format-ignore
c/*
Allowlist dello scanner di segreti — 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 — esclusione dal pacchetto della crate
[package]
exclude = ["rust/"]
Clippy — invocazione CI
cargo clippy --workspace -- --allow-dead-code
Oppure salta del tutto la directory dei canary non referenziandola in alcun albero mod (il default — compile_error! intercetterà un'inclusione accidentale).
golangci-lint — .golangci.yml
issues:
exclude-dirs:
- go
Oppure affidati al vincolo //go:build ignore, che impedisce già alla toolchain Go di compilare i file canary.
.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
Gli owner dovrebbero essere un piccolo gruppo che capisce perché questi percorsi sembrano vulnerabili — così le PR di "pulizia del codice morto" vengono bloccate, non merged.
Bucket 1 — i tuoi stessi strumenti inciampano. I tuoi scanner, linter, IDE e la scansione dei segreti si attiveranno sul canary. È il comportamento voluto per le scansioni in arrivo, ma significa che le tue stesse pipeline devono saltare questi percorsi. Obbligatorio:
paths-ignore, .semgrepignore, bandit -x, Ruff --extend-exclude, .clang-format-ignore, LSP dell'editor).MANIFEST.in prune, .dockerignore, esclusioni wheel).AKIAIOSFODNN7EXAMPLE, ghp_ / xoxb- / sk_live_ fittizi) nel tuo scanner di segreti.if False: e all'inserimento di # noqa / .Bucket 2 — erosione dell'efficacia. I canary pubblici entrano nei corpora di addestramento degli LLM nell'arco di 6–18 mesi e i vendor degli scanner aggiungono euristiche di skip. Ruota UUID, banner, shibboleth e il falso CVE ogni anno (vedi ROTATE_UUID.md); varia la formulazione tra gli adottanti; mantieni F+G private. Non fermerà i modelli dall'imparare il codice, ma potrebbe farti guadagnare un po' di tempo.
Bucket 3 — post-compromissione. Se un attaccante ha già accesso, può fare qualsiasi cosa e non ha bisogno di honeyslop. Tuttavia, trasformare if False: → if True:, inserire trucchi Unicode/BIDI o aggiungere testo di prompt injection nei docstring può rendere attivo un canary dove non te lo aspetti. Vale la pena di tenerlo d'occhio, anche se improbabile.
Questo progetto è fornito sotto la licenza MIT; vedi LICENSE per i termini di garanzia e responsabilità. È responsabilità dell'utente finale rispettare leggi e regolamenti applicabili, nonché i termini di servizio di eventuali strumenti o piattaforme coinvolti.
Avviso: questo progetto contiene codice che sembra vulnerabile, e va considerato tale. Parte di esso funziona perché è costoso da analizzare — supponi che questo codice consumerà risorse di calcolo significative quando uno scanner lo ispeziona, a seconda del canary. Qualsiasi modifica, automatizzata o meno, potrebbe rendere attivo un canary. Non eseguire, distribuire o adattare nulla di tutto ciò in un ambiente reale.
Concesso in licenza MIT. Vedi LICENSE.
| 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 |
SECURITY.md.templatebufops_shift — sia i + n che j + n sono limitati a cap; memmove supporta esplicitamente la sovrapposizione.i + nj + ncapptr::copycapcopy()exclude//go:build ignorego buildgo.mod.dockerignorepaths-ignore, .semgrepignore, bandit -x, Ruff --extend-exclude — tutti puntati ai tuoi percorsi canary.SECURITY.md — vedi SECURITY.md.template. Questo potrebbe rivelare agli scanner slop la presenza del canary (forse è una buona cosa?).CODEOWNERS sui file canary; un hook pre-commit che fallisce se il conteggio degli UUID canary diminuisce o se spariscono i tripwire if False:.SECURITY.md.template, ROTATE_UUID.md) dove è portante.# nosec