Control de Acceso Roto
Autenticación frente a Autorización
Es crucial distinguir dos conceptos que suelen confundirse. La autenticación responde a "¿quién eres?"; la autorización responde a "¿qué tienes permitido hacer?". El control de acceso es la implementación de la autorización: las reglas que determinan qué recursos y acciones puede usar cada usuario. Cuando estas reglas están ausentes, son incompletas o se aplican incorrectamente, hablamos de control de acceso roto.
Un usuario puede estar perfectamente autenticado —haber iniciado sesión con credenciales válidas— y aun así realizar acciones que no le corresponden si la autorización falla. Son planos independientes: la autenticación establece la identidad; la autorización decide, para cada solicitud y cada recurso concreto, si esa identidad tiene derecho a proceder. Confundirlos lleva al error clásico de asumir que "si el usuario ya inició sesión, puede acceder a lo que pida".
El OWASP Top 10 sitúa el Broken Access Control en la primera posición (A01:2021), porque es la categoría de vulnerabilidad más extendida y frecuentemente explotada. La razón es que el control de acceso depende de la lógica de negocio específica de cada aplicación: no hay un parche genérico, ni una biblioteca que lo resuelva de forma universal, ni una cabecera HTTP que lo active. Cada endpoint, cada recurso y cada acción sensible necesita su propia comprobación, y basta con que un solo camino quede sin proteger para abrir una brecha. Esa dependencia de la lógica de negocio es también la razón por la que las herramientas automáticas detectan mal estos fallos: la máquina no sabe qué factura debería poder ver cada cliente.
IDOR: Referencias Directas Inseguras a Objetos
La IDOR (Insecure Direct Object Reference, CWE-639) es una de las formas más comunes de control de acceso roto. Ocurre cuando una aplicación expone una referencia directa a un objeto interno (como un identificador numérico en la URL) y usa ese valor para acceder a un recurso sin verificar que el usuario actual tiene derecho a verlo.
Por ejemplo, si al ver tu factura la URL contiene factura?id=1024 y la aplicación simplemente devuelve la factura con ese identificador sin comprobar que te pertenece, cambiar el número a 1025 podría mostrarte la factura de otra persona. El mecanismo conceptual es simple: el servidor confía en un parámetro controlado por el usuario para decidir qué datos entregar, sin una comprobación de autorización del lado del servidor. Como los identificadores suelen ser secuenciales, un atacante puede enumerarlos (1023, 1024, 1025…) y recorrer los datos de todos los usuarios de forma sistemática. Las IDOR pueden afectar a la lectura, modificación o borrado de datos ajenos.
Código vulnerable
Este endpoint de Express devuelve la factura por su id sin comprobar de quién es. Cualquier usuario autenticado puede leer las facturas de todos los demás:
// VULNERABLE: no verifica la propiedad del recurso
app.get("/invoices/:id", requireAuth, async (req, res) => {
const invoice = await db.query(
"SELECT * FROM invoices WHERE id = $1",
[req.params.id]
);
res.json(invoice.rows[0]);
});
La causa raíz está en la consulta: filtra por id, un valor que controla el cliente, pero nunca cruza ese id con el dueño real del recurso.
Versión segura
El arreglo consiste en atar el recurso al usuario autenticado en la propia consulta y responder 404 cuando no hay coincidencia, sin revelar si el recurso existe o no:
// SEGURO: el recurso debe pertenecer al usuario autenticado
app.get("/invoices/:id", requireAuth, async (req, res) => {
const invoice = await db.query(
"SELECT * FROM invoices WHERE id = $1 AND owner_id = $2",
[req.params.id, req.user.id]
);
if (invoice.rowCount === 0) {
// 404 (no 403) para no confirmar la existencia del recurso ajeno
return res.status(404).json({ error: "Not found" });
}
res.json(invoice.rows[0]);
});
La comprobación AND owner_id = $2 convierte la propiedad en una condición inseparable de la lectura: aunque el atacante adivine el id, la consulta no devuelve nada si no es el dueño. El req.user.id proviene de la sesión o del token verificado en el servidor, nunca de un parámetro enviado por el cliente.
Escalada de Privilegios
La escalada de privilegios horizontal ocurre cuando un usuario accede a recursos de otro usuario del mismo nivel: por ejemplo, leer los mensajes privados de otra cuenta de cliente. La IDOR es a menudo el vehículo de este tipo de escalada. El atacante no obtiene más poder, pero sí acceso a datos que no le corresponden.
La escalada de privilegios vertical es más grave: un usuario con pocos permisos logra realizar acciones reservadas a roles superiores, como un usuario normal que accede al panel de administración. Esto suele ocurrir cuando la aplicación oculta las funciones administrativas en la interfaz pero no las protege en el servidor, asumiendo erróneamente que "si no se ve el botón, nadie puede llamar a la función". Un atacante que conozca o adivine la URL del endpoint administrativo puede invocarlo directamente. Recuerda el principio: la seguridad no puede depender de ocultar funcionalidad en el cliente.
Código vulnerable
Aquí el panel de administración solo se protege ocultando el enlace en la interfaz. El endpoint del servidor no comprueba el rol, así que confía en que "nadie que no sea admin conocerá esta ruta":
// VULNERABLE: asume que si no se ve el botón, nadie llamará al endpoint
app.delete("/admin/users/:id", requireAuth, async (req, res) => {
await db.query("DELETE FROM users WHERE id = $1", [req.params.id]);
res.json({ deleted: true });
});
Cualquier usuario autenticado que descubra la ruta puede borrar cuentas. La ocultación en el cliente no es un control de seguridad.
Versión segura
La autorización se aplica en el servidor mediante un middleware reutilizable que verifica el rol antes de llegar al manejador. Si el rol no basta, la petición se rechaza con 403:
// Middleware de autorización centralizado y reutilizable
function requireRole(role) {
return (req, res, next) => {
if (!req.user || req.user.role !== role) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
// SEGURO: el rol se verifica en el servidor, en cada request
app.delete(
"/admin/users/:id",
requireAuth,
requireRole("admin"),
async (req, res) => {
await db.query("DELETE FROM users WHERE id = $1", [req.params.id]);
res.json({ deleted: true });
}
);
El rol se lee de req.user, que el servidor construye a partir de la sesión autenticada, y se compara contra el rol exigido. Al ser un middleware, la misma verificación se reutiliza en todos los endpoints administrativos sin duplicar lógica ni arriesgarse a olvidarla en alguno.
Otros Patrones de Control de Acceso Roto
Más allá de la IDOR y la escalada, existen otros patrones frecuentes. El forced browsing consiste en acceder directamente a URLs o archivos no enlazados desde la interfaz, descubriendo paneles o recursos que se creían ocultos. La manipulación de metadatos ocurre cuando un atacante modifica un token, una cookie o un campo (por ejemplo un campo role=user) para elevar sus permisos.
También entran aquí los problemas de CORS mal configurado que permiten que sitios no autorizados consuman APIs protegidas, y los fallos donde una API confía en parámetros del cliente para decidir el rol o el tenant. Un caso típico de CORS peligroso es reflejar el Origin recibido y permitir credenciales sin validar el origen:
// VULNERABLE: refleja cualquier Origin y habilita credenciales
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header("Access-Control-Allow-Credentials", "true");
next();
});
// SEGURO: solo se permiten orígenes de una lista blanca explícita
const ALLOWED_ORIGINS = new Set([
"https://app.ejemplo.com",
"https://admin.ejemplo.com",
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && ALLOWED_ORIGINS.has(origin)) {
res.header("Access-Control-Allow-Origin", origin);
res.header("Access-Control-Allow-Credentials", "true");
res.header("Vary", "Origin");
}
next();
});
El denominador común de todos estos casos es el mismo: la decisión de autorización se toma en un lugar manipulable o no se toma en absoluto. Cada acción sensible debe preguntarse explícitamente "¿este usuario, en este momento, tiene derecho a hacer esto sobre este recurso concreto?".
Cómo Prevenir el Control de Acceso Roto
La regla de oro es denegar por defecto: ninguna acción debe permitirse a menos que exista una verificación explícita que la autorice. Centraliza la lógica de control de acceso en un único componente reutilizable —un middleware, un guard, una capa de política— en lugar de dispersar comprobaciones ad-hoc por todo el código; así es más difícil olvidar una y más fácil auditar todas. Verifica siempre en el servidor que el usuario autenticado es el propietario del recurso o tiene el rol necesario, en cada request, sin confiar en lo que envíe el cliente.
Adopta un modelo claro de control de acceso, como RBAC (basado en roles) o ABAC (basado en atributos), y aplica el principio de mínimo privilegio: cada rol recibe solo los permisos que necesita, nada más. Nunca confíes en campos de rol, tenant o owner enviados por el cliente en el cuerpo o la query: derívalos siempre de la identidad autenticada en el servidor. Evita usar identificadores predecibles como único criterio de acceso; los UUID dificultan la enumeración, pero un UUID no es un control de acceso por sí mismo, solo una capa adicional: la verificación de propiedad sigue siendo obligatoria. Configura un CORS restrictivo con lista blanca de orígenes y registra los intentos de acceso denegados para detectar abusos.
Herramientas de Detección
Para probar el control de acceso, la técnica más eficaz es trabajar con varias cuentas de distinto privilegio (por ejemplo, dos usuarios normales y un administrador) e intentar acceso cruzado. Con un proxy como Burp Suite o OWASP ZAP se capturan las peticiones de una cuenta y se reenvían sustituyendo el token o la sesión por los de otra cuenta, comprobando si el servidor entrega datos ajenos. La extensión Autorize de Burp automatiza parte de este contraste entre sesiones.
Aun así, conviene ser realista: el control de acceso es difícil de automatizar porque las herramientas no conocen la lógica de negocio ni qué recurso debería ver cada usuario. Un escáner puede señalar un endpoint sin autenticación, pero no sabe que la factura 1025 pertenece a otro cliente. Por eso la revisión manual del código y las pruebas dirigidas son insustituibles: hay que leer los manejadores sensibles y confirmar, uno por uno, que cada acción verifica propiedad y rol del lado del servidor.
Checklist de Prevención
- Denegar por defecto: toda ruta y acción se rechaza salvo autorización explícita.
- Verifica propiedad del recurso en el servidor en cada request (por ejemplo
WHERE id = $1 AND owner_id = $2). - Verifica el rol con un middleware/guard centralizado (
requireRole), nunca ocultando botones en el cliente. - Centraliza la lógica de autorización en un componente reutilizable en lugar de comprobaciones dispersas.
- Deriva
user.id,roleytenantde la sesión o token verificado, nunca de parámetros del cliente. - Aplica RBAC/ABAC y mínimo privilegio: cada rol solo con los permisos imprescindibles.
- Devuelve 404 para recursos ajenos y no reveles su existencia.
- No uses identificadores predecibles como único control; los UUID son una capa, no una autorización.
- Configura CORS restrictivo con lista blanca de orígenes; nunca reflejes
Origincon credenciales. - Prueba con varias cuentas de distinto privilegio usando Burp Suite u OWASP ZAP, y complementa siempre con revisión manual.
- Registra y monitoriza los accesos denegados para detectar intentos de abuso.