Capstone: Full Security Audit & Hardening
Stage 3 · Auth, Identity & Security · Capstone
You vibe-coded something real. Users signed up. People logged in. Now the question isn't "does it work?" — it's "would you trust it with your grandmother's password, your user's health data, or your own social security number?" This capstone is how you answer yes with evidence, not hope.
🎯 The mission
Take a real application — ideally something you've already built or vibe-coded — and run it through a complete, documented threat model and security audit. Then harden every finding you uncover, re-test to confirm each fix holds, and produce a before/after report that proves the app is fit for real people's real data. You're not patching holes blindly; you're doing what professional security engineers do: model the adversary first, then systematically eliminate their options.
🧱 What to do
Work through these steps in order. Check each box only after you have evidence, not just intention.
Phase 1 — Map the attack surface
- List every entry point into your app: login form, signup, password reset, API routes, file upload endpoints, admin panel, OAuth callbacks, public vs. authenticated pages
- List every data asset: what is stored, where, in what table/bucket, who can read it
- List every trust boundary: browser ↔ server, server ↔ database, server ↔ third-party OAuth provider, server ↔ storage bucket
- Draw (even a rough sketch counts) a data-flow diagram showing these three layers together
Phase 2 — Run STRIDE on your top five features
For each key feature, identify which STRIDE threat categories apply and what the concrete attack would look like. Example row:
| Feature | Threat | Category | Impact | Mitigation |
|---|---|---|---|---|
| Password reset | Attacker requests reset for any email, intercepts link | Spoofing | Account takeover | Short-lived single-use token; rate-limit endpoint; send to verified email only |
| File upload | Malicious .php disguised as .jpg | Tampering | RCE on server | Validate MIME server-side; store outside web root; serve via signed URL |
| JWT in localStorage | XSS reads token, replays it | Info disclosure | Full session hijack | Move to httpOnly cookie; add SameSite=Strict |
Fill one row per threat, minimum five features.
Phase 3 — Run the full Security Audit Checklist
Go line by line. For each item record: Pass / Fail / N/A and a one-sentence note.
Authentication & passwords
- Passwords hashed with bcrypt/argon2 (cost factor ≥ 12) — no plaintext, no MD5, no SHA-1
- Password reset tokens are single-use, expire in ≤ 1 hour, and are stored as hashes
- Brute-force protection: rate limiting on login + lockout or CAPTCHA after N failures
- "Forgot password" does not leak whether an email is registered (same response either way)
Sessions & tokens
- JWTs signed with a strong secret (≥ 256-bit) stored in env, not hardcoded
- JWT expiry is short (≤ 1 hour for access tokens); refresh tokens rotate on use
- Session cookies set
httpOnly,Secure,SameSite=Strict - Logout actually invalidates the token/session server-side (not just clears client storage)
Authorization & data ownership
- Every authenticated route checks
userId === resource.ownerId(or equivalent) — no IDOR - Row-Level Security (or equivalent) is on for every user-data table
- Admin-only routes verify the admin role on the server, not just client-side
- RBAC roles cannot be self-assigned by regular users
Secrets & environment
- Zero secrets in source code or git history (
git log -S "sk_live"returns nothing) -
.envis in.gitignore; production secrets are in the platform's secret store - All API keys that were ever accidentally committed have been rotated
-
npm audit(orpip audit/cargo audit) returns zero critical vulnerabilities
Input validation & injection
- All database queries use parameterized statements or an ORM — no string concatenation
- User-supplied HTML is sanitized before rendering (no raw
innerHTMLwith user data) - File uploads: MIME type validated server-side, size capped, stored outside web root
- Redirect targets after login are validated against an allowlist (open redirect check)
HTTPS & transport
- HTTPS enforced on all routes; HTTP redirects to HTTPS
-
Strict-Transport-Securityheader present withmax-age ≥ 31536000 -
Content-Security-Policyheader set;unsafe-inlinenot used without a nonce/hash - CORS:
Access-Control-Allow-Originis not*for authenticated endpoints
Uploads & storage
- Uploaded files are not executable in their storage location
- Files are served via signed/expiring URLs, not public permanent links
- File names are sanitized or replaced with random UUIDs before storage
Observability & incident response
- Failed login attempts are logged with timestamp, IP, and username (not password)
- Privilege escalation attempts (unauthorized role access) are logged and alerted
- You can answer "when did user X last log in and from what IP?" within 60 seconds
Phase 4 — Fix every Fail
For each finding, write a fix, deploy it, and re-test. A finding is not closed until you have re-run the specific check and it passes.
Short example of a hardening diff you might make:
- // BAD: secret in code
- const jwtSecret = "my-super-secret-key-123";
+ // GOOD: secret from environment, startup fails fast if missing
+ const jwtSecret = process.env.JWT_SECRET;
+ if (!jwtSecret || jwtSecret.length < 32) {
+ throw new Error("JWT_SECRET env var missing or too short — refusing to start");
+ }
- // BAD: IDOR — any user can fetch any order
- app.get("/orders/:id", async (req, res) => {
- const order = await db.orders.findById(req.params.id);
- res.json(order);
- });
+ // GOOD: ownership enforced
+ app.get("/orders/:id", requireAuth, async (req, res) => {
+ const order = await db.orders.findById(req.params.id);
+ if (!order || order.userId !== req.user.id) return res.status(404).json({ error: "Not found" });
+ res.json(order);
+ });
🗺️ Run it through B.U.I.L.D.
B — Build the baseline. Your starting point is the app as it exists right now. Screenshot or document the current state before you change anything. You need a before to compare against.
U — Understand the attack surface. This is Phase 1 and Phase 2 above. Don't skip it. Every hour you spend here saves three hours of chasing bugs that "shouldn't happen." Adversaries think in attack paths; so should you.
I — Implement the checklist. Phase 3. Methodical, line by line. "Probably fine" is not a status. Pass, Fail, or N/A.
L — Lock it down. ← The heart of this capstone. Phase 4. Every Fail gets a fix. Every fix gets a re-test. The goal is not a perfect score before you start — it's a perfect score before you ship. Real security engineering is iterative, not lucky.
D — Document and demonstrate. Your deliverables are the proof. The threat model doc, the findings report, the before/after summary. Anyone on your team (or a future auditor) should be able to read your report and understand exactly what was wrong, what you did, and what the current state is. Undocumented fixes don't exist.
🧪 Deliverables
Submit all four of the following:
1. Threat Model Document A written document (Markdown is fine) containing: your entry-point and data-asset lists, your trust boundary diagram (hand-drawn and photographed counts), and your completed STRIDE table with at minimum five rows covering your top features.
2. Security Findings Report A table with one row per audit checklist item that was a Fail. Columns: Finding · Severity (Critical / High / Medium / Low) · How to Reproduce · Fix Applied · Re-Test Status.
| Finding | Severity | Repro | Fix Applied | Re-Test |
|---|---|---|---|---|
JWT secret hardcoded in server.js line 12 | Critical | git log -S "jwt_secret" reveals it | Moved to JWT_SECRET env var; rotated key | Pass |
No rate limiting on /login | High | for i in {1..200}; do curl -X POST /login ...; done completes in 4s | Added express-rate-limit 10 req/min per IP | Pass |
3. The Hardened Codebase Your app after all fixes are applied. A git diff or a clean branch showing only the security changes is ideal — it makes the before/after obvious.
4. Before/After Summary One page (or a short README section) that a non-technical stakeholder could read. How many findings total? How many Critical/High/Medium/Low? What percentage are now resolved? What residual risk (if any) remains and why?
🏆 Stretch goals
These are not required to graduate this capstone, but they separate a good audit from a great one:
- Add MFA. Implement TOTP (Google Authenticator-compatible) or magic-link email as a second factor for login. Require it for admin accounts at minimum.
- Automate secret scanning in CI. Add
trufflesecurity/trufflehogorgitleaksas a GitHub Actions step that blocks merges if secrets are detected. Addnpm audit --audit-level=high(or equivalent) to the same pipeline. - Red team with a second pair of eyes. Have someone who did NOT build the app try to break it for 30 minutes with only the running URL. Document what they tried, what they found, and how you responded.
- Add a
Security.mdto the repo root: how to report a vulnerability, your response commitment, contact address.
✅ You're done when…
- The Security Audit Checklist (Phase 3 above) has been completed in full — every item is Pass or N/A, with a note; zero Fails remain open
- The Production-Readiness Checklist from Stage 3 has been reviewed and every security-related item is checked off
- A threat model document exists for your app, and every finding rated High or Critical has been fixed, re-tested, and marked Pass in your findings report
- No secrets exist in your code or git history; any secret that was ever accidentally committed has been rotated and the exposure window is documented
- Your findings report has at least one entry (honest audits always find something); even a Medium finding counts — the goal is honest enumeration, not a clean scorecard
- A second person (peer, mentor, or rubber duck that you explained every finding to) has reviewed your findings report and agrees the fixes are real
A vibe-coded app that ships fast and then gets hardened is infinitely better than an app that never ships because it's waiting to be perfect. You built something. Now you've locked it down with evidence. That's the move professionals make.
➡️ Next: Stage 3 continues with Architecture & System Design — build things that survive success.
Build It Right, Or Don't Build It At All. 🏛️