Skip to main content
Auth, Identity & Security
🔐 Auth & SecurityLesson 13 of 13

Capstone: Full Security Audit & Hardening

Full security audit and hardening of an app: threat model, fixes, before/after.

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:

FeatureThreatCategoryImpactMitigation
Password resetAttacker requests reset for any email, intercepts linkSpoofingAccount takeoverShort-lived single-use token; rate-limit endpoint; send to verified email only
File uploadMalicious .php disguised as .jpgTamperingRCE on serverValidate MIME server-side; store outside web root; serve via signed URL
JWT in localStorageXSS reads token, replays itInfo disclosureFull session hijackMove 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)
  • .env is in .gitignore; production secrets are in the platform's secret store
  • All API keys that were ever accidentally committed have been rotated
  • npm audit (or pip 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 innerHTML with 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-Security header present with max-age ≥ 31536000
  • Content-Security-Policy header set; unsafe-inline not used without a nonce/hash
  • CORS: Access-Control-Allow-Origin is 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.

FindingSeverityReproFix AppliedRe-Test
JWT secret hardcoded in server.js line 12Criticalgit log -S "jwt_secret" reveals itMoved to JWT_SECRET env var; rotated keyPass
No rate limiting on /loginHighfor i in {1..200}; do curl -X POST /login ...; done completes in 4sAdded express-rate-limit 10 req/min per IPPass

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/trufflehog or gitleaks as a GitHub Actions step that blocks merges if secrets are detected. Add npm 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.md to 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. 🏛️

Pre-launch security · partner tool

Built it? Now scan it. The HYVE Audit finds security holes before launch — $55, and your code never leaves your machine.

Run the audit ↗

Always-on rigor toolkit

🏛️ Build It Right, Or Don't Build It At All.