Learning Goals
3 minBy the end of this lesson you can:
- Recognise the common misconfigurations: defaults, debug, exposure, verbose errors.
- Explain why
debug=Truein production is a critical vulnerability. - Apply a hardening checklist and the "secure by default" principle.
- Audit an app/deployment for misconfiguration with a checklist tool.
Warm-Up · The Door Left Unlocked
5 minMany breaches need no exploit at all — just a door someone forgot to lock: a database with no password exposed to the internet, an admin panel at /admin with admin/admin, a debug page leaking the source code and secrets, a cloud bucket set to "public." Boring. Devastating. Extremely common.
Security Misconfiguration (A05) is the gap between "works" and "safe." Software often ships insecure by default (debug on, sample accounts, permissive settings) for convenience, and it's your job to harden it before production. The fix isn't clever code — it's discipline: change defaults, turn off debug, close what you don't use, hide internal details, and follow a checklist every deploy.
New Concept · The Misconfigurations & Fixes
14 min⚠️ Debug mode in production — a critical one
# DANGEROUS in production: app.run(debug=True) # Flask: exposes the Werkzeug debugger
debug=True is a vulnerability, not just a settingFlask's debug mode shows an interactive traceback on errors — which includes source code, variable values, and config (often secrets). Worse, the Werkzeug debugger has an interactive console: with the PIN (sometimes brute-forceable or leaked in the traceback) an attacker can run arbitrary Python on your server. debug=True in production is effectively remote code execution waiting to happen. Always debug=False in production; control it via an environment variable.
The misconfiguration catalogue
MISCONFIGURATION FIX debug mode on in prod debug=False; config via env default credentials (admin/admin) force a strong password on setup exposed services (DB on 0.0.0.0) bind to localhost / firewall (L8-03,20) verbose error pages (stack traces) generic error pages; log details server-side directory listing enabled disable it; don't serve source/configs unused features/ports/accounts open remove them (minimise attack surface, L8-02) permissive CORS (Access-Control-* *) allow-list specific origins missing security headers HSTS, CSP, X-Frame-Options, X-Content-Type public cloud buckets / open .git lock down storage; block /.git, /.env
Verbose errors leak the map
# BAD: a stack trace shown to users reveals file paths, library versions, # SQL queries, and sometimes secrets — a recon goldmine for attackers. # GOOD: generic message to the user, full detail to the server log. @app.errorhandler(500) def server_error(e): log.exception("unhandled error") # detail → server log (L8-45) return "Something went wrong.", 500 # generic → the user
Secure by default + a deploy checklist
import os # config from the environment; SECURE defaults baked in: DEBUG = os.getenv("FLASK_DEBUG", "false").lower() == "true" # default OFF SECRET_KEY = os.environ["SECRET_KEY"] # required, from env (no default!) app.config.update( DEBUG=DEBUG, SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SECURE=True, SESSION_COOKIE_SAMESITE="Lax", )
"Secure by default" means the safe setting is the one you get if you do nothing: debug off, cookies hardened, secrets required (not defaulted). Pair it with a written hardening checklist run before every deploy.
Misconfiguration spans your app, framework, web server, database, OS, cloud, and dependencies. The web server might list directories; the DB might allow remote root; the cloud bucket might be public. Hardening is a checklist across all layers — and the self-scan from Lesson 20 (own ports) plus passive recon (Lesson 6, own footprint) are how you find your own exposure.
Worked Example · A Hardening Checklist Tool
12 minGoal: a tool that checks a Flask app/config for common misconfigurations and reports a hardening score — the kind of pre-deploy gate that catches the boring-but-fatal mistakes.
import os, re from pathlib import Path def audit_flask_app(source_path: str, env: dict) -> list[str]: findings = [] src = Path(source_path).read_text(encoding="utf-8") # 1. debug mode if re.search(r"debug\s*=\s*True", src): findings.append("🔴 CRITICAL: debug=True found — RCE risk in prod. " "Set debug=False / control via env.") # 2. hard-coded secret key if re.search(r"secret_key\s*=\s*['\"][^'\"]+['\"]", src, re.I): findings.append("🔴 hard-coded SECRET_KEY — load from env.") # 3. binding to all interfaces if re.search(r"host\s*=\s*['\"]0\.0\.0\.0['\"]", src): findings.append("🟡 binds 0.0.0.0 — ensure a firewall; localhost in dev.") # 4. cookie hardening missing if "SESSION_COOKIE_HTTPONLY" not in src: findings.append("🟡 session cookies not hardened — set HttpOnly/Secure/SameSite.") # 5. env-based runtime checks if env.get("FLASK_DEBUG", "false").lower() == "true": findings.append("🔴 FLASK_DEBUG=true in this environment.") if not env.get("SECRET_KEY"): findings.append("🟡 SECRET_KEY not set in the environment.") return findings issues = audit_flask_app("app.py", dict(os.environ)) print(f"Hardening audit — {len(issues)} issue(s):\n") for i in issues: print(" ", i) print("\n✅ secure-by-default" if not issues else "\nfix the above before deploying.")
Hardening audit — 3 issue(s): 🔴 CRITICAL: debug=True found — RCE risk in prod. Set debug=False / control via env. 🔴 hard-coded SECRET_KEY — load from env. 🟡 session cookies not hardened — set HttpOnly/Secure/SameSite. fix the above before deploying.
Read the code
The audit encodes the misconfiguration catalogue as concrete checks, ranked by severity — and the standout is debug=True flagged CRITICAL, because in production it's effectively remote code execution, not a minor setting. Run this as a pre-deploy gate (or in CI, Level 7) and the boring-but-devastating mistakes get caught before they ship. Combine it with the Lesson 20 port self-scan (is the DB exposed?) and Lesson 6 footprint check (is /.git or a debug page reachable?) for full-stack coverage.
Try It Yourself
13 minRun a local Flask app with debug=True, trigger an error, and observe what the debug page reveals (source, locals, the console prompt). Then set debug=False and confirm a generic error instead. Never do this on a public server.
Take an app config and apply the secure-by-default pattern: debug off by default, secret from env (required), cookies hardened, generic 500 handler. Run the audit tool to confirm zero findings.
For an app you deployed/run locally, combine three checks: the config audit (this lesson), the port self-scan (Lesson 20 — is anything exposed that shouldn't be?), and a request for /.git/config, /.env, and a forced error (are internal files/traces reachable?). Report every gap.
Hint
import requests # check for accidentally-exposed sensitive paths (on YOUR app): for path in ["/.git/config", "/.env", "/admin", "/debug"]: r = requests.get(f"http://127.0.0.1:5000{path}") if r.status_code == 200: print(f"⚠️ {path} is reachable ({len(r.text)} bytes) — should it be?")
Mini-Challenge · A Security-Headers Auditor
8 minMissing security headers are a classic A05. Build a tool that requests a URL (your own app) and grades the presence of key headers: HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy. Output a grade and the missing headers with the one-line fix for each.
Show a sample solution
import requests WANT = { "strict-transport-security": "force HTTPS (HSTS)", "content-security-policy": "restrict scripts/resources (anti-XSS)", "x-frame-options": "prevent clickjacking (DENY/SAMEORIGIN)", "x-content-type-options": "nosniff — stop MIME sniffing", "referrer-policy": "limit referrer leakage", } def audit_headers(url: str) -> None: r = requests.get(url, timeout=10) present = {k.lower() for k in r.headers} have = sum(h in present for h in WANT) print(f"{have}/{len(WANT)} security headers present for {url}\n") for h, why in WANT.items(): mark = "✓" if h in present else "✗" line = f" [{mark}] {h}" if h not in present: line += f" → add it: {why}" print(line) audit_headers("http://127.0.0.1:5000") # your own app
Non-negotiables: checks the key security headers on your own app, grades them, and gives a fix for each missing one.
Recap
3 minSecurity Misconfiguration (A05) is the gap between "works" and "safe" — and it's the most preventable category. The usual suspects: debug mode in production (effectively RCE via Flask's console — always off), default credentials, exposed services (bind localhost/firewall), verbose error pages (generic to users, detail to logs), and missing security headers. The fix is discipline, not cleverness: secure by default (safe settings unless you opt out), minimise attack surface, and run a hardening checklist across the whole stack every deploy. Combine config audits, port self-scans, and exposed-path checks for full coverage.
Vocabulary Card
- security misconfiguration
- Insecure settings/defaults/exposure rather than a code flaw.
- debug mode (prod)
- Leaks internals and (in Flask) enables an RCE console — never in production.
- secure by default
- The safe configuration is what you get without extra effort.
- hardening checklist
- A pre-deploy list of settings to lock down across the stack.
Homework
4 minBuild the config-audit and security-headers tools and run them on an app you deployed/run locally. Apply the secure-by-default config pattern and re-audit to zero findings. Write a one-page hardening checklist for your stack (app, server, DB, deps) that you'd run before every deploy — and explain why debug=True in production is treated as critical, not cosmetic.
Sample · pre-deploy hardening checklist
PRE-DEPLOY HARDENING CHECKLIST App / framework [ ] debug=False (env-controlled); no Werkzeug console in prod [ ] SECRET_KEY from env (required, not defaulted) [ ] generic error pages; full detail only in server logs [ ] session cookies HttpOnly + Secure + SameSite [ ] security headers set (HSTS, CSP, X-Frame-Options, nosniff) Server / network [ ] only required ports open (port self-scan, L8-20) [ ] DB/cache bound to localhost or firewalled (not 0.0.0.0) [ ] directory listing OFF; /.git, /.env, backups not served Accounts / data [ ] no default credentials; strong admin password enforced [ ] least-privilege DB user (L8-31) Dependencies [ ] pinned + scanned (pip-audit) — no known CVEs (L8-35) Why debug=True is CRITICAL: Flask's debugger exposes source, locals, and config (secrets) on any error, and its interactive console can run arbitrary Python on the server. In production that's remote code execution — not a cosmetic dev convenience. So it's a 🔴, not a 🟡.
Non-negotiables: working audit + header tools run on a real app, a secure-by-default config, a full-stack checklist, and the debug=True criticality explanation.