Skip Content

A09: Fallos de Logging y Alertas de Seguridad (Blog)

¿Que es un fallo de logging y alertas?

Estos fallos ocurren cuando la aplicacion no genera las senales de seguridad correctas, en el momento correcto y hacia el destino correcto. Puedes tener logs, pero sin eventos accionables, sin contexto y sin alertas.

Laravel 12 ofrece logging estructurado con canales, stacks, integracion con Slack y hooks de reporte de excepciones en bootstrap/app.php.

Fallo tipico en Laravel

  • Logueas muy poco, muy tarde o cosas incorrectas.
  • Logueas errores pero no eventos de seguridad.
  • Sin alertas por abuso de auth, acciones admin, cambios de rol, OTP fallido, mal uso de tokens.

Impacto

  • Ataques pasan desapercibidos (o se detectan semanas despues).
  • Sin rastro de auditoria para incident response.
  • Fallas de cumplimiento si se requieren eventos de seguridad.

Remediacion en Laravel 12

Loguea eventos relevantes de seguridad:

  • login exito/fallo
  • password reset solicitado/completado
  • MFA habilitado/deshabilitado
  • cambios de roles/permisos
  • token creado/revocado
  • exportacion/descarga de datos sensibles
  • acciones solo-admin

Eventos tipicos en un blog:

  • post creado/publicado
  • comentario creado
  • comentario editado/eliminado

Guia operativa:

  • Usa stacks de canales para archivo + Slack/SIEM.
  • Agrega contexto: user id, tenant id, IP, user agent, correlation id.
  • Nunca loguees secretos ni payloads sensibles completos.

Arreglo concreto

  1. Enruta eventos de seguridad a un stack de canales (archivo + Slack/SIEM):
// config/logging.php (ejemplo)
 
'channels' => [
// ...
'security' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
'ignore_exceptions' => false,
],
],
  1. Agrega contexto por request una sola vez (correlation id, actor, tenant, cliente):
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
 
$correlationId = $request->header('X-Request-Id') ?? (string) Str::uuid();
 
Log::withContext([
'correlation_id' => $correlationId,
'user_id' => $request->user()?->id,
'tenant_id' => $request->user()?->getAttribute('tenant_id'),
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
  1. Emite un evento de seguridad con payload minimo y seguro (nunca loguees secretos):
use Illuminate\Support\Facades\Log;
 
Log::channel('security')->warning('auth.login_failed', [
'email' => $request->string('email')->toString(),
'reason' => 'invalid_credentials',
]);
 
Log::channel('security')->info('auth.password_reset_requested', [
'email' => $request->string('email')->toString(),
]);
 
Log::channel('security')->notice('admin.role_changed', [
'target_user_id' => $targetUser->id,
'new_role' => $newRole,
]);
 
Log::channel('security')->notice('blog.post_created', [
'post_id' => $post->id,
'actor_user_id' => $request->user()?->id,
]);
 
Log::channel('security')->info('blog.comment_created', [
'post_id' => $post->id,
'comment_id' => $comment->id,
'actor_user_id' => $request->user()?->id,
]);

Enfoque de patron de diseno

Trata los logs de seguridad como un producto: nombres de eventos consistentes, ruteo consistente (canal security), contexto consistente (Log::withContext()), y payload minimo.

Usa dos capas:

  • Un inicializador de contexto por request (Middleware) que setea correlation_id, user_id, tenant_id, ip, user_agent una sola vez.
  • Un puerto SecurityAudit que emite eventos normalizados (abuso de auth, acciones admin, ciclo de vida de tokens) al canal security.
use Illuminate\Support\Facades\Log;
 
interface SecurityAudit
{
public function failedLogin(string $email, string $ip, string $userAgent, ?int $tenantId = null): void;
public function passwordResetRequested(string $email, string $ip, ?int $tenantId = null): void;
public function roleChanged(int $targetUserId, string $newRole, int $actorUserId, ?int $tenantId = null): void;
}
 
final class LaravelSecurityAudit implements SecurityAudit
{
public function failedLogin(string $email, string $ip, string $userAgent, ?int $tenantId = null): void
{
Log::channel('security')->warning('auth.login_failed', [
'email' => $email,
'ip' => $ip,
'user_agent' => $userAgent,
'tenant_id' => $tenantId,
]);
}
 
public function passwordResetRequested(string $email, string $ip, ?int $tenantId = null): void
{
Log::channel('security')->info('auth.password_reset_requested', [
'email' => $email,
'ip' => $ip,
'tenant_id' => $tenantId,
]);
}
 
public function roleChanged(int $targetUserId, string $newRole, int $actorUserId, ?int $tenantId = null): void
{
Log::channel('security')->notice('admin.role_changed', [
'target_user_id' => $targetUserId,
'new_role' => $newRole,
'actor_user_id' => $actorUserId,
'tenant_id' => $tenantId,
]);
}
}
 
// Idea de Middleware: inicializar contexto una vez por request
 
final class SecurityLogContextMiddleware
{
public function handle($request, $next)
{
$correlationId = $request->header('X-Request-Id') ?? (string) \Illuminate\Support\Str::uuid();
 
\Illuminate\Support\Facades\Log::withContext([
'correlation_id' => $correlationId,
'user_id' => $request->user()?->id,
'tenant_id' => $request->user()?->getAttribute('tenant_id'),
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
 
return $next($request);
}
}
 
// Luego SecurityAudit emite eventos solo con campos especificos del evento.

Referencias