← Volver al blog

Seguridad en ASP.NET Core: Peligros de ex.Message en Producción y Logs Vacíos

  • .NET
  • Backend
  • Security
  • ASP.NET Core
  • Logging

Devolver ex.Message y no tener logging estructurado son dos graves problemas de seguridad. Aprende a manejar excepciones y logs en ASP.NET Core.

TL;DR: Durante una auditoría de seguridad en una app ASP.NET Core MVC encontré controladores que devolvían ex.Message directamente al cliente y acciones críticas sin ningún evento registrado. Dos problemas opuestos, misma causa raíz: no hay contrato claro sobre qué información va a cada capa. En este post explico el patrón que apliqué para resolverlo con ILogger estructurado y respuestas de error genéricas.


Ejemplo de vulnerabilidad: Fuga de información en un controlador de ASP.NET Core

Revisando los controladores de la aplicación durante una auditoría, encontré varios bloques catch similares a este:

public APIResponse UpdateMiniGameData([FromBody] MiniGameDataStructDto mgs)
{
	try
	{
		var oldMgs = _context.MiniGameDataStructs.FirstOrDefault(m => m.id == mgs.id);
		oldMgs.score = mgs.score;
		oldMgs.trys = mgs.trys;
		_context.MiniGameDataStructs.Update(oldMgs);
		_context.SaveChanges();

		return new APIResponse() { Success = true };
	}
	catch (Exception ex)
	{
		return new APIResponse() { Success = false, Error = ex.Message };
	}
}
⚠️Aviso

Este es el código tal como estaba en el momento de la auditoría. El problema que nos ocupa aquí es la línea del catch, especialmente el hecho de que la respuesta de la API incluya el mensaje de la excepción. La lógica de negocio en el controlador, la falta de async, no comprobar ownership... quedan fuera del alcance de este post.

A primera vista este bloque try-catch en C# parece inofensivo. Pero ex.Message puede contener nombres de tabla, rutas de ficheros internos, reglas de dominio... ofreciendo al usuario mucha más información de la que necesita y, si estamos ante un atacante, le estamos entregando un mapa parcial de nuestro sistema en cada error. Esto es lo que se conoce como Information Leakage o fuga de información.

Y esta es la otra cara de la moneda: ese catch no estaba registrando absolutamente nada. Si algo fallaba en producción, era imposible saber qué había pasado, sobre qué recurso, ni desde dónde. Estábamos ciegos al carecer de un logging.


Fuga de datos (Data Exposure) y la falta de Logging en APIs

Antes de entrar en la solución que apliqué merece la pena señalar por separado los errores, porque se resuelven de forma distinta:

El servidor habla demasiado hacia afuera. El cliente recibe detalles técnicos que no necesita - y que pueden transmitir una sensación de producto poco pulido - y, en el peor de los casos, un atacante puede aprovechar estos detalles técnicos para conocer mejor el sistema y afinar su puntería.

El servidor calla demasiado hacia adentro. El catch se ejecuta, hay una excepción, pero no se registra absolutamente nada. Si no registramos los eventos de seguridad críticos en pos de la System Security, cualquier incidente de seguridad será invisible para el desarrollador.

La misma línea de código que filtra demasiado al cliente generalmente no registra nada útil para ti. Son las dos caras del mismo descuido.


Qué información no debes devolver nunca en una respuesta HTTP

El problema con devolver ex.Message no es solo teórico. Dependiendo del stack, ese mensaje puede causar una vulnerabilidad advertida en el artículo de Information Exposure de OWASP y puede contener:

  • Nombres de columnas o tablas de base de datos
  • Rutas absolutas del servidor
  • Reglas de negocio que revelan lógica interna
  • Versiones de dependencias con vulnerabilidades conocidas

La solución es separar lo que el cliente recibe de lo que queda registrado:

[HttpPost("UpdateMiniGameData")]

public async Task<APIResponse> UpdateMiniGameData([FromBody] MiniGameDataStructDto mgs)
{
	try
	{
		var oldMgs = await _context.MiniGameDataStructs.FirstOrDefaultAsync(m => m.id == mgs.id);
		if (oldMgs == null) 
			return new APIResponse { Success = false, Error = "Not found" };
		if (!await _authService.OwnsGameDataResource(oldMgs.GameDataId))
			return new APIResponse { Success = false, Error = "Forbidden" };
		oldMgs.score = mgs.score;		
		oldMgs.trys = mgs.trys;
		_context.MiniGameDataStructs.Update(oldMgs);
		await _context.SaveChangesAsync();
		
		return new APIResponse() { Success = true };
	}
	catch (Exception ex)
	{
		_logger.LogError(ex, "Error en {Endpoint} para MiniGameDataStruct {Id}", nameof(UpdateMiniGameData), mgs?.id);
		
		return new APIResponse() { Success = false, Error = "Ha ocurrido un error" };
	}
}

El cliente recibe un mensaje genérico consistente. El detalle técnico va al log, donde solo tú puedes verlo.


Cómo implementar un Logging Estructurado con ILogger en ASP.NET

Parte de la seguridad de un sistema está en un logging estructurado mediante interfaces como ILogger de Microsoft que registre acciones críticas. No disponer de logging en un sistema equivale a no saber si te están atacando.

Los eventos mínimos que se recomienda que queden registrados en un sistema con autenticación son:

  • Login exitoso y fallido (con usuario e IP)
  • Cambio o reseteo de contraseña, identificando claramente qué usuario realizó la acción.
  • Denegaciones de acceso por ownership o rol: es vital vigilar las políticas de autorización y registrar a quién se le deniega el acceso a un recurso.
  • Acciones administrativas sensibles
  • Exportaciones de datos
[HttpPost]
public async Task<IActionResult> Login(LoginDto model)
{
   var result = await _signInManager.PasswordSignInAsync(
       model.Email, model.Password, false, lockoutOnFailure: true);

   if (!result.Succeeded)
   {
       _logger.LogWarning("Login fallido para {Email} desde {IP}",
           model.Email,
           HttpContext.Connection.RemoteIpAddress);
       return View(model);
   }

   _logger.LogInformation("Login exitoso para {Email}", model.Email);
   return RedirectToAction("Index", "Home");
}

Esto es lo mínimo para que cuando pase algo en tu aplicación, puedas identificar quién lo provocó, qué recurso estaba implicado y en qué momento sucedió. Un buen punto de partida para realizar un seguimiento de lo que ha pasado.

Nuestro Logging debe incluir eventos con propiedades tipadas para poder filtrar, agregar, correlacionar y analizar para detectar brechas y bugs o ataques que de otra manera podrían pasar inadvertidos.


Lo que queda pendiente

Resolver estos problemas endpoint por endpoint es el primer paso, pero deja deuda técnica documentada para fases siguientes:

  • Middleware global de manejo de errores: centralizar el catch en un Global Exception Handler o Middleware en ASP.NET Core en lugar de repetir el patrón en cada controlador. Un solo punto de control para toda la app.
  • Correlación de requests: añadir un traceId o correlationId a cada respuesta de error y al log correspondiente. Cuando llegue un incidente, el usuario puede reportar ese ID y tú puedes encontrar el evento exacto en los logs.
  • Destino de logs estructurados: Implementar Serilog, Seq, Application Insights de Azure o cualquier agregador de logs estructurados para consultar y analizar estos eventos.

Lo que aprendí

La seguridad no está solo en crear un sistema capaz de bloquear ataques. El sistema debe poder detectarlos, reconstruirlos y entender qué ocurrió cuando algo falla.

Un sistema que bloquea correctamente pero no tiene un logging estructurado que registre el incidente, resuelve el problema pero no lo entiende. Un sistema que registra demasiado hacia el cliente le regala al atacante valiosa información para afinar su siguiente intento.

El cliente no necesita saber por qué falló, tú sí.