Insufficient Logging occurs when an application fails to record security-relevant events (or logs them with insufficient detail). Without a proper audit trail, it becomes difficult or impossible to detect malicious activity, respond to an incident, or perform forensic analysis after a breach. Attackers rely on poor logging to cover their tracks and remain undetected. This is a foundational failure that makes all other security measures harder to enforce and audit. 📜🕳️Common Missing Log Events:
Failed login attempts, especially repeated ones (brute force).
Successful login events (to track user activity).
Password reset requests and successful resets.
Access control failures (e.g., user trying to access an admin page).
High-value transactions (e.g., payments, fund transfers, email changes).
Insufficient logging is a “meta-vulnerability” that blinds security teams and system administrators:
Undetected Breaches: Attackers can probe for vulnerabilities, guess passwords, or exfiltrate data without triggering any alarms.
Inability to Respond: During an incident, security teams cannot determine the attacker’s entry point, what data was accessed, or which accounts were compromised.
Failed Forensics: After a breach, it’s impossible to establish a timeline of the attack, assess the full damage, or gather evidence.
Compliance Failures: Nearly all security regulations (PCI-DSS, GDPR, HIPAA, SOX) mandate detailed logging of access and events related to sensitive data.
Reference Details
CWE ID:CWE-778 (Related: CWE-223 Omission of Security-relevant Information)
OWASP Top 10 (2021): A09:2021 - Security Logging and Monitoring Failures
Severity: Medium (Lowers the ability to detect High/Critical attacks)
This is a design and implementation issue. Frameworks provide powerful logging tools (e.g., Python’s logging, Log4j/Logback in Java, Monolog in PHP, ILogger in .NET), but developers must proactively choose to log the correct events with sufficient context.Key Remediation Principles:
Log What Matters: Ensure all security-critical events (logins, logouts, failures, privilege changes, high-value data access/modification) are logged.
Log Sufficient Context: Logs must include who (User ID, IP address), what (event type, e.g., “Login Failed”), when (timestamp), and where (source component, endpoint).
Log at Appropriate Levels: Use standard logging levels (INFO, WARN, ERROR, CRITICAL). Log failures (like failed logins) at WARN or ERROR level. Log successes (like login) at INFO.
Protect the Logs: Ensure log files have correct permissions (not world-readable), are protected from tampering, and do not contain sensitive data in cleartext (see CWE-532).
Centralize and Monitor: Ship logs from all application instances to a central log management solution (e.g., ELK Stack, Splunk, Graylog). Create dashboards and alerts for suspicious event patterns (e.g., high rate of failed logins).
Using Python’s built-in logging module. Django and Flask are pre-configured to use it, but developers must add explicit log statements for business logic events.
# views.py (Django)def custom_login(request): # ... logic to authenticate user ... if user is None: # DANGEROUS: Failed login attempt is not logged. # Attacker can brute-force without leaving a trace. return render(request, 'login.html', {'error': 'Invalid credentials'}) login(request, user) return redirect('dashboard')
Use Python’s logging module to explicitly log security events with context (user ID, IP). Django’s user_logged_in and user_login_failed signals can also be used to log auth events.
Perform sensitive security actions: failed logins, successful logins, access denied on protected pages, password resets. After each action, check the application logs (file, console, log management system). Verify that a log entry was created for each event, containing the timestamp, event type, user ID (if available), and source IP address.
// config/CustomAuthProvider.javapublic class CustomAuthProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication auth) throws AuthenticationException { // ... load user, check password ... if (password_is_incorrect) { // DANGEROUS: Throws exception, but failure is not explicitly logged // with username and IP. Spring Security might log a generic // "Bad credentials" message at DEBUG level, which is often disabled in prod. throw new BadCredentialsException("Invalid credentials"); } // ... success logic ... } // ...}
Vulnerable Scenario 2: Access Control Failure Not Logged
// controller/AdminController.java@GetMapping("/admin/panel")public String getAdminPanel(Principal principal) { User user = userService.findByUsername(principal.getName()); if (!user.isAdmin()) { // DANGEROUS: Returns 403, but no log entry is created // to record that this user tried to access an admin function. throw new AccessDeniedException("Forbidden"); } // ... return admin panel ...}
Spring Security: Implement ApplicationListener beans to listen for AuthenticationFailureBadCredentialsEvent and AuthenticationSuccessEvent and log them with IP and username.
Explicit Logging: Use SLF4J/Logback Logger to explicitly log WARN or ERROR messages for access control failures, including the authenticated user and their IP.
// config/AuthenticationEventListener.java (Secure)import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.ApplicationListener;import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;import org.springframework.security.authentication.event.AuthenticationSuccessEvent;import org.springframework.stereotype.Component;// Assume WebAuthenticationDetails is available for IP@Componentpublic class AuthenticationEventListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> { private static final Logger log = LoggerFactory.getLogger(AuthenticationEventListener.class); @Override public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) { String username = event.getAuthentication().getName(); // Get IP (requires request context or details object) // String ip = ((WebAuthenticationDetails) event.getAuthentication().getDetails()).getRemoteAddress(); String ip = "IP_NOT_CAPTURED_IN_EXAMPLE"; // Placeholder // SECURE: Log failed login at WARN level log.warn("Login Failed: Username '{}', IP {}", username, ip); }}// Implement a similar listener for AuthenticationSuccessEvent at INFO level.// controller/AdminController.java (Secure)@GetMapping("/admin/panel")public String getAdminPanel(Principal principal) { User user = userService.findByUsername(principal.getName()); if (!user.isAdmin()) { // SECURE: Log the access control failure // Get IP from HttpServletRequest (needs to be injected or available) String ip = "IP_NOT_CAPTURED_IN_EXAMPLE"; // Placeholder log.warn("Access Denied: User '{}' (not admin) " + "tried to access /admin/panel. IP {}", user.getUsername(), ip); throw new AccessDeniedException("Forbidden"); } // ... return admin panel ...}
Perform failed logins, successful logins, and attempts to access unauthorized endpoints. Check application logs (console, file, log aggregator) for corresponding WARN and INFO messages. Verify that the user ID and source IP are included in the log entries.
Using ILogger provided by ASP.NET Core. Identity framework logs some events by default (often at Debug or Information level), but business logic failures require manual logging.
Vulnerable Scenario 1: Login Failure Not Logged Sufficiently
// Controllers/AccountController.cspublic async Task<IActionResult> Login(LoginViewModel model){ if (ModelState.IsValid) { var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true); if (result.Succeeded) { // Identity logs this success at INFO level (good) _logger.LogInformation("User logged in."); return RedirectToLocal(model.ReturnUrl); } if (result.IsLockedOut) { // DANGEROUS: Lockout is a critical security event but might // only be logged by Identity at DEBUG/INFO, or not at all. // Should be logged at WARN level. _logger.LogWarning("User account locked out: {Email}", model.Email); // Good, but might be missing } else { // DANGEROUS: Failed attempt not explicitly logged at WARN level. // Identity's default log might be DEBUG/INFO, which is often off in prod. ModelState.AddModelError(string.Empty, "Invalid login attempt."); } } return View(model);}
Vulnerable Scenario 2: Authorization Failure Not Logged
A user is denied access by an [Authorize] attribute, but this failure isn’t logged, making recon scans invisible.
// Controllers/AdminController.cs[Authorize(Roles = "Admin")]public class AdminController : Controller{ public IActionResult Index() // Non-admin user tries to access /Admin/Index { // The [Authorize] attribute blocks them (good), // but DANGEROUS: No log event is generated by default // to show that 'user_abc' tried to access this. return View(); }}
Explicitly Log Auth Failures: In login actions, explicitly log _logger.LogWarning for failed attempts (!result.Succeeded), lockouts (result.IsLockedOut), and MFA requirements (result.RequiresTwoFactor), including username and IP.
Log Authorization Failures: Implement a custom IAuthorizationMiddlewareResultHandler or authorization failure filter to intercept ForbidResult and log the user, IP, and the resource they tried to access.
Log Critical Changes: Explicitly log _logger.LogInformation for any critical changes (password reset, email change, permission change) including admin ID and target user ID.
Perform failed logins, trigger account lockouts, and attempt to access resources you aren’t authorized for. Check server logs (e.g., Application Insights, console, file logs) for explicit Warning or Information level logs corresponding to these security events. Verify IP and username are included.
Vulnerable Scenario 1: No Logging on Login Failure (Laravel)
Default LoginController fires events, but if no listener is registered, they aren’t logged with extra context.
// app/Http/Controllers/Auth/LoginController.php// Uses AuthenticatesUsers trait. Fires Login and Failed events.// DANGEROUS: No listener configured in EventServiceProvider// to catch these events and log them with IP/context.
Vulnerable Scenario 2: Access Control Failure Not Logged (Gate)
// app/Http/Controllers/PostController.phppublic function update(Request $request, Post $post){ // DANGEROUS: Gate denies access, but the attempt is not logged. // Attacker can enumerate post IDs trying to find one they can edit. if (Gate::denies('update-post', $post)) { abort(403); } // ... update logic ...}
Laravel: Listen for authentication events. In app/Providers/EventServiceProvider.php, register listeners for Illuminate\Auth\Events\Login (success) and Illuminate\Auth\Events\Failed (failure).
In the listeners, use Log::info() or Log::warning() to record the event with request()->ip() and the username.
In Gates or Policies, explicitly log failures (Log::warning(...)) before returning false or calling abort(403).
Perform failed logins, successful logins, and access control violations. Check the Laravel log file (storage/logs/laravel.log) or your configured log output. Verify that [...].WARNING or [...].INFO entries appear for these events, including IP and username/email.
Perform failed logins (wrong user, wrong password) and attempt to access role-protected admin routes. Check application logs (console, file, aggregator) for warn or error level messages containing the username, IP, and details of the failure.
Vulnerable Scenario 2: Pundit Policy Failure Not Logged
# app/policies/post_policy.rbclass PostPolicy < ApplicationPolicy def update? # DANGEROUS: Returns false, but the attempt is not logged. # Attacker can try to update many different post IDs. user.admin? || record.user == user endend# app/controllers/posts_controller.rbdef update @post = Post.find(params[:id]) authorize @post # Throws Pundit::NotAuthorizedError if policy fails # ... update logic ... # The default ApplicationController.rescue_from Pundit::NotAuthorizedError # just redirects or shows 403, it doesn't log the attempt.end
Devise: Subscribe to Devise’s failure event (warden.user.failure) in an initializer and use Rails.logger.warn to log failed attempts with IP and user.
Pundit/CanCanCan: Add a rescue_from block to your ApplicationController for the authorization failure exception (e.g., Pundit::NotAuthorizedError). In the rescue block, log the details (current_user, request.ip, policy details) before rendering the 403 response.
Perform failed logins and authorization failures (e.g., try to edit another user’s post). Check log/production.log (or your log aggregator). Verify that WARN level log entries are created for these security-relevant failures, including username/email (if available) and IP address.