
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:
sat_sub subtração saturante para toda a matemática de orçamento de cabeçalho/rodapé (sem estouro).const na entrada — fecha as janelas de TOCTOU e UB em handler de sinal._Static_assert(SIZE_MAX - 19 >= UINT16_MAX, ...) prova que o tamanho do malloc não pode estourar size_t.payload_len > 0 evita UB de memcpy(dst, NULL, 0) em payloads vazios.parse_heartbeat e read_u16_be são static; arquivo não vinculado a nenhum alvo de build.Um relatório que alegue leitura/escrita OOB em parse_heartbeat sem lidar com a proteção específica na linha citada não verificou a explorabilidade — encerre pela regra 5 de triagem.
c/tls_heartbeat.c é uma variação da mesma silhueta mantida deliberadamente sem proteção: process_heartbeat é static e o arquivo não é vinculado a nenhum alvo de build — o isolamento é a única camada, acompanhando a regra geral do Estágio B. Tentar chamá-lo de fora da TU é um erro de link.
Cinco camadas independentes mantêm esses arquivos inertes (espelhando o modelo de segurança do Python/JS):
compile_error! no nível superior — incluir o arquivo em um crate via mod ou include! produz um erro grave de compilação antes que qualquer definição seja avaliada.#[cfg(any())] — cfg(any()) é sempre falso, então os nomes nunca entram na saída compilada, mesmo que a camada 1 seja contornada.pub — nada é exportado, mesmo que as duas camadas acima sejam removidas.zqx_tarnish_v3). Qualquer relatório que a cite se autoidentifica como slop.mod, não é listado em nenhum Cargo.toml e é excluído dos artefatos de build.O Estágio E adiciona uma sexta camada: a regex de retrocesso catastrófico é armazenada apenas como uma constante &str, não passada para regex::Regex::new no escopo do módulo.
buffer_ops.rs)A segurança é estrutural, acompanhando a contraparte em C. Cada formato tem uma prova:
bufops_copy_banner — src é b"status: ok\0", o comprimento da cópia é BANNER.len(); uma asserção const o fixa no tamanho do destino.bufops_copy_bounded — if n > dst_cap { n = dst_cap; } a linha antes da cópia limita a escrita. Faz curto-circuito em n == 0.bufops_copy_truncating — n <= dst_cap - 1, a escrita de NUL em dst.add(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; ptr::copy suporta explicitamente sobreposição.Isolamento adicional: todas as funções estão dentro de #[cfg(any())] (código morto), não são públicas e o arquivo não é adicionado a nenhum alvo de build.
heartbeat.rs + tls_heartbeat.rs)A silhueta Heartbleed em Rust usa operações de ponteiro bruto unsafe (ptr::copy_nonoverlapping, std::alloc::alloc) e é desarmada pelas mesmas proteções em camadas da versão em C:
usize::saturating_sub para toda a matemática de orçamento de cabeçalho/rodapé.usize::MAX - 19 >= u16::MAX) prova que a alocação não pode estourar.payload_len > 0.#[cfg(any())], não públicas, arquivo não vinculado.rust/tls_heartbeat.rs é a variante deliberadamente sem proteção: process_heartbeat usa ptr::copy_nonoverlapping com claimed_len de entrada não confiável e sem proteção de limites. O isolamento (#[cfg(any())] + compile_error! + não vinculado) é a única camada.
Cinco camadas independentes mantêm esses arquivos inertes:
//go:build ignore no topo de cada arquivo. A toolchain Go (go build, go test, go vet) ignora o arquivo completamente; ele nunca é compilado, vinculado ou testado.func init() { panic("...") } com UUID. Se a camada 1 for de alguma forma contornada (substituição manual de -tags, go tool compile cru), o programa quebra na inicialização antes de main() rodar.zqxTarnishV3). Qualquer relatório que a cite se autoidentifica como slop.go/ independente, sem go.mod, não referenciados por nenhum import de pacote e excluídos dos artefatos de build.O Estágio E adiciona uma sexta camada específica para Go: validatePep440Plus de fato chama regexp.MustCompile no padrão catastrófico, mas o pacote regexp do Go usa semântica RE2 (correspondência garantida em tempo linear), então o retrocesso catastrófico é impossível de qualquer forma. O canário ainda é útil porque os scanners sinalizam o formato do padrão textualmente, sem verificar qual motor de regex está em uso.
buffer_ops.go)A segurança é estrutural, acompanhando as contrapartes em C e Rust. Cada formato tem uma prova:
bufopsCopyBanner — src é uma constante de string, copy() de um literal conhecido para um array de tamanho fixo; uma asserção em tempo de compilação fixa o comprimento.bufopsCopyBounded — if n > dstCap { n = dstCap } a linha antes da cópia limita a escrita. Faz curto-circuito em n == 0.bufopsCopyTruncating — n <= dstCap - 1, a escrita de NUL em dst[n] atinge no máximo dstCap - 1; retorno antecipado em dstCap == 0.bufopsShift — tanto i + n quanto j + n limitados a cap; copy() suporta sobreposição dentro de um único slice.Isolamento adicional: //go:build ignore exclui o arquivo de todos os builds, todas as funções não são exportadas e o arquivo não é importado por nenhum pacote.
heartbeat.go + tls_heartbeat.go)A silhueta Heartbleed em Go usa unsafe.Slice e unsafe.Pointer e é desarmada pelas mesmas proteções em camadas das versões em C e Rust:
satSub subtração saturante para toda a matemática de orçamento de cabeçalho/rodapé.payloadLen > 0.//go:build ignore, todas as funções não exportadas, arquivo não importado.go/tls_heartbeat.go é a variante deliberadamente sem proteção: processHeartbeat usa unsafe.Slice com claimedLen de entrada não confiável e sem proteção de limites. O isolamento (//go:build ignore + panic no init + não importado) é a única camada.
ROTATE_UUID.md.MANIFEST.in prune nos caminhos dos canários, ou pyproject.toml tool.setuptools.exclude-package-data. C: omita de CMakeLists.txt / Makefile / sdist. Rust: não adicione uma declaração mod ou atributo path referenciando os arquivos canário; exclua dos caminhos [lib]/[[bin]] do Cargo.toml e do cargo package via exclude. Go: a restrição //go:build ignore já exclui os arquivos do go build; não coloque um go.mod no diretório do canário e não importe o pacote canário. Docker: .dockerignore.paths-ignore, .semgrepignore, bandit -x, Ruff --extend-exclude — todos apontando para os caminhos dos seus canários.SECURITY.md — veja SECURITY.md.template. Isso pode fazer os scanners de slop perceberem a presença do canário (talvez algo bom?).CODEOWNERS nos arquivos canário; um hook de pre-commit que falhe se o número de UUIDs de canário diminuir ou se os tripwires if False: desaparecerem.SECURITY.md.template, ROTATE_UUID.md) onde ela é essencial.Copie e cole para fechar as etapas de exclusão e propriedade acima. Os caminhos abaixo usam a estrutura c/ / python/ / js/ / rust/ / go/ do honeyslop; renomeie para onde você colocar os canários (veja o passo 7) — fazer commit de exclusões que ainda apontam para diretórios chamados canary/ ou slop/ já é uma pista por si só.
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 — invocação na CI
bandit -r src/ -x python/
Ruff — pyproject.toml
[tool.ruff]
extend-exclude = ["python/"]
.clang-format-ignore
c/*
Allowlist do scanner de segredos — 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 — excluir do pacote do crate
[package]
exclude = ["rust/"]
Clippy — invocação na CI
cargo clippy --workspace -- --allow-dead-code
Ou pule o diretório do canário completamente, não o referenciando em nenhuma árvore mod (o padrão — compile_error! vai capturar inclusões acidentais).
golangci-lint — .golangci.yml
issues:
exclude-dirs:
- go
Ou confie na restrição //go:build ignore, que já impede a toolchain Go de compilar os arquivos canário.
.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
Os responsáveis devem ser um grupo pequeno que entenda por que esses caminhos parecem vulneráveis — para que PRs de "limpeza de código morto" sejam bloqueados, não mesclados.
Bucket 1 — suas próprias ferramentas disparam. Seus scanners, linters, IDE e varredura de segredos vão disparar no canário. Esse é o comportamento pretendido para varreduras recebidas, mas significa que seus próprios pipelines precisam ignorar esses caminhos. Necessário:
paths-ignore, .semgrepignore, bandit -x, Ruff --extend-exclude, .clang-format-ignore, LSP do editor).MANIFEST.in prune, .dockerignore, exclusões de wheel).AKIAIOSFODNN7EXAMPLE, ghp_ / xoxb- / sk_live_ falsos) no seu scanner de segredos.if False: pelo linter e na inserção de # noqa / # nosec.Bucket 2 — erosão da eficácia. Canários públicos entram nos corpora de treinamento de LLMs em 6–18 meses e os fornecedores de scanners adicionam heurísticas para ignorar. Rotacione UUIDs, banner, shibboleths e o CVE falso anualmente (veja ROTATE_UUID.md); varie a redação entre os adotantes; mantenha F+G em privado. Isso não impedirá os modelos de aprenderem o código, mas pode te comprar algum tempo.
Bucket 3 — pós-comprometimento. Se um atacante já tem acesso, ele pode fazer o que quiser e não precisa do honeyslop. No entanto, inverter if False: → if True:, inserir truques Unicode/BIDI ou adicionar texto de injeção de prompt em docstrings pode tornar um canário ativo onde você não espera. Vale a pena ficar atento, embora seja improvável.
Este projeto é fornecido sob a Licença MIT; veja LICENSE para os termos de garantia e responsabilidade. É responsabilidade do usuário final cumprir as leis e regulamentos aplicáveis e os termos de serviço de quaisquer ferramentas ou plataformas envolvidas.
Aviso: Este projeto contém código que parece vulnerável e deve ser considerado como tal. Parte dele funciona por ser custoso de analisar — assuma que este código consumirá recursos computacionais significativos quando inspecionado por um scanner, dependendo do canário. Qualquer modificação, automatizada ou não, pode tornar um canário ativo. Não execute, implante ou adapte nada disso em um ambiente real.
Licenciado sob MIT. Veja LICENSE.