A09: Security Logging and Alerting Failures (Blog)
What is a security logging and alerting failure?
Security Logging and Alerting Failures happen when your application does not produce the right security signals, at the right time, to the right destination.
You may have logs, but no actionable security events, no context, and no alerting.
Laravel 12 provides structured logging through channels, stacks, Slack integration, and exception reporting hooks in bootstrap/app.php.
Typical Laravel failure
- You log too little, too late, or the wrong things.
- You log errors but not security events.
- No alerting on auth abuse, admin actions, role changes, failed OTP, token misuse.
Impact
- Attacks go undetected (or are detected weeks later).
- No audit trail for incident response.
- Compliance failures when security events are required.
Laravel 12 remediation
Log security-relevant events:
- login success/failure
- password reset requested/completed
- MFA enabled/disabled
- role/permission changes
- token created/revoked
- export/download of sensitive data
- admin-only actions
Blog-specific events:
- post created/published
- comment created
- comment deleted/edited
Operational guidance:
- Use channel stacks for file + Slack/SIEM.
- Add context: user id, tenant id, IP, user agent, correlation id.
- Never log secrets or full sensitive payloads.
Concrete fix
- Route security events to a channel stack (file + Slack/SIEM):
// config/logging.php (example) 'channels' => [ // ... 'security' => [ 'driver' => 'stack', 'channels' => ['daily', 'slack'], 'ignore_exceptions' => false, ],],
- Add per-request context once (correlation id, actor, tenant, client):
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(),]);
- Emit a security event with minimal, safe payload (never log secrets):
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,]);
Design pattern angle
Treat security logs as a product: consistent event names, consistent routing (security channel), consistent context (Log::withContext()), and minimal payload.
Use two layers:
- A request-level context initializer (Middleware) that sets
correlation_id,user_id,tenant_id,ip,user_agentonce. - A
SecurityAuditport that emits normalized security events (auth abuse, admin actions, token lifecycle) to thesecuritychannel.
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, ]); }} // Middleware idea: initialize context once per 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()?->tenant_id, 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), ]); return $next($request); }} // Then SecurityAudit emits events with only event-specific fields.