← Volver al blog

[Authorize] no es autorización: cómo detecté una escalada de privilegios en ASP.NET Core MVC

  • .NET
  • Backend
  • Security
  • ASP.NET Core
  • MVC

Descubre por qué la etiqueta [Authorize] no es suficiente para asegurar los endpoints en ASP.NET Core y cómo solucioné una grave escalada de privilegios en el backend implementando autorización explícita por rol.

TL;DR: Durante una revisión, detecté que el rol Teacher podí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.

⚠️Warning

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.

ℹ️Info

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:

  1. La autenticación no sustituye a la autorización. Un usuario logueado no debe tener todos los privilegios.
  2. La UI no es un control de seguridad.
  3. No confíes en nada que venga del cliente. Valida y autoriza siempre en el servidor.
  4. Revisa tu código con ojos de atacante, no solo de desarrollador.

¡Hasta más ver!