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
- 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, ],],
- 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(),]);
- 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_agentuna sola vez. - Un puerto
SecurityAuditque emite eventos normalizados (abuso de auth, acciones admin, ciclo de vida de tokens) al canalsecurity.
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.