OWASP Top 10:2025 Course — Module 9 of 10. Course home · Previous: Module 8 — Software/Data Integrity Failures · Next: Module 10 — Mishandling of Exceptional Conditions

Learn first: what is a Security Logging Failure

A09:2025 is the category that makes every other category worse: if an attack leaves no trace, you can’t detect it, investigate it, or even know it happened. This module runs the exact same brute-force attack twice — once against an app with zero logging, once against the same app with one structured JSON log line per attempt plus a watcher that alerts in real time. The attack itself doesn’t change at all; only your ability to see it does.

Stage 1 (no logging):            Stage 2 (structured logging):
attack.py  ──▶  app               attack.py  ──▶  app  ──▶  login_attempts.log
                 │                                              │
            server console                                      ▼
            stays EMPTY                                   watcher.py (tails log)
            account taken over,                                │
            zero trace                                    failures/window > threshold?
                                                                 │
                                                           *** ALERT ***

The attack succeeds in both cases in this lab — the difference is entirely about whether anyone finds out.

Scenario briefing

You’re the on-call engineer at a company whose login app was just quietly compromised. You’ve been asked to figure out, after the fact, whether the incident response team would have had anything to go on — and then to fix that gap.

Lab setup

  1. Download: Download the lab (zip)
  2. Prerequisites: Docker Engine 20+, Docker Compose v2, Python 3.9+ on your host (the attack and watcher scripts run on the host, against the containerized apps).
  3. Start it:
    cd owasp-a09-detection-lab
    docker compose up -d --build
  4. Check it worked: curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:5000/ should print 200.

Missions

Mission 1 — attack the silent app. Brute-force the admin account and check what the server noticed.

Show solution

Solution — Mission 1

python3 app/attack.py 0 http://127.0.0.1:5000/login
[attack] tried 'lab-admin-pass' -> SUCCESS (200)
[attack] credentials found: admin:lab-admin-pass

Now check the server: docker compose logs no-logging — only the Flask startup banner. Nothing about ten login attempts, nothing about the one that succeeded. The account is compromised and there is no record of it.

Mission 2 — watch the same attack get caught live. Start the watcher and the logging-enabled app, then re-run the attack.

Show solution

Solution — Mission 2

python3 -u app/watcher.py --threshold 5 --window 30 &
python3 app/attack.py 0.3 http://127.0.0.1:5001/login
[watcher] failure #5 in window for 172.18.0.1 -> 'admin'

*** ALERT: possible brute-force attack ***
    source_ip   : 172.18.0.1
    failures    : 5 within 30s

The exact same attack now raises an alert mid-attack, before the attacker’s final (correct) guess lands.

Mission 3 — read the structured log. Confirm what got recorded for the successful login.

Show solution

Solution — Mission 3

tail -1 app/login_attempts.log
{"timestamp": "...", "event": "login_attempt", "username": "admin", "source_ip": "...", "outcome": "success", "failure_streak": 0}

The attack script itself also prints the recovered credential pair directly: credentials found: admin:lab-admin-pass — submit FLAG{a09-detected-brute-force-admin-lab-admin-pass} once you’ve confirmed that line in the attack output against the logging-enabled app.

Mission 4 — add logging to the silent app. Port the structured JSON logging from app_with_logging.py into app_no_logging.py.

Show solution

Solution — Mission 4

log_entry = {
    "timestamp": datetime.utcnow().isoformat() + "Z",
    "event": "login_attempt",
    "username": username,
    "source_ip": request.remote_addr,
    "outcome": "success" if ok else "failure",
    "failure_streak": streak,
}
with open(LOG_FILE, "a") as f:
    f.write(json.dumps(log_entry) + "\n")

Restart the service, run python3 lab.py verify — the attack still succeeds (this module isn’t about stopping the attack, that’s Module 7), but now it leaves a trail a watcher can act on.

Hints

Fix it + re-test

Add one structured log line per security-relevant event (not a free-text message — JSON with a consistent schema is what a watcher or SIEM can actually parse), covering at minimum: timestamp, actor (username/IP), event type, and outcome. Then build or buy the correlation layer (watcher.py here, a real SIEM rule in production) that turns "five failures in 30 seconds from one source" into an alert, not just a line in a file nobody reads.

Detect it

This module’s lab literally is the detection answer: a sliding-window velocity check, per (source_ip, username) pair, is a minimal but real brute-force detector. Push the same structured events to a central log pipeline so the detection survives even if the attacker gets far enough to delete local files.

Quick self-check

  1. Why is "the attack succeeded either way" the whole point of this module, rather than a failure of the lab?
  2. Why log every attempt (including failures) instead of only logging successful logins?
  3. What’s the practical difference between a log line and an alert?
Show answers

Answers

  1. This module isolates one variable — visibility — from the attack’s success; it proves that detection and prevention are separate problems, and that you can (and should) build detection even for attacks you haven’t fully prevented yet.
  2. A string of failures followed by a success is the actual signature of a brute-force attack; logging only successes would make a compromised account look identical to a normal login, with no way to tell the difference after the fact.
  3. A log line is a record that something can be reviewed later, usually after the fact; an alert is an active notification the moment a pattern crosses a threshold, in time to respond while the attack is still happening.