
Canários de código para agilizar a triagem de relatórios de vulnerabilidade alucinados ('slop')
honeyslop são canários de código, iscas, para projetos open-source afogados em relatórios de vulnerabilidade alucinados por IA ("slop") e não verificados. Com essa injeção adversarial de ruído, um scanner de slop ingere o canário e então gera um "relatório" de vulnerabilidade baseado nele. O relatório se autoidentifica como slop. Encerre-o com um único grep.
Este é um PoC rápido, feito no improviso como brincadeira (não é de nível de produção), porque nós mesmos recebemos um relatório slop no raptor, um agente autônomo de ataque/defesa baseado no Claude Code. Deve ser divertido!
Os canários de código estendem sinais familiares de triagem (por exemplo, detecções em arquivos de teste, segredos de exemplo, caminhos inexistentes) até marcadores deliberados, ou iscas. Em testes, esses canários funcionam bem o suficiente para sinalizar slop, mas podem ser melhorados ainda mais (embutidos em código real, nomes de funções/arquivos/diretórios menos sugestivos, regenerados regularmente como código novo, etc.).
Escrito por: Gadi Evron (@gadievron), John Cartwright (@grokjc), Daniel Cuthbert (@danielcuthbert) e Michal Kamensky (@kamenskymic, com créditos por nomear o projeto).
Use por sua conta e risco. Se você colar isso em produção, o problema é seu. Veja Disclaimer abaixo.
Para cada relatório recebido, em ordem:
zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus; também handle_*_request se você adotou F+G de forma privada) → encerrar. (Rust usa o mesmo nome snake_case zqx_tarnish_v3 que Python. Go usa zqxTarnishV3, igual ao nome em JS.)CVE-2025-99919 (falso) → encerrar.Duas categorias de canário:
| Estágio | Arquivo(s) | Formato |
|---|---|---|
| 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 + segredos falsos + shibboleths |
| B | c/buffer_ops.c, rust/buffer_ops.rs, go/buffer_ops.go | 4 formatos de memcpy/memmove (CWE-120/121/787/170) |
| C | mesclado em A | Rendimento estendido de 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 | Silhueta Heartbleed |
| E | python/regex_validator.py, js/regex_validator.js, rust/regex_validator.rs, go/regex_validator.go | Regex de retrocesso catastrófico + CVE-2025-99919 falso |
| F+G | private/fractal_dag/ (não está neste repositório) | sinks do Estágio A em um DAG de 12 nós de entradas handle_*_request |
Veja Modelo de segurança sobre como cada estágio permanece inerte apesar de parecer vulnerável.
Os arquivos de código canário são deliberadamente escritos como módulos obsoletos plausíveis — sem linguagem de "canário", "honeypot" ou "tripwire" em comentários ou identificadores. Isso impede que os arquivos se autoidentifiquem para scanners, mas também significa que a documentação do por que esses arquivos são seguros vive aqui, em vez de no docstring de cada arquivo. Ao revisar ou rotacionar um canário, verifique se todas as camadas abaixo ainda estão intactas.
Estes arquivos são código com formato vulnerável — e alguns (por exemplo, o pickle.loads de python/session_restore.py, o memcpy sem limites de c/tls_heartbeat.c) seriam genuinamente exploráveis se alcançáveis. Esse é o design. Scanners sinalizando os sinks é exatamente o objetivo; as camadas abaixo bloqueiam a execução, não o sinal do scanner. Scanners baseados em padrão (grep, semgrep, pipelines de slop baseados em LLM — o modelo de ameaça primário) leem o código-fonte como texto e exibem achados independentemente da alcançabilidade em tempo de execução. Scanners C integrados à compilação (CodeQL padrão, clang-static-analyzer) só veem arquivos compilados, portanto canários C não compilados são invisíveis para eles — um trade-off aceito, já que os geradores de slop leem esmagadoramente o código-fonte, não os builds.
Cinco camadas independentes mantêm esses arquivos inertes:
raise ImportError / throw new Error no nível superior — um import / require simples aborta antes que qualquer definição seja vinculada.if False: / if (false) — os nomes nunca entram no namespace em tempo de execução, mesmo que a camada 1 seja contornada.__all__: list[str] = [] (star-import não exporta nada). JS: module.exports = {} (consumidores CommonJS recebem um objeto vazio).zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus). Qualquer relatório que cite uma delas se autoidentifica como slop.SECURITY.md.template e Como testar).O Estágio E adiciona uma sexta camada: a regex de retrocesso catastrófico é armazenada apenas como literal de string, não passada para re.compile / new RegExp no escopo do módulo. Mesmo um harness que remova a camada 1 não consegue disparar um motor de backtracking compilado.
Os scanners percorrem a AST além do raise / throw e entram no bloco morto, então os sinks ainda aparecem como achados — esse é o comportamento pretendido.
buffer_ops.c)A segurança é estrutural — cada formato tem uma prova ao lado:
bufops_copy_banner — src é um literal de string, n = sizeof(literal), _Static_assert o fixa no tamanho do destino.bufops_copy_bounded — if (n > dst_cap) n = dst_cap; a linha antes do memcpy torna a alegação de CWE-787 impossível. Faz curto-circuito em n == 0 para evitar UB de C17 memcpy(dst, NULL, 0).bufops_copy_truncating — n <= dst_cap - 1, dst[n] atinge no máximo dst_cap - 1; retorno antecipado em dst_cap == 0.bufops_shift — tanto i + n quanto j + n limitados a cap; memmove suporta explicitamente sobreposição.Isolamento adicional: todas as funções são static (sem vinculação externa) e o arquivo não é adicionado a nenhum alvo de build.
heartbeat.c + sat.h)A silhueta Heartbleed (uint16_t payload_len → malloc(1+2+payload_len+16) → memcpy) é desarmada por proteções em camadas: