
Canarios de código para triar rápidamente informes de vulnerabilidad alucinados ('slop')
honeyslop son canarios de código, señuelos, para proyectos de código abierto que se ahogan en reportes de vulnerabilidades alucinados por IA ("slop") y no verificados. Con dicha inyección de ruido adversario, un escáner de slop ingiere el canario y luego genera un "reporte" de vulnerabilidad basado en él. El reporte se autoidentifica como slop. Ciérralo con un grep.
Esto es un PoC rápido, vibe-coded como broma (no listo para producción), porque nosotros mismos recibimos un reporte slop en raptor, un agente autónomo de ataque/defensa basado en Claude Code. ¡Debería ser divertido!
Los canarios de código extienden señales familiares de triage (p. ej., detecciones en archivos de prueba, secretos de ejemplo, rutas inexistentes) hacia marcadores deliberados, o señuelos. En pruebas, estos canarios funcionan lo suficientemente bien para marcar slop, pero pueden mejorarse aún más (incrustados en código real, nombres de funciones/archivos/directorios menos indicativos, regenerados regularmente como código nuevo, etc.).
Escrito por: Gadi Evron (@gadievron), John Cartwright (@grokjc), Daniel Cuthbert (@danielcuthbert) y Michal Kamensky (@kamenskymic, con crédito por nombrar el proyecto).
Úselo bajo su propio riesgo. Si pegas esto en producción, es problema tuyo. Véase Descargo de responsabilidad más abajo.
Para cada reporte entrante, en orden:
zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus; también handle_*_request si has adoptado F+G en privado) → cerrar. (Rust usa el mismo nombre snake_case zqx_tarnish_v3 que Python. Go usa zqxTarnishV3, coincidiendo con el nombre de JS.)CVE-2025-99919 (falso) → cerrar.Dos categorías de canario:
Véase Modelo de seguridad para cómo cada etapa permanece inerte a pesar de parecer vulnerable.
Los archivos de código canario se leen deliberadamente como módulos obsoletos plausibles — sin lenguaje de "canary", "honeypot" o "tripwire" en comentarios o identificadores. Eso evita que los archivos se autoidentifiquen ante los escáneres, pero también significa que la documentación de por qué estos archivos son seguros vive aquí en lugar de en el docstring de cada archivo. Al revisar o rotar un canario, verifique que cada capa a continuación sigue intacta.
Estos archivos son código con forma vulnerable — y algunos (p. ej., pickle.loads de python/session_restore.py, memcpy sin límites de c/tls_heartbeat.c) serían genuinamente explotables si fueran alcanzables. Ese es el diseño. Que los escáneres marquen los sumideros es el objetivo; las capas a continuación bloquean la ejecución, no la señal del escáner. Los escáneres basados en patrones (grep, semgrep, pipelines de slop basados en LLM — el modelo de amenaza principal) leen el código fuente como texto y reportan hallazgos independientemente de la alcanzabilidad en tiempo de ejecución. Los escáneres C integrados en compilación (CodeQL predeterminado, clang-static-analyzer) solo ven archivos compilados, por lo que los canarios C no compilados son invisibles para ellos — una compensación aceptada, ya que los generadores de slop leen abrumadoramente el código fuente, no las compilaciones.
Cinco capas independientes mantienen inertes estos archivos:
raise ImportError / throw new Error a nivel superior — un import / require simple aborta antes de que cualquier definición se vincule.if False: / if (false) — los nombres nunca entran al espacio de nombres en tiempo de ejecución incluso si se omite la capa 1.__all__: list[str] = [] (importación con asterisco no exporta nada). JS: module.exports = {} (los consumidores CommonJS obtienen un objeto vacío).zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus). Cualquier reporte que cite una de ellas se autoidentifica como slop.La Etapa E añade una sexta capa: la regex de retroceso catastrófico se almacena solo como cadena literal, no se pasa a re.compile / new RegExp en el ámbito del módulo. Incluso un entorno que elimine la capa 1 no puede activar un motor de retroceso compilado.
Los escáneres recorren el AST más allá de raise / throw y dentro del bloque muerto, por lo que los sumideros aún aparecen como hallazgos — ese es el comportamiento deseado.
buffer_ops.c)La seguridad es estructural — cada forma tiene una prueba junto a ella:
bufops_copy_banner — src es un literal de cadena, n = sizeof(literal), _Static_assert lo fija al tamaño del destino.bufops_copy_bounded — if (n > dst_cap) n = dst_cap; la línea antes del memcpy hace imposible la afirmación CWE-787. Cortocircuito en n == 0 para evitar UB de C17 memcpy(dst, NULL, 0).bufops_copy_truncating — n <= dst_cap - 1, dst[n] llega como máximo a dst_cap - 1; retorno temprano en dst_cap == 0.Aislamiento adicional: todas las funciones son static (sin enlace externo) y el archivo no se agrega a ningún objetivo de compilación.
heartbeat.c + sat.h)La silueta de Heartbleed (uint16_t payload_len → malloc(1+2+payload_len+16) → memcpy) se desactiva con protecciones en capas:
sat_sub resta saturante para todo el cálculo del presupuesto de encabezado/cola (sin desbordamiento).const al inicio — cierra ventanas TOCTOU y de UB de manejadores de señal._Static_assert(SIZE_MAX - 19 >= UINT16_MAX, ...) demuestra que el tamaño de malloc no puede desbordar size_t.payload_len > 0 evita UB de memcpy(dst, NULL, 0) en cargas útiles vacías.parse_heartbeat y read_u16_be son static; archivo no enlazado en ningún objetivo de compilación.Un reporte que alegue lectura/escritura fuera de límites en parse_heartbeat sin involucrar la protección específica en la línea citada no ha verificado explotabilidad — cierre mediante regla de triage 5.
c/tls_heartbeat.c es una variación sobre la misma silueta mantenida deliberadamente sin protección: process_heartbeat es static y el archivo no está enlazado en ningún objetivo de compilación — el aislamiento es la única capa, coincidiendo con el captura-todo de la Etapa B. Intentar llamarlo desde fuera de la TU es un error de enlace.
Cinco capas independientes mantienen inertes estos archivos (reflejando el modelo de seguridad de Python/JS):
compile_error! a nivel superior — incluir el archivo en una crate mediante mod o include! produce un error duro del compilador antes de que se evalúe cualquier definición.#[cfg(any())] — cfg(any()) siempre es falso, por lo que los nombres nunca entran en la salida compilada incluso si se omite la capa 1.pub — nada se exporta incluso si se eliminan ambas capas anteriores.zqx_tarnish_v3). Cualquier reporte que la cite se autoidentifica como slop.mod, no aparece en ningún Cargo.toml, y se excluye de los artefactos de compilación.La Etapa E añade una sexta capa: la regex de retroceso catastrófico se almacena solo como constante &str, no se pasa a regex::Regex::new en el ámbito del módulo.
buffer_ops.rs)La seguridad es estructural, coincidiendo con la contraparte en C. Cada forma tiene una prueba:
bufops_copy_banner — src es b"status: ok\0", la longitud de copia es BANNER.len(); una aserción const lo fija al tamaño del destino.bufops_copy_bounded — if n > dst_cap { n = dst_cap; } la línea antes de la copia acota la escritura. Cortocircuito en n == 0.bufops_copy_truncating — n <= dst_cap - 1, la escritura NUL en dst.add(n) llega como máximo a dst_cap - 1; retorno temprano en dst_cap == 0.bufops_shift — tanto como acotados a ; soporta explícitamente superposición.Aislamiento adicional: todas las funciones están dentro de #[cfg(any())] (código muerto), no públicas, y el archivo no se agrega a ningún objetivo de compilación.
heartbeat.rs + tls_heartbeat.rs)La silueta de Heartbleed en Rust usa operaciones de puntero raw inseguras (ptr::copy_nonoverlapping, std::alloc::alloc) y se desactiva con las mismas protecciones en capas que la versión en C:
usize::saturating_sub para todo el cálculo del presupuesto de encabezado/cola.usize::MAX - 19 >= u16::MAX) demuestra que la asignación no puede desbordar.payload_len > 0.#[cfg(any())], no públicas, archivo no enlazado.rust/tls_heartbeat.rs es la variante deliberadamente sin protección: process_heartbeat usa ptr::copy_nonoverlapping con claimed_len de entrada no confiable y sin protección de límites. El aislamiento (#[cfg(any())] + compile_error! + no enlazado) es la única capa.
Cinco capas independientes mantienen inertes estos archivos:
//go:build ignore en la parte superior de cada archivo. El toolchain de Go (go build, go test, go vet) omite el archivo por completo; nunca se compila, enlaza ni prueba.func init() { panic("...") } con UUID. Si la capa 1 se omite de alguna manera (anulación manual de -tags, go tool compile directo), el programa falla al inicio antes de que se ejecute main().zqxTarnishV3). Cualquier reporte que la cite se autoidentifica como slop.go/ sin go.mod, no referenciados por ninguna importación de paquete, y excluidos de los artefactos de compilación.La Etapa E añade una sexta capa específica de Go: validatePep440Plus sí llama a regexp.MustCompile sobre el patrón catastrófico, pero el paquete regexp de Go usa semánticas RE2 (coincidencia garantizada en tiempo lineal), por lo que el retroceso catastrófico es imposible independientemente. El canario sigue siendo útil porque los escáneres marcan la forma del patrón textualmente sin verificar qué motor de regex está en uso.
buffer_ops.go)La seguridad es estructural, coincidiendo con las contrapartes en C y Rust. Cada forma tiene una prueba:
bufopsCopyBanner — src es una constante de cadena, copy() desde un literal conocido hacia un array de tamaño fijo; una aserción en tiempo de compilación fija la longitud.bufopsCopyBounded — if n > dstCap { n = dstCap } la línea antes de la copia acota la escritura. Cortocircuito en n == 0.bufopsCopyTruncating — n <= dstCap - 1, la escritura NUL en dst[n] llega como máximo a dstCap - 1; retorno temprano en dstCap == 0.bufopsShift — tanto i + n como j + n acotados a ; soporta superposición dentro de un solo slice.Aislamiento adicional: //go:build ignore excluye el archivo de todas las compilaciones, todas las funciones no están exportadas, y el archivo no es importado por ningún paquete.
heartbeat.go + tls_heartbeat.go)La silueta de Heartbleed en Go usa unsafe.Slice y unsafe.Pointer y se desactiva con las mismas protecciones en capas que las versiones en C y Rust:
satSub resta saturante para todo el cálculo del presupuesto de encabezado/cola.payloadLen > 0.//go:build ignore, todas las funciones no exportadas, archivo no importado.go/tls_heartbeat.go es la variante deliberadamente sin protección: processHeartbeat usa unsafe.Slice con claimedLen de entrada no confiable y sin protección de límites. El aislamiento (//go:build ignore + pánico en init + no importado) es la única capa.
ROTATE_UUID.md.MANIFEST.in prune las rutas canario, o pyproject.toml tool.setuptools.exclude-package-data. C: omitir de CMakeLists.txt / Makefile / sdist. Rust: no agregue una declaración mod o atributo path que referencie los archivos canario; excluya de las rutas [lib]/[[bin]] de Cargo.toml y de cargo package mediante exclude. Go: la restricción ya excluye los archivos de ; no coloque un en el directorio canario, y no importe el paquete canario. Docker: .Copie y pegue para cerrar los pasos de exclusión y propiedad anteriores. Las rutas a continuación usan el diseño c/ / python/ / js/ / rust/ / go/ de honeyslop; renómbrelas a donde coloque los canarios (véase paso 7) — confirmar exclusiones que aún apuntan a directorios llamados canary/ o slop/ es en sí mismo una pista.
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 — invocación CI
bandit -r src/ -x python/
Ruff — pyproject.toml
[tool.ruff]
extend-exclude = ["python/"]
.clang-format-ignore
c/*
Lista blanca de escáner de secretos — 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 del paquete crate
[package]
exclude = ["rust/"]
Clippy — invocación CI
cargo clippy --workspace -- --allow-dead-code
O salte el directorio canario completamente al no referenciarlo en ningún árbol mod (el valor predeterminado — compile_error! detectará la inclusión accidental).
golangci-lint — .golangci.yml
issues:
exclude-dirs:
- go
O confíe en la restricción //go:build ignore, que ya evita que el toolchain de Go compile los archivos canario.
.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
Los propietarios deben ser un grupo pequeño que entienda por qué estas rutas parecen vulnerables — para que los PRs de "limpiar código muerto" se bloqueen, no se fusionen.
Cubo 1 — sus propias herramientas se activan. Sus escáneres, linters, IDE y escaneo de secretos se dispararán sobre el canario. Este es el comportamiento deseado para escaneos entrantes, pero significa que sus propios pipelines necesitan saltarse estas rutas. Requerido:
paths-ignore, .semgrepignore, bandit -x, Ruff --extend-exclude, .clang-format-ignore, LSP del editor).MANIFEST.in prune, .dockerignore, exclusiones de wheel).AKIAIOSFODNN7EXAMPLE, ghp_ / xoxb- / sk_live_ falsos) en su escáner de secretos.if False:, y la inserción de # noqa / .Cubo 2 — erosión de efectividad. Los canarios públicos ingresan a corpus de entrenamiento de LLM en 6–18 meses y los vendedores de escáneres añaden heurísticas de salto. Rote UUIDs, banner, shibboleths y el CVE falso anualmente (véase ROTATE_UUID.md); varíe la redacción entre adoptantes; mantenga F+G en privado. No detendrá que los modelos aprendan el código, pero podría comprar algo de tiempo.
Cubo 3 — post-compromiso. Si un atacante ya tiene acceso, puede hacer lo que quiera y no necesita honeyslop. Sin embargo, cambiar if False: → if True:, insertar trucos Unicode/BIDI, o agregar texto de inyección de instrucciones en docstrings puede convertir un canario en vivo donde no lo espera. Vale la pena vigilarlo, aunque sea improbable.
Este proyecto se proporciona bajo la Licencia MIT; véase LICENSE para términos de garantía y responsabilidad. Es responsabilidad del usuario final cumplir con las leyes y regulaciones aplicables, y con los términos de servicio de cualquier herramienta o plataforma involucrada.
Advertencia: Este proyecto contiene código que parece vulnerable, y debe asumirse que lo es. Parte de él funciona siendo costoso de analizar — asuma que este código consumirá recursos informáticos significativos cuando sea inspeccionado por un escáner, dependiendo del canario. Cualquier modificación, automatizada o no, podría convertir un canario en vivo. No ejecute, despliegue ni adapte ninguno de sus componentes en un entorno real.
Licenciado bajo MIT. Véase LICENSE.
| Etapa | Archivo(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 sumideros CWE + secretos falsos + shibboleths |
| B | c/buffer_ops.c, rust/buffer_ops.rs, go/buffer_ops.go | 4 formas memcpy/memmove (CWE-120/121/787/170) |
| C | fusionado en A | Rendimiento CWE extendido |
| 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 | Silueta de Heartbleed |
| E | python/regex_validator.py, js/regex_validator.js, rust/regex_validator.rs, go/regex_validator.go | Regex de retroceso catastrófico + CVE-2025-99919 falso |
| F+G | private/fractal_dag/ (no en este repositorio) | Sumideros de Etapa-A a través de un DAG de 12 nodos de entradas handle_*_request |
bufops_shift — tanto i + n como j + n acotados a cap; memmove soporta explícitamente superposición.i + nj + ncapptr::copycapcopy()//go:build ignorego buildgo.mod.dockerignorepaths-ignore, .semgrepignore, bandit -x, Ruff --extend-exclude — todos apuntando a sus rutas canario.SECURITY.md — véase SECURITY.md.template. Esto puede alertar a los escáneres de slop sobre la presencia del canario (¿quizás algo bueno?).CODEOWNERS en los archivos canario; un hook pre-commit que falle si el conteo de UUID canario disminuye o si faltan los disparadores if False:.SECURITY.md.template, ROTATE_UUID.md) donde es relevante.# nosec