CWE-223: Omission of Security-Relevant Information
Overview
Omission of security-relevant information occurs when applications fail to log critical security events (login failures, access denials, privilege escalations, data modifications), preventing security monitoring, incident response, compliance auditing, and attack detection.
OWASP Classification
A09:2025 - Security Logging & Alerting Failures
Risk
Medium: Missing security logs prevents detection of attacks, delays incident response, violates compliance requirements (PCI-DSS, HIPAA, SOX), hinders forensics investigation, enables attackers to operate undetected, and prevents security trend analysis.
Remediation Steps
Core principle: Ensure exceptions and failure modes do not disclose sensitive data or bypass security checks; fail closed.
Identify Missing Security Logs
When reviewing security scan results:
- Find unlogged security events: Authentication attempts (login/logout), authorization failures, privilege changes, sensitive data access, configuration modifications
- Check authentication flows: Login, logout, password reset, MFA, session creation/destruction
- Review authorization points: Access denied events, role/permission checks, resource access attempts
- Identify sensitive operations: Data modifications (create, update, delete), admin actions, configuration changes
- Trace audit requirements: Compliance needs (PCI-DSS, HIPAA, SOX, GDPR)
Common missing logs:
# No logging of authentication
@app.route('/login')
def login():
user = authenticate(username, password)
if user:
return create_token(user) # No log!
return 'Failed', 401 # No log!
# No logging of authorization failure
@app.route('/admin')
def admin():
if not is_admin(current_user):
return 'Forbidden', 403 # No log of denied access!
return admin_panel()
# No logging of sensitive data modification
@app.route('/delete_user/<id>')
def delete():
db.delete(User, id) # No audit trail!
return 'OK'
Log All Security-Relevant Events (Primary Defense)
import logging
import json
from datetime import datetime
from flask import Flask, request, g
app = Flask(__name__)
logger = logging.getLogger(__name__)
def log_security_event(event_type, action, result, user=None, details=None):
"""Log security event with structured format."""
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'event_type': event_type,
'action': action,
'result': result,
'user': user or 'anonymous',
'ip_address': request.remote_addr,
'user_agent': request.headers.get('User-Agent'),
'request_id': g.get('request_id'),
'details': details or {}
}
logger.info(json.dumps(log_entry))
# VULNERABLE - no logging
@app.route('/login', methods=['POST'])
def login_bad():
username = request.form['username']
password = request.form['password']
user = authenticate(username, password)
if user:
# No log of successful login!
return jsonify({'token': create_token(user)})
# No log of failed login!
return jsonify({'error': 'Invalid credentials'}), 401
# SECURE - comprehensive logging
@app.route('/login', methods=['POST'])
def login_safe():
username = request.form['username']
password = request.form['password']
user = authenticate(username, password)
if user:
# Log successful authentication
log_security_event(
event_type='authentication',
action='login',
result='success',
user=username,
details={'method': 'password', 'session_id': create_session_id()}
)
return jsonify({'token': create_token(user)})
else:
# Log failed authentication attempt
log_security_event(
event_type='authentication',
action='login',
result='failure',
user=username,
details={'reason': 'invalid_credentials'}
)
return jsonify({'error': 'Invalid credentials'}), 401
@app.route('/logout', methods=['POST'])
def logout_safe():
user = get_current_user()
# Log logout
log_security_event(
event_type='authentication',
action='logout',
result='success',
user=user.username,
details={'session_id': get_session_id()}
)
destroy_session()
return jsonify({'status': 'logged out'})
What to log:
- Authentication: Login (success/failure), logout, password change, MFA enable/disable, session creation/expiration
- Authorization: Access granted/denied, permission changes, role assignments
- Data access: View/modify sensitive data, exports, searches
- Administrative: Config changes, user creation/deletion, privilege escalation
- Security events: Account lockouts, suspicious patterns, policy violations
Include Sufficient Context in Logs
# VULNERABLE - insufficient context
logger.info('Login failed') # Who? When? From where?
logger.info('Data modified') # What data? By whom?
logger.info('Access denied') # To what? Why?
# SECURE - comprehensive context
def log_security_event(event_type, action, result, user=None, details=None):
"""Log with full context for investigation."""
log_entry = {
# When
'timestamp': datetime.utcnow().isoformat(),
'timezone': 'UTC',
# What
'event_type': event_type, # authentication, authorization, data_access, etc.
'action': action, # login, delete_user, view_record, etc.
'result': result, # success, failure, denied
# Who
'user': user or 'anonymous',
'user_id': get_user_id() if user else None,
'session_id': g.get('session_id'),
# Where
'ip_address': request.remote_addr,
'user_agent': request.headers.get('User-Agent'),
'endpoint': request.endpoint,
'method': request.method,
# Context
'request_id': g.get('request_id'), # For correlation
'details': details or {} # Event-specific data
}
logger.info(json.dumps(log_entry))
# Usage examples
log_security_event(
event_type='authorization',
action='access_sensitive_data',
result='denied',
user='jsmith',
details={
'resource': 'patient_record',
'record_id': 12345,
'required_role': 'doctor',
'user_role': 'nurse',
'reason': 'insufficient_permissions'
}
)
log_security_event(
event_type='data_modification',
action='delete_user',
result='success',
user='admin',
details={
'target_user_id': 789,
'target_username': 'olduser',
'reason': 'account_termination'
}
)
Use Structured Logging for Analysis
import logging
import json
from logging.handlers import RotatingFileHandler, SysLogHandler
# Configure structured JSON logging
class JSONFormatter(logging.Formatter):
def format(self, record):
log_data = {
'timestamp': self.formatTime(record),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage()
}
# Include extra fields if present
if hasattr(record, 'event_type'):
log_data['event_type'] = record.event_type
if hasattr(record, 'user'):
log_data['user'] = record.user
if hasattr(record, 'ip_address'):
log_data['ip_address'] = record.ip_address
return json.dumps(log_data)
# Setup logging with JSON formatter
logger = logging.getLogger('security_audit')
logger.setLevel(logging.INFO)
# File handler with rotation
file_handler = RotatingFileHandler(
'security_audit.log',
maxBytes=10*1024*1024, # 10MB
backupCount=10
)
file_handler.setFormatter(JSONFormatter())
logger.addHandler(file_handler)
# Optional: Send to SIEM (syslog, Splunk, ELK)
syslog_handler = SysLogHandler(address=('siem.company.com', 514))
syslog_handler.setFormatter(JSONFormatter())
logger.addHandler(syslog_handler)
# Log with structured data
def log_security_event(event_type, action, result, user, details):
extra = {
'event_type': event_type,
'action': action,
'result': result,
'user': user,
'ip_address': request.remote_addr,
'details': json.dumps(details)
}
logger.info(f'{event_type}: {action} - {result}', extra=extra)
Protect Log Integrity and Implement Monitoring
import os
import hashlib
from datetime import datetime
# Append-only log file permissions
def setup_secure_logging():
log_file = 'security_audit.log'
# Create with restricted permissions (owner read/write only)
if not os.path.exists(log_file):
with open(log_file, 'w') as f:
pass
os.chmod(log_file, 0o600) # -rw-------
# For maximum security, use append-only attribute (Linux)
# os.system(f'chattr +a {log_file}')
# Log integrity verification (hash chain)
class IntegrityLogger:
def __init__(self, log_file):
self.log_file = log_file
self.previous_hash = self._get_last_hash()
def log(self, message):
timestamp = datetime.utcnow().isoformat()
entry = f"{timestamp}|{message}|{self.previous_hash}"
current_hash = hashlib.sha256(entry.encode()).hexdigest()
with open(self.log_file, 'a') as f:
f.write(f"{entry}|{current_hash}\n")
self.previous_hash = current_hash
def _get_last_hash(self):
try:
with open(self.log_file, 'r') as f:
lines = f.readlines()
if lines:
return lines[-1].split('|')[-1].strip()
except FileNotFoundError:
pass
return '0' * 64 # Genesis hash
# Monitoring: Alert on suspicious patterns
def monitor_security_logs():
"""Example monitoring function."""
# Alert on multiple failed logins
failed_logins = count_recent_events(
event_type='authentication',
result='failure',
minutes=5
)
if failed_logins > 5:
alert_security_team(f'{failed_logins} failed logins in 5 minutes')
# Alert on privilege escalations
privilege_changes = count_recent_events(
event_type='authorization',
action='grant_admin',
hours=1
)
if privilege_changes > 0:
alert_security_team(f'Admin privilege granted: review logs')
# Alert on bulk data access
data_access = count_recent_events(
event_type='data_access',
minutes=1
)
if data_access > 100:
alert_security_team(f'Possible data exfiltration: {data_access} records/min')
Test Security Logging Implementation
import pytest
import json
from unittest.mock import patch
def test_login_success_logged(caplog):
"""Verify successful login is logged."""
with caplog.at_level(logging.INFO):
response = client.post('/login', data={
'username': 'testuser',
'password': 'password123'
})
# Check log contains security event
assert any('authentication' in record.message for record in caplog.records)
# Parse JSON log
for record in caplog.records:
if 'authentication' in record.message:
log_data = json.loads(record.message)
assert log_data['event_type'] == 'authentication'
assert log_data['action'] == 'login'
assert log_data['result'] == 'success'
assert log_data['user'] == 'testuser'
assert 'ip_address' in log_data
assert 'timestamp' in log_data
print("✓ Login success logged\n")
def test_login_failure_logged(caplog):
"""Verify failed login is logged."""
with caplog.at_level(logging.INFO):
response = client.post('/login', data={
'username': 'testuser',
'password': 'wrongpassword'
})
# Verify failure is logged
log_found = False
for record in caplog.records:
if 'authentication' in record.message:
log_data = json.loads(record.message)
if log_data['result'] == 'failure':
log_found = True
assert log_data['user'] == 'testuser'
assert 'reason' in log_data['details']
assert log_found, "Failed login should be logged"
print("✓ Login failure logged\n")
def test_authorization_denial_logged(caplog):
"""Verify authorization failures are logged."""
with caplog.at_level(logging.INFO):
# Try to access admin endpoint without permission
response = client.get('/admin/config')
# Check authorization denial logged
log_found = False
for record in caplog.records:
if 'authorization' in record.message:
log_data = json.loads(record.message)
if log_data['result'] == 'denied':
log_found = True
assert 'resource' in log_data['details']
assert log_found, "Authorization denial should be logged"
print("✓ Authorization denial logged\n")
def test_sensitive_data_modification_logged(caplog):
"""Verify data modifications are logged."""
with caplog.at_level(logging.INFO):
response = client.delete('/api/user/123')
# Verify deletion logged
log_found = False
for record in caplog.records:
if 'data_modification' in record.message:
log_data = json.loads(record.message)
if log_data['action'] == 'delete_user':
log_found = True
assert 'target_user_id' in log_data['details']
assert log_found, "User deletion should be logged"
print("✓ Data modification logged\n")
def test_log_contains_required_fields():
"""Verify logs contain all required context."""
with caplog.at_level(logging.INFO):
client.post('/login', data={'username': 'test', 'password': 'test'})
required_fields = ['timestamp', 'event_type', 'action', 'result',
'user', 'ip_address']
for record in caplog.records:
if 'authentication' in record.message:
log_data = json.loads(record.message)
for field in required_fields:
assert field in log_data, f"Log missing required field: {field}"
print("✓ Logs contain required context\n")
if __name__ == '__main__':
test_login_success_logged()
test_login_failure_logged()
test_authorization_denial_logged()
test_sensitive_data_modification_logged()
test_log_contains_required_fields()
print("All security logging tests passed!\n")
Common Vulnerable Patterns
- Only logging errors, not security events
- Missing login attempt logging
- No audit trail for data changes
- Insufficient context (no user, timestamp)
- Not logging authorization failures
No Logging of Authentication Events (Python Flask)
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
user = authenticate(username, password)
if user:
# No logging of successful login!
return jsonify({'token': create_token(user)})
# No logging of failed login!
return jsonify({'error': 'Invalid credentials'}), 401
@app.route('/api/delete_user/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
# No logging of sensitive operation!
db.delete(User, user_id)
return jsonify({'status': 'deleted'})
@app.route('/admin/config', methods=['POST'])
def update_config():
# No logging of admin action!
config.update(request.json)
return jsonify({'status': 'updated'})
Why this is vulnerable: Failing to log authentication events, authorization failures, and sensitive operations prevents security teams from detecting brute-force attacks, privilege escalations, or unauthorized data access, delays incident response, violates compliance requirements (PCI-DSS, HIPAA), and enables attackers to operate undetected without leaving audit trails.
Secure Patterns
Comprehensive Authentication and Event Logging (Python Flask)
import logging
from flask import Flask, request, jsonify, g
import json
from datetime import datetime
app = Flask(__name__)
# Configure structured logging
logging.basicConfig(
level=logging.INFO,
format='%(message)s'
)
logger = logging.getLogger(__name__)
def log_security_event(event_type, action, result, user=None, details=None):
"""Log security event with full context."""
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'event_type': event_type,
'action': action,
'result': result,
'user': user or 'anonymous',
'ip_address': request.remote_addr,
'user_agent': request.headers.get('User-Agent'),
'request_id': g.get('request_id'),
'details': details or {}
}
logger.info(json.dumps(log_entry))
@app.route('/login', methods=['POST'])
def login_secure():
username = request.form['username']
password = request.form['password']
user = authenticate(username, password)
if user:
# Log successful authentication
log_security_event(
event_type='authentication',
action='login',
result='success',
user=username,
details={'method': 'password'}
)
return jsonify({'token': create_token(user)})
# Log failed authentication
log_security_event(
event_type='authentication',
action='login',
result='failure',
user=username,
details={'reason': 'invalid_credentials'}
)
return jsonify({'error': 'Invalid credentials'}), 401
@app.route('/api/delete_user/<int:user_id>', methods=['DELETE'])
def delete_user_secure(user_id):
current_user = get_current_user()
# Log sensitive data operation
log_security_event(
event_type='data_modification',
action='delete_user',
result='success',
user=current_user.username,
details={'target_user_id': user_id}
)
db.delete(User, user_id)
return jsonify({'status': 'deleted'})
@app.route('/api/sensitive_data/<int:record_id>')
def access_sensitive_data(record_id):
current_user = get_current_user()
# Check authorization
if not has_permission(current_user, record_id):
# Log authorization failure
log_security_event(
event_type='authorization',
action='access_sensitive_data',
result='denied',
user=current_user.username,
details={'record_id': record_id, 'reason': 'insufficient_permissions'}
)
return jsonify({'error': 'Access denied'}), 403
# Log successful access
log_security_event(
event_type='data_access',
action='view_sensitive_data',
result='success',
user=current_user.username,
details={'record_id': record_id}
)
return jsonify(get_sensitive_data(record_id))
Why this works:
- Logs all authentication events (both success and failure) with timestamp, user, IP, and action context
- Logs authorization failures before denying access, enabling detection of unauthorized access attempts
- Logs sensitive data modifications and access with full audit trail (who, what, when)
- Uses structured JSON logging for automated parsing, analysis, and SIEM integration
- Includes request IDs for correlation and troubleshooting across distributed systems
Structured Security Event Logging (Java Spring)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.security.core.Authentication;
@Service
public class AuditLogger {
private static final Logger auditLog = LoggerFactory.getLogger("AUDIT");
public void logSecurityEvent(String eventType, String action,
String result, String user,
Map<String, Object> details) {
// Use MDC for structured logging
MDC.put("eventType", eventType);
MDC.put("action", action);
MDC.put("result", result);
MDC.put("user", user);
MDC.put("timestamp", Instant.now().toString());
auditLog.info("Security event: {} - {} - {}, details: {}",
eventType, action, result, details);
MDC.clear();
}
}
@RestController
public class UserController {
@Autowired
private AuditLogger auditLogger;
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest req) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
req.getUsername(),
req.getPassword()
)
);
if (auth.isAuthenticated()) {
auditLogger.logSecurityEvent(
"authentication",
"login",
"success",
req.getUsername(),
Map.of("method", "password")
);
return ResponseEntity.ok(new LoginResponse(generateToken(auth)));
} else {
auditLogger.logSecurityEvent(
"authentication",
"login",
"failure",
req.getUsername(),
Map.of("reason", "invalid_credentials")
);
return ResponseEntity.status(401)
.body(new LoginResponse("Invalid credentials"));
}
}
@DeleteMapping("/api/user/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id,
Authentication auth) {
auditLogger.logSecurityEvent(
"data_modification",
"delete_user",
"success",
auth.getName(),
Map.of("target_user_id", id)
);
userService.delete(id);
return ResponseEntity.ok().build();
}
@GetMapping("/api/sensitive/{id}")
public ResponseEntity<?> getSensitiveData(@PathVariable Long id,
Authentication auth) {
if (!hasPermission(auth, id)) {
auditLogger.logSecurityEvent(
"authorization",
"access_sensitive_data",
"denied",
auth.getName(),
Map.of("record_id", id, "reason", "insufficient_permissions")
);
return ResponseEntity.status(403).build();
}
auditLogger.logSecurityEvent(
"data_access",
"view_sensitive_data",
"success",
auth.getName(),
Map.of("record_id", id)
);
return ResponseEntity.ok(dataService.get(id));
}
}
Why this works:
- Uses SLF4J MDC (Mapped Diagnostic Context) for thread-safe structured logging in multi-threaded environments
- Logs authentication, authorization, and data modification events with full context
- Separates audit logs from application logs for compliance and forensics
- Integrates with Spring Security to automatically capture authenticated user context
- Provides consistent logging format across all security-sensitive operations
Comprehensive Event Logging with Winston (Node.js)
const winston = require('winston');
const { v4: uuidv4 } = require('uuid');
// Configure audit logger
const auditLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'auth-service' },
transports: [
new winston.transports.File({ filename: 'audit.log' }),
new winston.transports.File({ filename: 'security-events.log' })
]
});
function logSecurityEvent(eventType, action, result, user, details = {}) {
auditLogger.info({
eventType,
action,
result,
user: user || 'anonymous',
requestId: details.requestId || uuidv4(),
ip: details.ip,
userAgent: details.userAgent,
...details
});
}
app.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
const user = await authenticate(username, password);
logSecurityEvent(
'authentication',
'login',
'success',
username,
{
ip: req.ip,
userAgent: req.get('user-agent'),
method: 'password'
}
);
res.json({ token: generateToken(user) });
} catch (error) {
logSecurityEvent(
'authentication',
'login',
'failure',
username,
{
ip: req.ip,
userAgent: req.get('user-agent'),
reason: 'invalid_credentials'
}
);
res.status(401).json({ error: 'Invalid credentials' });
}
});
app.delete('/api/user/:id', authenticate, async (req, res) => {
const { id } = req.params;
logSecurityEvent(
'data_modification',
'delete_user',
'success',
req.user.username,
{
targetUserId: id,
ip: req.ip
}
);
await userService.delete(id);
res.json({ status: 'deleted' });
});
app.get('/api/sensitive/:id', authenticate, async (req, res) => {
const { id } = req.params;
if (!hasPermission(req.user, id)) {
logSecurityEvent(
'authorization',
'access_sensitive_data',
'denied',
req.user.username,
{
recordId: id,
reason: 'insufficient_permissions',
ip: req.ip
}
);
return res.status(403).json({ error: 'Access denied' });
}
logSecurityEvent(
'data_access',
'view_sensitive_data',
'success',
req.user.username,
{
recordId: id,
ip: req.ip
}
);
const data = await dataService.get(id);
res.json(data);
});
Why this works:
- Uses Winston for production-grade structured logging with JSON format for automated analysis
- Logs authentication success/failure, authorization denials, and sensitive data operations
- Captures comprehensive context (IP address, user agent, request ID) for forensic investigation
- Writes to dedicated audit log files separate from application logs for compliance retention
- Enables real-time monitoring and alerting through structured log data
Security Checklist
- All authentication events logged (success/failure)
- Authorization failures logged
- Sensitive data access/modifications logged
- Administrative actions logged
- Logs include: timestamp, user, IP, action, result
- Structured logging (JSON) for analysis
- Logs protected (restricted permissions, append-only)
- Monitoring/alerting on suspicious patterns