TL;DR: Durante una revisión, detecté que el rol
Teacherpodía acceder al CRUD y a controladores globales exclusivos de administradores simplemente navegando a ciertas URLs, ya que solo estaban protegidos con[Authorize]básico. La solución para acotar esta escalada de privilegios consistió en aplicar autorización estricta mediante el decorador[Authorize(Roles = "Admin")]a nivel de clase o de los endpoints concretos que lo requerían, devolviendo un 403 a los no autorizados.
Autorización por Roles en ASP.NET Core MVC: Cómo evitar la escalada de privilegios
Repasando los controladores de un backend ASP.NET Core MVC me di cuenta de varios errores muy típicos en proyectos realizados rápidamente: confundir autenticación con autorización. El sistema pedía a todos los usuarios iniciar sesión, lo que era absolutamente correcto, pero completamente insuficiente cuando revisé los controladores en profundidad. Crear un modelo Role-Based Access Control (RBAC), con claims de roles en las cookies pero no usarlo donde toca, es lo mismo que no crearlo.
El rol Teacher podía acceder fácilmente - simplemente cambiando la url tras iniciar sesión - a controladores y vistas que por diseño solo debían ser accesibles para Admin.
Es un error clásico considerar que poner [Authorize]ya protege el endpoint en su totalidad. Pero autenticación ≠ autorización. Solo por estar autenticado no deberías poder acceder a cualquier endpoint.
El layout del frontend - con el menú de la aplicación- "protegía" el acceso al ocultar, según el rol, los botones de acceso a las vistas exclusivas para admins. Y digo "protegía" porque podías simplemente escribir la url, cambiar la vista y acceder a todos los privilegios del admin porque apenas habían unos pocos endpoints protegidos por rol.
Riesgos de seguridad al depender solo del atributo [Authorize]
A continuación muestro algunos de los ejemplos más problemáticos para el diseño de la aplicación y del propio negocio. El profesor podía acceder al CRUD de entidades globales que estaban pensadas para administradores.
Ejemplos protegidos por [Authorize]pero no por rol:
/Teachers/Delete/5
/Licenses/Create
/Schools/Edit/3
En el backend ya existía un filtro global de autenticación y el login de Teacher/Admin emitía un claim de rol correctamente, pero luego apenas se usaban para autorizar o desautorizar el acceso a endpoints o controladores concretos. Por tanto, había que ponerse a revisar los controladores y cerrar la escalada de privilegios existente sin romper los flujos legítimos de los profesores.
Autorización por atributos de rol y bloqueo por clase en controladores 100% Admin
Elegí utilizar un modelo de autorización basado en atributos sin llegar a aplicar policies por agilidad en la implementación y porque la "lógica" detrás de la autorización de acceso era simplemente ser Admin o no.
Generalmente se recomienda utilizar Policies porque centraliza la configuración y permite inyectar dependencias y una lógica más compleja para autorizar/desautorizar.
Configurar [Authorize(Roles = "Admin")] en Controladores
En muchos casos, los controladores eran 100% de administradores y bastó con poner [Authorize(Roles = "Admin")] a nivel de clase apoyándonos en la documentación sobre Autorización basada en Roles de Microsoft Learn:
[Authorize(Roles = "Admin")]
public class LicensesController : Controller
{
//...
}
Otros controladores tuvieron que ser analizados más cuidadosamente para no impedir el acceso de los profesores a sus propios recursos:
public class TeachersController : Controller
{
//..
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(int? id)
{
//...
}
//...
[Authorize]
public async Task<IActionResult> UserInfo()
{
//...
}
//...
}
Una vez ejecutados los cambios, el profesor ya no podía acceder a los controladores y endpoints protegidos recibiendo una vista con el 403.
builder.Services.AddAuthentication(...)
.AddCookie(options => {
options.AccessDeniedPath = "/Home/AccessDenied";
});
Conclusiones sobre Seguridad en ASP.NET MVC
Tal y como vimos en el primer post de la serie sobre cómo evitar vulnerabilidades IDOR, no debemos confiar ciegamente en lo que el cliente nos envía o a dónde intenta navegar de manera premeditada. De este caso podemos asentar varios conceptos clave:
- La autenticación no sustituye a la autorización. Un usuario logueado no debe tener todos los privilegios.
- La UI no es un control de seguridad.
- No confíes en nada que venga del cliente. Valida y autoriza siempre en el servidor.
- Revisa tu código con ojos de atacante, no solo de desarrollador.
¡Hasta más ver!