From n00b to ZeroCool / El lado oscuro
Broken Access Control: el bug que te deja entrar donde no debes
Detecta y corrige Broken Access Control: roles, permisos, IDOR, pruebas negativas, logs y fixes reales para apps web y APIs.
Lo que vale la pena leer aquí
Te metes a los logs con el ojo medio cerrado y empiezas a negociar con la realidad: “seguro fue caché”, “seguro era un admin”, “igual era ambiente de pruebas”…
Intro con gancho
Son las 11:47 pm y tu laptop ya pide jubilación. Te cae un ticket con urgencia: “Oye, un usuario vio facturas que no son suyas”.
Te metes a los logs con el ojo medio cerrado y empiezas a negociar con la realidad: “seguro fue caché”, “seguro era un admin”, “igual era ambiente de pruebas”…
Spoiler: era autorización rota. De esas fallas que no salen en el happy path, pero cuando pegan, abren puertas prohibidas como si el edificio no tuviera guardias.
Broken Access Control no es un hack de película. Es lo básico mal puesto. Y por eso mismo duele tanto en production: porque se cuela entre features, prisas y ese deploy de viernes que juraste no volver a hacer.
Qué vas a aprender
- Qué significa Broken Access Control en la vida real (sin definición de diccionario).
- Los patrones más comunes: IDOR, falta de checks server-side, roles mal modelados, endpoints “olvidados”.
- Cómo hacer threat modeling express para permisos sin volverte loco.
- Un workflow defensivo para detectar, probar y corregir fallas de autorización en web y APIs.
- Controles que sí aguantan producción: middleware, policies, pruebas automatizadas y logging que sirve.
Contexto práctico (la neta de por qué pasa)
Access control se rompe por tres cosas bien humanas:
-
Confundimos autenticación con autorización
- Autenticación: “¿Quién eres?” (login, token, sesión).
- Autorización: “¿Qué puedes hacer?” (permisos, ownership, roles).
Si tu backend solo valida “token válido” y ya, estás a un paso del desastre.
-
La lógica vive en el frontend
El UI oculta botones (“solo admin ve esto”) y parece seguro… hasta que alguien pega directo al endpoint.Esto pasa un chorro cuando el equipo está chico y el backend trae mil pendientes, entonces el frontend “lo resuelve” para sacar la demo antes del deadline.
-
Crecimiento orgánico sin modelo de permisos
Empiezas con “admin/user” y de pronto ya existe “operador”, “contador”, “sucursal”, “franquicia”, “super-admin”, “partner”… y el access control se vuelve un copy/paste deifs.
El resultado típico: endpoints con lógica inconsistente. Unos sí validan ownership, otros no. Unos revisan rol, otros solo revisan que exista el usuario.
Paso a paso para identificar y arreglar Broken Access Control
1) Dibuja el mapa mínimo: recursos, acciones y actores
No necesitas UML. Necesitas claridad y que el equipo lo entienda en corto.
- Recursos:
Factura,Pedido,Usuario,Proyecto,Archivo. - Acciones:
ver,crear,editar,cancelar,exportar,borrar. - Actores:
cliente,staff,admin,partner,soporte.
Preguntas que sí cambian el outcome:
- ¿Quién es dueño del recurso? (ownership)
- ¿Hay multi-tenant? (empresa_id, sucursal_id)
- ¿Hay roles globales vs por proyecto/empresa?
Si tu app es SaaS, el multi-tenant es donde se rompe todo con más frecuencia: “solo cambié el companyId en la URL y pum, ya vi datos de otro”.
2) Encuentra los puntos de entrada reales (no los que crees que existen)
Haz inventario de rutas/operaciones:
- Web: rutas del backend (REST, GraphQL, RPC)
- Mobile: endpoints que pega la app
- Admin panel: endpoints internos
- Jobs: workers que procesan cosas con credenciales de servicio
Consejo de guerra: el admin panel casi siempre trae endpoints “temporales” que se quedaron para siempre. Y esos son los que más fácil se olvidan sin guard.
3) Verifica que el control esté server-side (si no, no existe)
Regla simple: la autorización vive en el servidor.
Red flags clásicas:
- “El frontend manda
role: 'admin'y el backend confía”. - “El backend filtra por
userIdque viene del request, no del token”. - “La query busca por
idy ya; nunca validaowner_id/tenant_id”.
Ejemplo conceptual (malo):
// MALO: confías en userId del request
app.get('/api/invoices/:id', async (req, res) => {
const invoice = await db.invoice.findById(req.params.id);
// ...regresas invoice sin validar que sea del usuario/empresa
res.json(invoice);
});
Ejemplo defensivo (mejor):
// MEJOR: usas identidad del token y validas ownership/tenant
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
const invoice = await db.invoice.findFirst({
where: {
id: req.params.id,
tenant_id: req.user.tenant_id,
owner_id: req.user.id // o policy por rol
}
});
if (!invoice) return res.status(404).end();
res.json(invoice);
});
Nota: 404 vs 403 es un tradeoff real. 404 evita filtrar existencia del recurso; 403 es más claro para debugging. Define una política consistente y respétala.
4) Haz pruebas negativas (las que se brincan por el deadline)
La prueba positiva es: “un usuario ve su factura”. Eso casi siempre funciona.
La prueba negativa es: “un usuario NO ve la factura del vecino”. Esa es la que te salva la chamba.
Checklist de pruebas negativas:
- Usuario A intenta acceder recurso de Usuario B (ownership).
- Usuario de Empresa 1 intenta acceder recurso de Empresa 2 (tenant).
- Rol sin permiso intenta acción sensible (cancelar, exportar, borrar).
- Cambias parámetros en URL/body:
id,user_id,tenant_id,account_id. - Si hay GraphQL: pruebas field-level authorization (no solo el resolver principal).
No necesitas “hackear”. Solo simula usuarios normales con tokens distintos, en un ambiente autorizado.
5) Centraliza autorización: policies/middleware (menos ifs, menos rollback)
Cuando cada endpoint trae su propio if (user.role === ...), terminas con 10 versiones del “mismo” permiso.
Mejor separa así:
- Middleware para autenticación (quién eres).
- Policies/guards para autorización (qué puedes hacer).
- Helpers de ownership y tenant.
Estrategias que funcionan:
can(user, action, resource)requirePermission('invoice:read')requireOwnershipOrRole('admin')
Ejemplo conceptual:
function canReadInvoice(user, invoice) {
if (user.role === 'admin') return true;
return invoice.tenant_id === user.tenant_id && invoice.owner_id === user.id;
}
Lo importante: que todas las rutas usen el mismo criterio y no “versiones creativas” que salen cuando alguien andaba a las carreras.
6) Protege también los listados y las búsquedas
Muchos bugs no viven en GET /invoices/:id, sino en:
GET /invoices?userId=...GET /search?q=...POST /reports/export(exporta “todo” por default)
Regla práctica: filtra por tenant/ownership desde la query, no después en memoria cuando ya traes datos de más.
7) Logging útil (sin tirar datos sensibles al log)
Cuando hay incidente, necesitas responder rápido:
- ¿Qué usuario pidió qué?
- ¿Desde dónde? (IP, user-agent)
- ¿Qué recurso? (IDs)
- ¿Qué decisión tomó el guard? (allowed/denied y por qué)
Evita loggear:
- Tokens, passwords, PII completa
- Contenido de facturas, direcciones completas, etc.
Un patrón sano es loggear eventos de autorización:
authz_decision: deny,reason: not_owner,resource: invoice,resource_id
Esto ayuda un buen cuando soporte te escribe por WhatsApp: “el cliente dice que no ve su pedido” y tú vas en el metro con señal chafa y prisa.

Screenshots sugeridos
- Captura de tu herramienta de API (Postman/Insomnia) con:
- una petición autorizada (usuario A a su recurso)
- una petición denegada (usuario A a recurso de usuario B)
- Captura de logs donde se vea
authz_decision=denyconresource_id(sin datos sensibles). - Captura del archivo de policy/guard o middleware donde centralizas permisos.
Errores comunes + solución (de los que duelen en production)
Error 1: “Si no hay botón, no hay acceso”
Síntoma: el UI oculta acciones, pero el endpoint existe y responde.
Solución: checks server-side en cada operación. El frontend mejora UX, no seguridad.
Error 2: IDOR por confiar en IDs directos
Síntoma: GET /users/123 devuelve datos si estás logueado, aunque no seas el 123.
Solución: valida ownership/tenant. Si necesitas IDs públicos, usa UUIDs o IDs opacos, pero ojo: IDs opacos no reemplazan autorización.
Error 3: Multi-tenant “de buena fe”
Síntoma: todas las tablas tienen tenant_id, pero varias queries no lo filtran.
Solución: agrega scopes obligatorios a nivel ORM/DB (row-level security si aplica) y tests que fallen si falta tenant_id.
Error 4: Roles gigantes tipo “admin hace todo”
Síntoma: se asigna rol admin “para destrabar” y ya nadie lo baja.
Solución: separa permisos: billing:read, billing:export, users:manage. Usa roles que reflejen funciones reales, no estados de emergencia.
Error 5: Endpoints internos expuestos
Síntoma: /internal/reindex o /admin/exportAll accesible desde internet.
Solución: red de confianza (VPN, allowlist), auth fuerte y, de preferencia, separado por servicio. Y sí: revisa tus rutas antes de abrir el firewall en el datacenter o en el VPS barato.

Checklist final (para cerrar bien el bug)
- Cada endpoint valida autorización en el servidor (no solo login).
- Las queries incluyen
tenant_id/ownership desde el inicio. - Hay pruebas negativas para recursos de otro usuario/empresa.
- Policies/guards centralizados; nada de lógica duplicada por ruta.
- Logs de decisiones de autorización (permit/deny) sin datos sensibles.
- Revisión de endpoints “internos” y acciones masivas (export, delete).
- Roles/permisos modelados con intención (y documentados para el equipo).
FAQ
1) ¿Broken Access Control es lo mismo que un bug de login?
No. El login puede estar perfecto y aun así un usuario autenticado podría acceder a cosas que no le tocan. Ese es el punto: ya está adentro, nomás en el cuarto equivocado.
2) ¿Si uso UUID en lugar de IDs numéricos ya quedé?
Ayuda contra adivinanza, pero no es control de acceso. Si tu endpoint no valida ownership/tenant, un UUID filtrado o compartido abre la misma puerta.
3) ¿Qué regreso: 403 o 404 cuando no tiene permiso?
Depende de tu política. 404 reduce filtración de existencia; 403 es más transparente. Lo importante es consistencia y que tus logs tengan el motivo real.
4) ¿Cómo lo aterrizo en microservicios?
Define autorización en el service owner del recurso y estandariza claims en tokens (tenant, roles, scopes). No confíes en headers “inyectados” por otro servicio sin verificación.
5) ¿Qué pruebas automatizadas valen más aquí?
Las de autorización negativa: usuarios cruzados, tenants cruzados, roles sin permisos, y rutas masivas (export/delete). Son baratas de correr y carísimas de ignorar.
Siguiente episodio
Cuando ya controlas quién entra, toca ver qué pasa cuando alguien mete datos maliciosos a propósito.
La siguiente parada huele a inyección y a validación que “luego le metemos”, hasta que un día truena.
Idea para cerrar bien este post: toma una sola práctica de aquí y conviértela en algo que tu equipo pueda aplicar hoy.
Cuando un artículo aterriza en decisiones reales, deja de ser contenido y se vuelve ventaja.


