Cerramos el módulo 2 anunciando que en el módulo 3 recorreríamos el Top Ten categoría a categoría sobre el código real de BazarNube, convirtiendo el vocabulario en conocimiento operativo. Empezamos por donde empieza la lista de 2021: A01 – Pérdida de Control de Acceso (Broken Access Control). No es casualidad que ocupe el primer puesto: es la categoría más extendida, la que más aplicaciones afecta y la que más impacto directo tiene sobre la confidencialidad y la integridad de los datos.

El control de acceso decide qué puede hacer cada usuario autenticado. Cuando falla, un cliente normal de BazarNube puede leer los pedidos de otro cliente, un usuario sin permisos puede acceder al panel de administración, o cualquiera puede modificar recursos que no le pertenecen. En este curso lo ilustraremos con un caso muy típico: un endpoint de la API Node/Express de BazarNube que devuelve pedidos usando directamente el identificador que llega en la URL, sin comprobar si ese pedido es del usuario que pregunta.

Aviso legal y ético: todas las técnicas de ataque que se muestran son ilustrativas y con datos ficticios. Practica solo sobre sistemas propios o con autorización explícita y por escrito. Probar control de acceso sobre sistemas de terceros sin permiso es delito en la mayoría de jurisdicciones.

Contenido

  1. Autenticación vs. autorización: no confundir A01 con A07
  2. IDOR y referencias directas inseguras a objetos
  3. Escalada horizontal y vertical de privilegios
  4. Forzado de rutas y control de acceso en el cliente
  5. CORS mal configurado como fallo de control de acceso
  6. Principios de prevención: denegar por defecto, autorizar en el servidor
  7. Modelos de autorización: RBAC y ABAC
  8. Errores comunes y consejos
  9. Ejercicios y soluciones

  1. Autenticación vs. autorización

Son dos controles distintos y consecutivos:

Concepto Pregunta que responde Categoría OWASP
Autenticación ¿Quién eres? ¿Eres quien dices ser? A07 (lección 03-09)
Autorización / Control de acceso Una vez identificado, ¿qué puedes hacer? A01 (esta lección)

Un sistema puede autenticar perfectamente (login sólido, MFA, sesiones bien gestionadas) y aun así tener el control de acceso roto: sabe quién eres, pero no comprueba si puedes acceder al recurso que pides. A01 se ocupa de este segundo control.

  1. IDOR y referencias directas inseguras a objetos

Un IDOR (Insecure Direct Object Reference) ocurre cuando la aplicación expone una referencia a un objeto interno (un id de base de datos, un nombre de fichero) y decide el acceso solo a partir de ese valor controlado por el usuario, sin verificar la propiedad del recurso.

Código vulnerable en BazarNube

Lucía, la backend lead, escribió este endpoint para consultar el detalle de un pedido:

// routes/orders.js  (API Node/Express de BazarNube) — VULNERABLE
router.get('/api/orders/:orderId', requireLogin, async (req, res) => {
  // requireLogin solo comprueba que hay sesión valida (autenticacion)
  const order = await db.query(
    'SELECT * FROM orders WHERE id = $1',
    [req.params.orderId]
  );
  if (order.rows.length === 0) return res.status(404).json({ error: 'No existe' });
  res.json(order.rows[0]); // devuelve el pedido sea de quien sea
});

El middleware requireLogin garantiza que hay una sesión iniciada, pero nadie comprueba que el pedido pertenezca al usuario de esa sesión. La consulta filtra por id, no por propietario.

Cómo se explota (ilustrativo)

La clienta Ana (usuaria 501) ve su pedido en GET /api/orders/1042. Como los ids son numéricos y correlativos, prueba a incrementarlos:

GET /api/orders/1043   -> pedido de otro cliente (nombre, direccion, importe)
GET /api/orders/1044   -> otro mas

Con un simple bucle puede descargar el historial de pedidos de toda la tienda: datos personales, direcciones y líneas de compra. Es una fuga masiva sin necesidad de "hackear" nada; basta con cambiar un número.

Código corregido

La regla es: el servidor debe atar cada acceso a la identidad de la sesión, no al identificador que envía el cliente.

// routes/orders.js — SEGURO
router.get('/api/orders/:orderId', requireLogin, async (req, res) => {
  const order = await db.query(
    // filtramos TAMBIEN por el propietario tomado de la sesion del servidor
    'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
    [req.params.orderId, req.session.userId]
  );
  // Devolvemos 404 (no 403) para no revelar que el id existe pero es de otro
  if (order.rows.length === 0) return res.status(404).json({ error: 'No existe' });
  res.json(order.rows[0]);
});

Detalles clave:

  • req.session.userId procede del servidor (de la sesión), nunca de un parámetro que el cliente pueda manipular.
  • La condición AND user_id = $2 convierte la comprobación de autorización en parte de la propia consulta: si el pedido no es tuyo, simplemente no existe para ti.
  • Responder 404 en lugar de 403 evita filtrar la existencia de recursos ajenos (un pequeño refuerzo frente a la enumeración).

Como refuerzo adicional, muchos equipos sustituyen los ids secuenciales por UUID o identificadores no adivinables. Ojo: esto dificulta la enumeración, pero no sustituye a la comprobación de propiedad. Un UUID filtrado (en un log, en un email) seguiría siendo accesible. La defensa real es la comprobación en el servidor.

  1. Escalada horizontal y vertical de privilegios

  • Escalada horizontal: accedes a recursos de otro usuario del mismo nivel. El IDOR de arriba es exactamente esto: Ana leyendo pedidos de otros clientes.
  • Escalada vertical: obtienes privilegios de un rol superior. Por ejemplo, un cliente que invoca un endpoint de administración.

Ejemplo vertical en BazarNube

// routes/admin.js — VULNERABLE: solo comprueba login, no el rol
router.post('/api/admin/products/:id/price', requireLogin, async (req, res) => {
  await db.query('UPDATE products SET price = $1 WHERE id = $2',
    [req.body.price, req.params.id]);
  res.json({ ok: true });
});

Cualquier cliente autenticado podría cambiar precios. La corrección exige comprobar el rol en el servidor:

// middleware/authorize.js
function requireRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.session.role)) {
      return res.status(403).json({ error: 'Prohibido' });
    }
    next();
  };
}

// routes/admin.js — SEGURO
router.post('/api/admin/products/:id/price',
  requireLogin, requireRole('admin'), async (req, res) => { /* ... */ });

El rol se toma de req.session.role, establecido en el login a partir de la base de datos, nunca de un campo enviado por el cliente (como un role en el cuerpo o en una cookie manipulable).

  1. Forzado de rutas y control de acceso en el cliente

Un error frecuente en SPAs como el front React de BazarNube es ocultar botones o rutas de administración en el cliente y creer que eso protege. No protege: el navegador es territorio del atacante.

// AdminLink.jsx — esto es UX, NO seguridad
{user.role === 'admin' && <Link to="/admin">Panel</Link>}

Ocultar el enlace mejora la experiencia, pero cualquiera puede navegar directamente a /admin o, más importante, llamar a POST /api/admin/... con curl. Toda decisión de autorización debe repetirse y hacerse cumplir en el servidor. El cliente puede reflejar permisos; el servidor debe imponerlos.

El forzado de rutas (forced browsing) consiste precisamente en pedir directamente URLs no enlazadas (/api/admin, /backup.zip, /api/internal/metrics) confiando en que estén desprotegidas. Se previene aplicando el mismo control de acceso a todos los endpoints, incluidos los que no aparecen en la interfaz.

  1. CORS mal configurado

CORS (Cross-Origin Resource Sharing) controla qué orígenes web pueden leer respuestas de tu API desde el navegador. Una configuración laxa puede convertirse en un fallo de control de acceso.

// VULNERABLE: refleja cualquier origen y ademas permite credenciales
app.use(cors({
  origin: (o, cb) => cb(null, true), // acepta TODOS los origenes
  credentials: true                  // ...enviando cookies de sesion
}));

Reflejar el Origin del atacante junto con credentials: true permite que un sitio malicioso haga peticiones autenticadas a la API de BazarNube en nombre de la víctima y lea las respuestas. La corrección es una lista blanca explícita:

// SEGURO
const allowed = ['https://bazarnube.com', 'https://app.bazarnube.com'];
app.use(cors({
  origin: (o, cb) => cb(null, !o || allowed.includes(o)),
  credentials: true
}));

Nunca uses origin: '*' combinado con credenciales (el propio navegador lo bloquea, pero reflejar el origen es el equivalente peligroso).

  1. Principios de prevención

flowchart TD
  A[Peticion entrante] --> B{Sesion valida?}
  B -- No --> X[401 No autenticado]
  B -- Si --> C{Autorizado para este recurso?}
  C -- No --> Y[403/404 Denegado por defecto]
  C -- Si --> D[Ejecutar accion]

Las reglas de oro de A01:

  1. Denegar por defecto. Todo endpoint parte de "prohibido" y solo se permite lo explícitamente autorizado. Nada queda abierto por olvido.
  2. Autorizar siempre en el servidor, con identidad tomada de la sesión, no de parámetros del cliente.
  3. Centralizar la lógica de autorización en middlewares/servicios reutilizables, no dispersa y copiada endpoint a endpoint (donde es fácil olvidarla).
  4. Comprobar propiedad del recurso en cada acceso a objetos (evita IDOR).
  5. Registrar los fallos de control de acceso y alertar ante patrones de abuso (enlaza con A09, lección 03-11).

  1. Modelos de autorización: RBAC y ABAC

Modelo Idea Ejemplo en BazarNube Cuándo usarlo
RBAC (basado en roles) El permiso depende del rol del usuario admin, soporte, cliente Reglas estables y por rol
ABAC (basado en atributos) El permiso depende de atributos de usuario, recurso y contexto "un vendedor solo edita productos de su tienda" Reglas finas, dependientes del dato

BazarNube combina ambos: RBAC para separar cliente/soporte/admin, y una capa ABAC para "cada vendedor gestiona solo lo suyo". El IDOR de la sección 2 es, en el fondo, una regla ABAC (pedido.user_id == sesion.userId) que faltaba.

Errores Comunes y Consejos

  • Confiar en el cliente. Ocultar botones no es autorizar. Repite la comprobación en el servidor, siempre.
  • Usar el id del cliente como fuente de identidad. Toma el userId/role de la sesión del servidor, nunca del cuerpo, la query o una cookie no firmada.
  • Autorización copiada y pegada. Si cada endpoint reimplementa la comprobación, alguno se olvidará. Centraliza en middlewares.
  • Suponer que un UUID protege. Dificulta enumerar, no autoriza. Mantén la comprobación de propiedad.
  • Olvidar los métodos "secundarios". Proteges GET pero dejas PUT/DELETE abiertos. Cubre todos los verbos y todos los endpoints, incluidos los internos.
  • Consejo: escribe pruebas automáticas de autorización ("el usuario A no puede leer el pedido de B") y ejecútalas en CI. Los IDOR se detectan muy bien con tests.

Ejercicios

Ejercicio 1. El siguiente endpoint de BazarNube permite descargar una factura. Identifica la vulnerabilidad y corrígela.

router.get('/api/invoices/:id/pdf', requireLogin, async (req, res) => {
  const inv = await db.query('SELECT pdf_path FROM invoices WHERE id = $1',
    [req.params.id]);
  res.download(inv.rows[0].pdf_path);
});

Ejercicio 2. Marc propone "arreglar" el IDOR de pedidos cambiando los ids numéricos por UUID y no tocar la consulta. ¿Es suficiente? Justifica.

Ejercicio 3. Clasifica cada escenario como escalada horizontal o vertical: a) Un cliente accede al carrito de otro cliente. b) Un usuario de soporte ejecuta una acción reservada a administración. c) Un vendedor edita un producto de otra tienda.

Soluciones

Solución 1. Es un IDOR: se descarga cualquier factura por id sin comprobar propiedad, y además no valida que la fila exista (fallaría con undefined). Corrección:

router.get('/api/invoices/:id/pdf', requireLogin, async (req, res) => {
  const inv = await db.query(
    'SELECT pdf_path FROM invoices WHERE id = $1 AND user_id = $2',
    [req.params.id, req.session.userId]);
  if (inv.rows.length === 0) return res.status(404).json({ error: 'No existe' });
  res.download(inv.rows[0].pdf_path);
});

Solución 2. No es suficiente. El UUID solo dificulta adivinar ids, pero si uno se filtra (email, log, historial del navegador compartido) el acceso sigue abierto porque no hay comprobación de propiedad. La defensa correcta es añadir AND user_id = $2. El UUID es una capa complementaria, no la principal.

Solución 3. a) horizontal; b) vertical; c) horizontal (mismo nivel de rol, distinto propietario del recurso; es una regla ABAC).

Conclusión

La pérdida de control de acceso encabeza el Top Ten porque es fácil de introducir (basta olvidar una comprobación) y muy dañina (fuga o manipulación de datos ajenos). Las claves que anotamos en el backlog de BazarNube: denegar por defecto, tomar la identidad de la sesión del servidor, comprobar propiedad y rol en cada acceso, centralizar la autorización y no confiar nunca en el cliente.

Entrada de backlog — A01: corregido el IDOR en GET /api/orders/:orderId (añadido filtro por user_id), añadido requireRole('admin') a los endpoints de administración y sustituido el CORS reflejado por lista blanca. Pendiente: tests de autorización en CI.

Una vez que sabemos que solo la persona adecuada accede a un dato, surge la siguiente pregunta: cuando ese dato viaja o se guarda, ¿está protegido? Eso nos lleva a la siguiente lección, A02:2021 – Fallos Criptográficos y Exposición de Datos Sensibles, donde veremos cómo BazarNube protege contraseñas, tarjetas y tráfico.

Curso de OWASP: Directrices y Estándares para la Seguridad en Aplicaciones Web

Módulo 1: Introducción a OWASP

Módulo 2: Principales Proyectos de OWASP

Módulo 3: OWASP Top Ten 2021 en Profundidad

Módulo 4: OWASP ASVS (Application Security Verification Standard)

Módulo 5: OWASP SAMM (Software Assurance Maturity Model)

Módulo 6: OWASP ZAP (Zed Attack Proxy)

Módulo 7: Buenas Prácticas y Recomendaciones

Módulo 8: Ejercicios Prácticos y Casos de Estudio

Módulo 9: Evaluación y Certificación

© Copyright 2026. Todos los derechos reservados