R&D+i Manager en BASE4 Security
Un hecho. Sin intención ni severidad.
Una probabilidad. Por eso hay que revisarla.
Una afirmación. Eso construimos hoy.
¿Fuerza bruta o alguien que volvió de vacaciones?
¿Escalada o el frontend pidiendo de más?
¿Fuzzing o el deploy de las 3 de la tarde?
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.
Lo que se imagina
De lo que hablamos hoy
La ruta, la fila, la cuenta que existe sólo para ser tocada.
Lo que hace que el atacante lo encuentre.
Sirve incluso si se lo llevan: avisa cuando lo usan.
Lo único que importa. Sin señal, es decoración.
Tiene que oler a tu casa: tus nombres, tu formato de ID, tu estilo de error.
Ni un link, ni un test, ni un monitor. Si lo dispara tu QA, la alerta se muere.
Nunca una vulnerabilidad real. Detectar no puede costarte una brecha.
| WAF / IDS / reglas | Señuelos | |
|---|---|---|
| Qué mira | Si el contenido parece malicioso | Si el destino fue tocado |
| Falsos positivos | Inherentes a toda heurística | Casi cero por diseño |
| Ataque nuevo | Se le escapa hasta que haya firma | Lo detecta igual: no le importa el cómo |
| Evasión | Encoding, fragmentación, ofuscación | Sólo se evade no tocándolo |
| Cobertura | Amplia | Puntual |
| Operación | Revisión permanente | Si suena, mirás |
Rutas fantasma, IDs señuelo, headers de debug.
Registros canario, columnas señuelo, tablas _backup.
Cuentas señuelo, credenciales y tokens marcados.
Repos, pipelines, wikis, tickets, buckets.
| A01Broken Access Control | Rutas fantasma, IDs señuelo, roles inexistentes |
| A02Security Misconfiguration | Endpoints de debug y paneles de admin falsos |
| A03Supply Chain Failures | Paquete interno señuelo, token de CI marcado |
| A04Cryptographic Failures | Claves y certificados honeytoken |
| A05Injection | Filas y columnas canario |
| A06Insecure Design | Cada abuse case del threat model, con su sensor |
| A07Authentication Failures | Cuentas y credenciales sembradas |
| A08Integrity Failures | Archivos canario, webhooks señuelo |
| A09Logging & Alerting Failures | El engaño es la fuente de alta fidelidad que falta |
| A10Exceptional Conditions | Errores fabricados que delatan el fuzzing |
Si está roto, se sabe explorando.
Y explorar hace ruido, si hay algo puesto ahí para hacerlo.
/api/v1/admin/exportX-Debug-Mode: 1Cuanto más específico de tu app es el señuelo, más alta la severidad.
/wp-login.php te va a sonar 400 veces por día. Ese señuelo sirve para filtrar, no para alertar.
// 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' }); });
# 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.
Reservá esos IDs
¡Esto no tiene lectura inocente!
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.
-- 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.
Excluí procesos batch
# 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?"
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.
| Dónde | Qué te dice |
|---|---|
Repo, .env.example | Accedieron al código fuente |
| Wiki interna | Cuenta corporativa comprometida |
| Ticket viejo de soporte | Acceso al soporte o insider |
| Variable en el CI | Compromiso del pipeline |
Tabla usuarios_backup | La inyección funcionó y ya la usan |
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)
Igualá el tiempo
HMAC, no SHA
$ 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.
| Hora | Evento | Severidad | Recurso | Sujeto |
|---|---|---|---|---|
| 14:21:58 | decoy.route.hit | high | /api/v1/admin/export | anónimo · 10.20.4.17 |
| 14:22:01 | decoy.cuenta.intento | high | svc-backup-old · origen repo | anónimo · 10.20.4.17 |
| 14:22:07 | decoy.idor.hit | critical | factura:40217 | usr_8842 · 10.20.4.17 |
| 14:22:07 | decoy.idor.hit | critical | factura:40218 | usr_8842 · 10.20.4.17 |
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
10.000 eventos → 40 alertas → revisión → 2 incidentes, tres días después.
1 evento → 1 incidente. Sin correlación, sin espera.
Higiene
Métricas
Un señuelo "más creíble" con una SQLi de verdad es una brecha con excusa.
Se generan. Copiar de producción es exfiltrarte a vos mismo.
Es ilegal y te convierte en el problema.
Tu log puede llenarse de credenciales y PII de terceros.
Legales y tu equipo tienen que saber que existe.
El próximo dev que vea esa fila rara la va a borrar.
Un endpoint que suene a tu negocio y no exista. Veinte líneas.
Un canario en tu tabla más sensible, con un email honeytoken adentro.
Sembrada en un solo lugar para empezar. Y el test que la verifica.
El catálogo de detection points dentro de la app.
Engaño planificado contra técnicas de ATT&CK.
Honeytokens listos para usar, sin escribir una línea.
Servicios señuelo de red, si querés bajar a infraestructura.
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.