# microblog — Overnight Audit

> **This is a public sample report.** Forgehaven Labs ran the Overnight Audit
> process against [`miguelgrinberg/microblog`](https://github.com/miguelgrinberg/microblog),
> the MIT-licensed companion repo to the Flask Mega-Tutorial, to show what a
> paying customer receives. This is *teaching* code and it is excellent at that
> job. We audit it here as if a developer had forked it toward production, which
> is exactly how thousands of people use it. Every finding is reproducible from
> Section 7.

**Prepared for:** Public sample (miguelgrinberg/microblog, unsolicited)
**Prepared by:** Forgehaven Labs LLC · stevenagreen.com/overnight-audit
**Date:** 2026-07-01 · **Turnaround:** sample (process normally completes in under 24 h)
**Tier:** Repo Audit ($149)
**Scope:** `github.com/miguelgrinberg/microblog` @ `a975ef6` (default branch, 2025-04-06)

---

## 1. The verdict (read this first)

**Overall grade: C+ (as production code) / A (as a tutorial)** · Clean, idiomatic Flask with proper password hashing and CSRF protection. The gap between "great tutorial" and "safe to deploy" is two things: a **weak default secret** that, if it survives to production, makes password-reset tokens and session cookies forgeable, and a **dependency set frozen in late 2023** where 17 of 54 pinned packages now carry published CVEs, including two HIGH request-smuggling issues in the WSGI server you deploy behind.

> **If you fix only one thing:** guarantee `SECRET_KEY` is set from a strong random env var in every deployed environment and make the app refuse to boot without it (Section 3, P0-1). That single change closes an account-takeover path.

---

## 2. Scorecard

| Area | Grade | One-line summary |
|------|-------|------------------|
| Security | C | Password hashing + CSRF are correct; the weak `SECRET_KEY` fallback and an unignored `.env` are the risks |
| Correctness & error handling | A- | Idiomatic SQLAlchemy 2.0, sensible token expiry, clean auth flow |
| Dependencies & supply chain | D | 17 of 54 pinned deps have CVEs (2 HIGH in gunicorn, RCE-class in Werkzeug debugger) |
| Build, test & CI | B | `tests.py` present with real unit tests; CI wiring is minimal |
| Code health & maintainability | A | Small, readable, blueprint-structured, well-commented |
| Docs & onboarding | A | README + the tutorial itself are best-in-class |

**Verification statement:** the CVE list is authoritative OSV.dev data queried live against the exact pinned versions in `requirements.txt`; the secret-key and `.env` findings are confirmed against the source and `git check-ignore`. Section 7 lists every command.

---

## 3. Findings

Severity: **P0** fix before you ship · **P1** fix this month · **P2** fix when nearby · **P3** note.

### P0-1 · Weak fallback `SECRET_KEY` makes reset tokens and sessions forgeable if it reaches production

- **What we found:** `SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'`. That same key signs (a) Flask session cookies and (b) the HS256 JWT used for password-reset links (`get_reset_password_token` / `verify_reset_password_token`). If the app is ever deployed without `SECRET_KEY` in the environment, the key is public knowledge (it is in the source and in the tutorial), so an attacker can mint a valid reset token for any user id and take over the account, or forge an authenticated session cookie.
- **Evidence:** `config.py:9` (fallback); `app/models.py:181-190` (reset token signed with `SECRET_KEY`, verified with the same). Chain is: known key → forge `{'reset_password': <victim_id>}` JWT → `GET /reset_password/<token>` → set a new password.
- **Impact if ignored:** Full account takeover of any user, plus session forgery, for any deployment that forgot the env var. This is the single highest-impact issue in the repo.
- **Fix:** In production config, require the key and fail fast: `SECRET_KEY = os.environ['SECRET_KEY']` (or raise `RuntimeError` if unset in non-dev). Generate with `python -c "import secrets; print(secrets.token_hex(32))"`. Keep the friendly fallback only for the local tutorial config, clearly separated.
- **Effort:** 30 min.

### P1-1 · `.env` is not git-ignored; the README tells you to put secrets in it

- **What we found:** `config.py` calls `load_dotenv()` and reads `SECRET_KEY`, `MAIL_PASSWORD`, `MS_TRANSLATOR_KEY`, `DATABASE_URL` from `.env`. But `.env` is absent from `.gitignore` (which lists `venv`, `app.db`, `microblog.log*` but not `.env`).
- **Evidence:** `git check-ignore .env` → no match (not ignored); `.gitignore` contents confirm the omission; `config.py:1-12` reads secrets from `.env`.
- **Impact if ignored:** A developer who follows the setup and runs `git add .` commits real credentials (mail password, translator API key, production secret key) to history. This is the most common way secrets leak from Flask projects.
- **Fix:** Add `.env` (and `.env.*`) to `.gitignore` now. Ship a committed `.env.example` with empty values instead.
- **Effort:** 5 min. (If a real secret was ever committed: rotate first, then scrub history.)

### P1-2 · Dependencies frozen at late-2023 versions; 17 of 54 now carry published CVEs

- **What we found:** `requirements.txt` pins exact versions that were current around November 2023. Queried live against OSV.dev, 17 of the 54 pinned packages now have advisories. The ones that matter for a deployed app:
  - **`gunicorn==21.2.0`** — two **HIGH** HTTP request-smuggling advisories (GHSA-w3h3-4rj7-4ph4 endpoint-restriction bypass, GHSA-hc5x-x2vx-497g request/response smuggling). This is your production WSGI server. Fixed in 22.0.0+.
  - **`Werkzeug==3.0.1`** — six advisories incl. **HIGH** debugger remote code execution (GHSA-2g68-c3qc-8985), `safe_join` bypass on Windows, and form-parsing resource exhaustion.
  - **`Jinja2==3.1.2`** — five advisories incl. sandbox breakout via the `attr` filter (GHSA-cpwx-vrp4-4pq7).
  - **`requests==2.31.0`** — `Session` keeps `verify=False` after the first request (GHSA-9wx4-h78v-vm56); **`urllib3==2.1.0`** — 7 advisories; **`PyJWT==2.8.0`** — 6+ advisories (relevant given the reset-token flow above); plus `certifi`, `idna`, `setuptools`, `Flask`, `Flask-HTTPAuth`, `aiosmtpd`, `Mako`, `dnspython`, `Pygments`, `httpie`, `python-dotenv`.
- **Evidence:** OSV.dev `querybatch` over `requirements.txt` on 2026-07-01: **17/54 packages with advisories** (full GHSA list captured in Appendix A-1). Severities confirmed via OSV vuln records.
- **Impact if ignored:** The gunicorn smuggling issues are directly exploitable against a deployed instance; the Werkzeug debugger RCE matters if debug mode is ever enabled in a reachable environment. Compounding risk with P0-1 (PyJWT + weak key).
- **Fix:** Regenerate `requirements.txt` against current releases (`gunicorn>=23`, `Werkzeug>=3.0.6`, `Jinja2>=3.1.6`, `requests>=2.32.4`, `urllib3>=2.5`, `PyJWT>=2.10`), run the test suite, and add a scheduled `pip-audit`/Dependabot check so this does not refreeze.
- **Effort:** Half day incl. regression testing.

### P2-1 · No session-cookie hardening configured

- **What we found:** No `SESSION_COOKIE_SECURE`, `SESSION_COOKIE_HTTPONLY`, or `SESSION_COOKIE_SAMESITE` is set; the app relies entirely on Flask defaults. Over plain HTTP the session cookie ships without the `Secure` flag.
- **Evidence:** grep across the app for `SESSION_COOKIE*` / `REMEMBER_COOKIE*` returns nothing; `config.py` sets none.
- **Impact if ignored:** Session cookie can travel over HTTP and is more exposed to interception/CSRF-adjacent issues in a production deployment behind a misconfigured proxy.
- **Fix:** In production config set `SESSION_COOKIE_SECURE=True`, `SESSION_COOKIE_HTTPONLY=True`, `SESSION_COOKIE_SAMESITE='Lax'`, and `REMEMBER_COOKIE_SECURE=True`.
- **Effort:** 15 min.

### P2-2 · `ADMINS` still points at the placeholder address

- **What we found:** `ADMINS = ['your-email@example.com']`. The error-mail handler sends production exception emails to this list.
- **Evidence:** `config.py:20`.
- **Impact if ignored:** Production error notifications go to `example.com` (nowhere), so a deployed fork silently loses its crash alerts.
- **Fix:** Read `ADMINS` from an env var; document it in `.env.example`.
- **Effort:** 10 min.

### P3-1 · Things we checked that are NOT problems (so you can stop worrying about them)

- **`md5(self.email)` in `app/models.py:141`** is the Gravatar avatar hash, which Gravatar's API *requires*. It is not a password or security hash. Not a finding.
- **`os.system(...)` in `app/cli.py`** runs fixed `pybabel` command strings with no user input. Not injectable. Not a finding.
- **Password storage** uses `werkzeug.security.generate_password_hash` / `check_password_hash` correctly, and **CSRF** is on via Flask-WTF. Both correct.

---

## 5. The fix plan (next 30 days)

**Session 1 — 1 h (stop the bleeding):** Require `SECRET_KEY` in production and fail fast (P0-1); add `.env` to `.gitignore` and ship `.env.example` (P1-1).
**Session 2 — half day (this month):** Regenerate `requirements.txt` against current releases, re-run `tests.py`, add scheduled dependency scanning (P1-2).
**Later / opportunistic:** Session-cookie hardening (P2-1); real `ADMINS` from env (P2-2).
**Explicitly fine to ignore:** The Gravatar md5 and the `os.system` babel calls (P3-1).

## 6. Quick wins (under 30 minutes each)

1. Add `.env` and `.env.*` to `.gitignore` — one line, closes the most common secret-leak path.
2. `python -c "import secrets; print(secrets.token_hex(32))"` and set it as `SECRET_KEY` in every deployed env — de-risks P0-1 today even before the code guard lands.
3. Bump `gunicorn` to `>=23` — clears both HIGH request-smuggling CVEs on the server you actually deploy behind.

---

## 7. What we checked (methodology & reproducibility)

**Fleet + human:** an automated agent fleet ran the passes below on a fresh clone of `a975ef6`; a human engineer reviewed and reproduced every finding.

| Pass | Key commands (run on a fresh clone) |
|------|--------------------------------------|
| Secrets & config review | read `config.py`; traced `SECRET_KEY` usage to `app/models.py:181-190` |
| Gitignore check | `git check-ignore .env` (→ not ignored); read `.gitignore` |
| Dependency scan | OSV.dev `POST /v1/querybatch` over all 54 pinned versions in `requirements.txt` |
| Advisory severities | OSV.dev `GET /v1/vulns/<GHSA>` for the top issues |
| Auth/crypto review | read `app/models.py`, `app/auth/routes.py`, `app/api/auth.py` |
| False-positive triage | verified md5 = Gravatar, `os.system` = fixed babel strings |

**Limits (honest):** we did **not** execute `tests.py` in-sandbox. This host runs an externally-managed Python with no quick virtualenv path, and the suite pulls heavy optional services (Elasticsearch, Redis/RQ). The dependency findings come from OSV.dev against the exact pins, not from a runtime exploit; the secret-key and `.env` findings are confirmed from source and git. On a paid engagement we stand up a throwaway venv and run your suite. No penetration test or load test was performed.

---

## 8. Appendix — raw evidence

**A-1** — OSV.dev advisories by pinned package (2026-07-01):

```
aiosmtpd==1.4.4.post2   GHSA-pr2m-px7j-xg65, GHSA-wgjv-9j3q-jhg8, PYSEC-2024-221
certifi==2023.11.17     GHSA-248v-346w-9cwc, PYSEC-2024-230
dnspython==2.4.2        GHSA-3rq5-2g8h-59hc
Flask==3.0.0            GHSA-68rp-wp8r-4726
Flask-HTTPAuth==4.8.0   GHSA-p44q-vqpr-4xmg
gunicorn==21.2.0        GHSA-hc5x-x2vx-497g (HIGH), GHSA-w3h3-4rj7-4ph4 (HIGH)
httpie==3.2.2           GHSA-8r96-8889-qg2x, PYSEC-2023-242
idna==3.4               GHSA-65pc-fj4g-8rjx, GHSA-jjg7-2v4v-x38h, +2
Jinja2==3.1.2           GHSA-cpwx-vrp4-4pq7, +4
Mako==1.3.0             GHSA-2h4p-vjrc-8xpq, +2
Pygments==2.17.1        GHSA-5239-wwwm-4pmq
PyJWT==2.8.0            GHSA-752w-5fwx-jx9f, +10
python-dotenv==1.0.0    GHSA-mf9w-mj56-hr94
requests==2.31.0        GHSA-9wx4-h78v-vm56, +2
setuptools==68.2.2      GHSA-5rjg-fvgr-3xxf, GHSA-cx63-2mw6-8hw5, PYSEC-2025-49
urllib3==2.1.0          GHSA-2xpw-w6gg-jr37, +6
Werkzeug==3.0.1         GHSA-2g68-c3qc-8985 (HIGH debugger RCE), +5
```

---

*Forgehaven Labs LLC · Overnight Audit · fixed price, 24-hour turnaround.*
*Want this run against your repo? stevenagreen.com/overnight-audit*
