30:00
OWASP Village · Ekoparty 2026

Trampas en tu código

Ciberengaño aplicado al OWASP Top 10.
30 minutos Diego Staino
Quién les habla

Diego Staino

R&D+i Manager en BASE4 Security

Investigador Profesor
El problema

Nunca tuvimos tantos logs. Nunca detectamos tan tarde.

El log dice

"Esto pasó"

Un hecho. Sin intención ni severidad.

El WAF dice

"Esto se parece"

Una probabilidad. Por eso hay que revisarla.

Falta

"Esto es un ataque"

Una afirmación. Eso construimos hoy.

El falso positivo es el ataque que se te pasa
  • Una regla que suena 200 veces por día se silencia sola.
  • Nadie necesita evadir una detección que ya está enterrada.
La pregunta incómoda: de todo lo que emite tu app hoy, ¿qué evento no admite explicación inocente?
Ambiguo401 en /login

¿Fuerza bruta o alguien que volvió de vacaciones?

Ambiguo403 en /api/admin

¿Escalada o el frontend pidiendo de más?

AmbiguoPico de 500

¿Fuzzing o el deploy de las 3 de la tarde?

El axioma

Nadie legítimo toca lo que no existe.

Si nada en tu app lo referencia, la única forma de llegar es buscándolo. Y buscar es una intención.

Pasillo vacío y puerta cerrada
Fundamentos

Esto no es "poner un honeypot"

Lo que se imagina

  • Una máquina aparte, en una DMZ.
  • Un proyecto de investigación.
  • Algo que administra otro equipo.

De lo que hablamos hoy

  • Sensores dentro de tu superficie real.
  • Objetos que parecen tuyos porque están entre los tuyos.
  • Código que escribe el equipo de desarrollo.
OWASP AppSensor los llama detection points. MITRE Engage los modela como tácticas de engaño.
Vocabulario mínimo

Cuatro palabras

Señuelo

El objeto falso

La ruta, la fila, la cuenta que existe sólo para ser tocada.

Miga

El camino hacia él

Lo que hace que el atacante lo encuentre.

Honeytoken

El señuelo que viaja

Sirve incluso si se lo llevan: avisa cuando lo usan.

Señal

El evento que emitís

Lo único que importa. Sin señal, es decoración.

Las tres reglas

Un buen señuelo cumple las tres

Creíble en contexto

Tiene que oler a tu casa: tus nombres, tu formato de ID, tu estilo de error.

Inalcanzable por accidente

Ni un link, ni un test, ni un monitor. Si lo dispara tu QA, la alerta se muere.

Inocuo si lo tocan

Nunca una vulnerabilidad real. Detectar no puede costarte una brecha.

Corolario: excluí tus señuelos de tu propio DAST, o vas a auditarte a vos mismo todos los martes.
Fundamentos

Firma vs. interacción

WAF / IDS / reglasSeñuelos
Qué miraSi el contenido parece maliciosoSi el destino fue tocado
Falsos positivosInherentes a toda heurísticaCasi cero por diseño
Ataque nuevoSe le escapa hasta que haya firmaLo detecta igual: no le importa el cómo
EvasiónEncoding, fragmentación, ofuscaciónSólo se evade no tocándolo
CoberturaAmpliaPuntual
OperaciónRevisión permanenteSi suena, mirás
Fundamentos

Cuatro capas donde tu app puede mentir

Ruteo

Endpoints que no existen

Rutas fantasma, IDs señuelo, headers de debug.

Datos

Filas imposibles

Registros canario, columnas señuelo, tablas _backup.

Identidad

Usuarios que nunca entran

Cuentas señuelo, credenciales y tokens marcados.

Artefactos

Lo que rodea al código

Repos, pipelines, wikis, tickets, buckets.

El mapa

Top 10 : 2025 ↔ qué señuelo lo delata

A01Broken Access ControlRutas fantasma, IDs señuelo, roles inexistentes
A02Security MisconfigurationEndpoints de debug y paneles de admin falsos
A03Supply Chain FailuresPaquete interno señuelo, token de CI marcado
A04Cryptographic FailuresClaves y certificados honeytoken
A05InjectionFilas y columnas canario
A06Insecure DesignCada abuse case del threat model, con su sensor
A07Authentication FailuresCuentas y credenciales sembradas
A08Integrity FailuresArchivos canario, webhooks señuelo
A09Logging & Alerting FailuresEl engaño es la fuente de alta fidelidad que falta
A10Exceptional ConditionsErrores fabricados que delatan el fuzzing
A01 · Broken Access Control

Si está roto, se sabe explorando.

Y explorar hace ruido, si hay algo puesto ahí para hacerlo.

A01 · Diseño

Qué sembrar en la capa de ruteo

  • Rutas fantasma: /api/v1/admin/export
  • IDs señuelo en el rango que se va a iterar
  • Headers de debug: X-Debug-Mode: 1
  • Roles que no existen en el JWT

Cuanto más específico de tu app es el señuelo, más alta la severidad.

Ojo: /wp-login.php te va a sonar 400 veces por día. Ese señuelo sirve para filtrar, no para alertar.
A01 · Código
Middleware de rutas fantasma · ExpressRutas genéricas: severidad baja
// Rutas que NO existen en la app. Por eso mismo son perfectas.
const SENUELOS = new Set([
  '/api/v1/admin/export',   // suena a nuestro dominio
  '/api/v1/facturas/dump',  // idem, en nuestro idioma
  '/.env.bak', '/actuator/heapdump'
]);

app.use((req, res, next) => {
  if (!SENUELOS.has(req.path)) return next();

  senal({                                       // alta confianza
    tipo:      'decoy.route.hit',
    severidad: req.user ? 'critical' : 'high',  // autenticado = peor
    ruta: req.path, ip: req.ip, ua: req.get('user-agent'),
    sujeto: req.user?.sub ?? null, reqId: req.id
  });

  // Responder EXACTAMENTE como el 404 real: mismo body, mismos
  // headers y mismo tiempo. Si difiere, el señuelo se detecta.
  return res.status(404).json({ error: 'Not Found' });
});
A01 · Código
IDs señuelo para IDOR · FastAPI
# El atacante itera IDs. Sembrá algunos en el camino.
ID_SENUELO = {40217, 40218, 90001}

@router.get("/api/facturas/{factura_id}")
def get_factura(factura_id: int,
                user=Depends(usuario_actual)):

    if factura_id in ID_SENUELO:      #
        senal("decoy.idor.hit",
              severidad="critical",
              recurso=f"factura:{factura_id}",
              sujeto=user.sub, ip=ctx.ip)
        return FACTURA_FABRICADA

    return repo.get(factura_id, user.tenant)

Ninguna pantalla y ningún cliente piden esos IDs. La única forma de llegar es enumerando.

Devolvé datos fabricados, no un 403. El 403 le confirma que el ID existe. La factura falsa lo mantiene ocupado.

Reservá esos IDs

A01 · La señal

Así llega al SIEM

Critical decoy.idor.hit 14:22:07
recurso
factura:40217
sujeto
usr_8842 # rol operador
ip
10.x.x.x # red corporativa
ua
python-requests/2.31
secuencia
40210 → 40217 en 4 s
  • Sesión válida.
  • Cliente que no es el navegador de la app.
  • Enumeración secuencial.

¡Esto no tiene lectura inocente!

A05 · Injection

Si el señuelo vive en los datos, se lo llevan con el dump.

La inyección exitosa es silenciosa:
devuelve un 200 con datos de más.

A05 · Código
La fila que ningún query legítimo puede devolver
-- Todos los queries de la app filtran por tenant_id.
-- Este tenant no le pertenece a nadie.
INSERT INTO clientes (id, tenant_id, razon_social,
                      email_ap, cuit)
VALUES (
  999999,
  '__canario__',                    -- inalcanzable
  'Kestrel Holdings SA',          -- plausible
  'ap-kestrel@sink.tu-dominio.ar', -- honeytoken
  '30-99999999-7'                  -- CUIT inválido
);

Un OR 1=1 la devuelve. El uso normal, nunca.

Bonus: un ORM que se olvida del scope de tenant dispara el mismo evento. Es un test de aislamiento corriendo en producción.

Excluí procesos batch

A05 · Código
El sensor va en la salida, no en la entrada
# No inspeccionamos el input buscando payloads.
# Miramos qué está por salir de la aplicación.
IDS_CANARIO = {999999}

def serializar(filas, ctx):
    for f in filas:
        if f.id in IDS_CANARIO:          #
            senal("decoy.canario.fuga",
                  severidad="critical",
                  endpoint=ctx.ruta,
                  query_id=ctx.query_id,
                  filas_devueltas=len(filas),
                  sujeto=ctx.sujeto)

    return [dump(f) for f in filas]

De "¿esto parece un ataque?" a "¿salió algo que no podía salir?"

  • No hay firma que evadir.
  • Detecta el éxito, no el intento.
A07 · Authentication Failures

Una cuenta que nunca se usa es un sensor perfecto.

Su tasa de uso legítimo es cero. Un solo intento ya es información.

A07 · Diseño

Una variante por cada lugar donde la plantás

DóndeQué te dice
Repo, .env.exampleAccedieron al código fuente
Wiki internaCuenta corporativa comprometida
Ticket viejo de soporteAcceso al soporte o insider
Variable en el CICompromiso del pipeline
Tabla usuarios_backupLa inyección funcionó y ya la usan
Esto no te lo da ninguna otra detección: el evento te dice por dónde se filtró, sin investigar nada.
A07 · Código
Hook en el login · antes de validar nada
CUENTAS_SENUELO = {
  "svc-backup-old":  "repo",     # marca de siembra
  "administrador.old": "wiki",
  "jgomez.baja":      "ticket-4471",
}

def login(usuario: str, password: str, ctx):
    u = usuario.lower().strip()

    if u in CUENTAS_SENUELO:               #
        senal("decoy.cuenta.intento",
              severidad="high",
              usuario=u,
              origen=CUENTAS_SENUELO[u],   # ¿de dónde salió?
              pass_sha256=sha256(password),  # nunca en claro
              ip=ctx.ip, ua=ctx.ua)
        return respuesta_credenciales_invalidas()  # idéntica

    return autenticar(u, password, ctx)
  • Se chequea antes de tocar la base.
  • La respuesta es idéntica a un login fallido.
  • El hash correlaciona campañas. En claro, jamás.

Igualá el tiempo

HMAC, no SHA

Demo · Del lado del atacante

Lo que ve quien explora

$ curl -s https://app.tu-dominio.ar/robots.txt
User-agent: *
Disallow: /api/v1/admin/export          ← la miga

$ curl -si https://app.tu-dominio.ar/api/v1/admin/export
HTTP/1.1 404 Not Found
{"error":"Not Found"}                    # igual al 404 real

$ curl -s -X POST .../login -d user=svc-backup-old
{"error":"Credenciales inválidas"}       # igual a cualquier fallo

$ for id in $(seq 40214 40218); do
    curl -s -b sesion.txt -o /dev/null \
      -w "$id %{http_code}\n" .../api/facturas/$id; done
40214 404
40215 404
40216 404
40217 200                                ← "encontré algo"
40218 200

Ejemplo sintético

Para él: una ruta que no existe, una cuenta que no anda y un IDOR que funciona.

Nada le avisa que lo están mirando. Cada respuesta es la que esperaría de una app real.

Demo · Del lado del defensor

Lo que llega a la guardia

HoraEventoSeveridadRecursoSujeto
14:21:58decoy.route.hithigh/api/v1/admin/exportanónimo · 10.20.4.17
14:22:01decoy.cuenta.intentohighsvc-backup-old · origen repoanónimo · 10.20.4.17
14:22:07decoy.idor.hitcriticalfactura:40217usr_8842 · 10.20.4.17
14:22:07decoy.idor.hitcriticalfactura:40218usr_8842 · 10.20.4.17
Incidente1 incidente, 9 segundos

Una IP, cuatro señuelos, ninguna lectura inocente. La cuenta usr_8842 está comprometida y la credencial señuelo salió del repo.

Ejemplo sintético

  • Sin correlación ni reglas: cada evento ya es alerta.
  • El origen de la cuenta dice por dónde se filtró.
A09 · Logging & Alerting

El engaño no reemplaza tu logging. Lo vuelve accionable.

  • No admite explicación inocente.
  • Trae la severidad puesta desde el origen.
  • Va directo a la guardia, sin revisión previa.
Ruta normal

10.000 eventos → 40 alertas → revisión → 2 incidentes, tres días después.

Ruta del señuelo

1 evento → 1 incidente. Sin correlación, sin espera.

Operación

Cuando suena, ¿qué hacés?

AppSeñuelo tocado
→
EventoJSON, canal aparte
→
SIEMSeveridad alta, sin revisión
→
GuardiaPlaybook de una página
  • Cortar ya: contenés, pero le confirmás que lo viste.
  • Observar: aprendés el alcance, pero corre el reloj.
Decidilo antes del incidente, no durante.
Operación

Un señuelo olvidado es deuda técnica

Higiene

  • Inventario versionado, con dueño.
  • Un test que verifica que la señal sale.
  • Avisale a tu equipo. QA, SRE y el pentester.

Métricas

  • Tasa de falsos positivos: tiene que ser cero.
  • Tiempo hasta el primer toque.
  • MTTD contra tus detecciones tradicionales.
Los límites

Nunca

Una vuln real

Un señuelo "más creíble" con una SQLi de verdad es una brecha con excusa.

Datos reales de carnada

Se generan. Copiar de producción es exfiltrarte a vos mismo.

Contraatacar

Es ilegal y te convierte en el problema.

Guardar de más

Tu log puede llenarse de credenciales y PII de terceros.

Hacerlo en silencio

Legales y tu equipo tienen que saber que existe.

Siempre

Documentar el porqué

El próximo dev que vea esa fila rara la va a borrar.

Empezá el lunes

Una tarde. Tres señuelos.

01

Una ruta

Un endpoint que suene a tu negocio y no exista. Veinte líneas.

02

Una fila

Un canario en tu tabla más sensible, con un email honeytoken adentro.

03

Una cuenta

Sembrada en un solo lugar para empezar. Y el test que la verifica.

Antes de la primera línea: definí a dónde llega la señal y quién la mira.
Para seguir

Todo gratis

Marco de aplicación

OWASP AppSensor

El catálogo de detection points dentro de la app.

Marco adversario

MITRE Engage

Engaño planificado contra técnicas de ATT&CK.

Herramienta

Canarytokens

Honeytokens listos para usar, sin escribir una línea.

Herramienta

OpenCanary

Servicios señuelo de red, si querés bajar a infraestructura.

Gracias

Nadie legítimo toca lo que no existe.

Tu app ya sabe todo lo que pasa adentro. Le falta un lugar donde nada debería pasar.

Sala de operaciones de noche
Ekoparty Apertura Trampas en tu código 01 / 30

Índice

← →Navegar (también click en el slide) OÍndice TArrancar / parar el reloj de 30 min RReiniciar el reloj FPantalla completa EscCerrar