Skip to content
back to the map
practice
stage: operate
2 min read

A09 Security Logging and Alerting Failures

You cannot respond to what you cannot see. Logs without alerts are an archive; alerts without good logs are noise. Both must exist, and neither may be injectable or full of secrets.

OpenTelemetryLoki / Elastic / SplunkSigma rulesFalcoWazuhfail2ban

What it is

This category covers missing or poor security logging, monitoring and alerting: logins and access-control failures that are not recorded, logs that live only on the machine an attacker controls, events with no alert behind them, and logs that leak sensitive data or can be forged by user input.

It is A09 in :2025, with the name stressing alerting: a log nobody is paged about is not a control.

How the attack works

  • A credential-stuffing run of thousands of failed logins is not logged, or is logged and never reviewed.
  • An attacker with a foothold deletes local logs; with no central copy, the trail is gone.
  • : a username containing a newline writes a fake "login succeeded" line.
  • Logs record passwords, tokens or card numbers, turning the log store into the richest target.

Vulnerable code

Free-text logging with raw user input, a secret in the message, and no record of the failure that matters.

login.pypython
def login(email, password):
    print(f"login attempt {email} / {password}")          # secret in the log, newline injection possible
    user = authenticate(email, password)
    if not user:
        return None                                        # failure not recorded
    return user
login.jsjavascript
console.log("login failed for " + req.body.email + " with token " + req.headers.authorization);

The fix

Log security events as structured data with stable field names: who, what, where from, outcome, and a . Never log secrets; encode user input so it cannot forge lines. Ship logs off the host immediately, and attach alert rules to the events that matter (spikes in failures, privilege changes, access denied on sensitive objects).

login.pypython
import json, logging
log = logging.getLogger("security")

def event(action, outcome, **fields):
    log.info(json.dumps({"action": action, "outcome": outcome, **fields}))   # json.dumps escapes newlines

def login(email, password, ip, request_id):
    user = authenticate(email, password)
    if not user:
        event("login", "failure", email=email, ip=ip, request_id=request_id)   # no password, no token
        return None
    event("login", "success", user_id=user.id, ip=ip, request_id=request_id)
    return user
login.js (pino)javascript
req.log.warn({ action: "login", outcome: "failure", email: req.body.email, ip: req.ip }, "auth");
// pino redaction keeps secrets out even if someone logs the whole object
const logger = pino({ redact: ["req.headers.authorization", "*.password", "*.token"] });
alert.yml (Prometheus-style rule)yaml
- alert: LoginFailureSpike
  expr: sum(rate(auth_events_total{action="login",outcome="failure"}[5m])) > 5
  for: 5m
  labels: { severity: page }
  annotations:
    summary: "Login failures above baseline: possible credential stuffing"

Prove the fix

Test that the event is emitted with the right fields, that no secret appears, and that input cannot forge a line. Then test the alert path itself: replay a burst and confirm someone is notified. An untested alert is a hope.

test_logging.py (pytest)python
def test_failed_login_is_logged_without_secrets(caplog):
    with caplog.at_level("INFO", logger="security"):
        login("bob@x.io\nlogin success", "hunter2", "203.0.113.9", "req-1")
    text = caplog.text
    assert "hunter2" not in text                      # no secret
    assert text.count("\n") == 1                       # the injected newline did not create a line
    assert '"outcome": "failure"' in text

def test_alert_fires_on_burst(alert_harness):
    for _ in range(200):
        login("bob@x.io", "bad", "203.0.113.9", "r")
    assert alert_harness.fired("LoginFailureSpike")

Checklist

  • Log: authentication, access-control failures, input-validation failures, privileged actions, and changes to security configuration.
  • Use a consistent structured format; include time, actor, action, target, outcome, source and a correlation id.
  • Send logs to a central, append-only store quickly; restrict who can alter or delete them.
  • Do not log secrets, tokens or full card/ID numbers; encode untrusted input.
  • Define alerts and an owner for each; write the response in a runbook; test the alert path.
  • Set retention that matches legal and investigation needs.
OWASP Top 10The best-known reference list of web application security risks (A01 broken access control, A02 cryptographic failures, A03 injection...), revised every few years by the OWASP community. It is an educational map of risk categories, not a tool checklist or a certification.see in glossary →Log injectionUser input containing newlines or control characters forges log entries. Encode input or use structured (JSON) logging.see in glossary →Correlation idrequest idA unique id attached to a request and to every log line it creates, shown to the user in error messages so support can find the details without leaking them.see in glossary →

connections on the map

updated on 14/10/2026 · sources to go deeper · report an error

in other domains

Study before

Unlocks

test what you learned

Loading questions…