OWASP Top 10:2025 Course — Module 2 of 10. Course home · Previous: Module 1 — Broken Access Control · Next: Module 3 — Software Supply Chain Failures
Learn first: what is Security Misconfiguration
Security Misconfiguration (A02:2025) is what happens when every individual setting looks small — directory listing on, a default admin password, debug mode left on in production, a missing security header — but together they hand an attacker a map of your infrastructure and a front door. None of these require a clever exploit; they require only that someone forgot to turn something off before shipping.
Browser ──▶ Nginx ──▶ Flask app
│ │
│ ├─ /admin no auth check
│ ├─ /admin/config dumps secret_key, DB, AWS bucket
│ └─ /error-demo stack trace on any error
│
├─ /files/ directory listing ON (autoindex)
└─ no security headers at all (CSP, HSTS, X-Frame-Options...)
Four misconfigurations, one request each, zero exploit development.
Scenario briefing
You’re doing a quick external review of AcmeCorp’s internal portal before it goes live on a public subdomain. You have no credentials and no special access — just the URL.
Lab setup
- Download: Download the lab (zip)
- Prerequisites: Docker Engine 20+, Docker Compose v2.
- Start it:
cd owasp-a02-security-misconfiguration-lab docker compose up -d --build - Check it worked:
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/should print200.
Missions
Mission 1 — open bucket. List the files directory and read the quarterly revenue report.
Show solution
Solution — Mission 1
curl -s http://127.0.0.1:8080/files/
curl -s http://127.0.0.1:8080/files/q3-revenue.csv | tail -1
nginx.conf has autoindex on; for /files/ — Nginx lists the directory, no application involved at all. The last line of the CSV holds FLAG{a02-open-bucket-directory-listing}.
Mission 2 — unauthenticated config leak. Find the endpoint that leaks the app’s internal configuration without logging in.
Show solution
Solution — Mission 2
curl -s http://127.0.0.1:8080/admin/config
Returns the Flask secret_key, the admin credentials, the database URL, and an internal S3 bucket name — plus "ops_flag": "FLAG{a02-config-leak-no-auth}" — all with zero authentication, because the route was never protected and nothing in front of it blocks it either.
Mission 3 — default credentials. Log in with the vendor’s default admin password, then call /profile.
Show solution
Solution — Mission 3
curl -s -c cj.txt -d "username=admin&password=admin" http://127.0.0.1:8080/login
curl -s -b cj.txt http://127.0.0.1:8080/profile
admin / admin was never changed from the seeded default. /profile returns FLAG{a02-default-creds-admin-admin} for the admin role.
Mission 4 — verbose errors. Trigger an error and see what the response leaks.
Show solution
Solution — Mission 4
curl -s "http://127.0.0.1:8080/error-demo?input=invalid"
APP_DEBUG=true means Flask’s debug error page renders — a full Python traceback with absolute file paths, line numbers, and the exact eval() call that failed. That’s a roadmap for finding the next bug.
Mission 5 — harden it and verify. Apply the hardened Nginx config and app settings (docker-compose.hardened.yml has the reference), then check.
Show solution
Solution — Mission 5
docker compose down
docker compose -f docker-compose.hardened.yml up -d --build
bash scripts/check-misconfigs.sh
python3 lab.py verify
The hardened stack sets server_tokens off, adds six security headers, returns 404 for /admin and /admin/config at the Nginx layer, disables debug mode (generic error page), and changes the admin password away from the default. All checks go green.
Hints
/files/,/admin,/admin/config,/error-demo, and/loginare the five things to poke at — no fuzzing required, they’re all in the nav bar of the vulnerable app’s homepage.- The automated checker script (
scripts/check-misconfigs.sh) runs the same five checks you just did by hand — use it to confirm your own findings before and after hardening.
Fix it + re-test
The fixes are entirely configuration, not code: security headers at the reverse-proxy layer, return 404 for internal routes, server_tokens off, APP_DEBUG=false, and a real password instead of the vendor default. Re-run bash scripts/check-misconfigs.sh — every line should flip to green.
Detect it
Bake check-misconfigs.sh-style checks into CI so a regression (someone flips autoindex on again, or debug mode creeps back in staging configs copied to prod) fails the build instead of shipping. In production, alert on any 200 response from /admin* paths from an IP that never authenticated.
Quick self-check
- Why is "it’s just a config file" not a good reason to skip a security review of infrastructure changes?
- What’s the difference between this module’s
/adminleak and Module 1’s/adminleak? - Why does disabling debug mode matter even if the app has no other known bugs?
Show answers
Answers
- Config changes ship to production just as fast as code, and a single wrong line (
autoindex on, a default password left in an env var) is directly exploitable with no code execution needed. - Module 1’s
/adminwas an application-level missing authorization check (broken access control); this module’s exposure stack is layered — missing headers, directory listing, and debug mode are infrastructure/configuration issues, not a single code bug. - A debug error page leaks file paths, framework versions, and sometimes variable values — reconnaissance that shortens the time to find the next real bug, even on an otherwise solid app.