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

Running a Real Security Audit

Run a real, structured security audit on your own code, start to finish.

Running a Real Security Audit

Stage 3 · Auth, Identity & Security · B.U.I.L.D. letter: L

You've built eleven lessons worth of defenses — hashed passwords, short-lived tokens, RBAC, parameterized queries, HTTPS, secrets in env vars, rate limits on login. Now do the one thing most developers never do: go back and actually check whether you did them. A security audit is not a formality. It is the moment you find out whether your defenses are real or just well-intentioned.


⚠️ The vibe trap

"It hasn't been hacked yet" is not evidence of security — it is evidence that no one with enough motivation has looked yet. The reason most breaches feel like surprises is that developers shipped without ever attempting to attack their own app. Vibe coding moves fast and ships features; auditing is the deliberate gear-change where you slow down, act like an adversary, and find the gaps before someone else does. Not checking is not the same as not being vulnerable.


🗺️ Section 1 — The Repeatable Audit Process

A one-off scan is nearly useless. What you want is a documented, repeatable loop you run before every meaningful release and on a calendar schedule after launch. The five phases:

  1. Scope — define what you are auditing (routes, auth flows, file upload, DB rules, dependencies, infrastructure).
  2. Automated sweep — run the tools (secret scanner, dependency audit, SAST, headers check).
  3. Manual probe — act like an attacker; try the specific attacks the track has covered.
  4. Write up — every finding gets severity, reproduction steps, and a fix.
  5. Verify — after fixing, re-run the exact test that caught the issue and confirm it now fails cleanly.

That last step — re-testing after the fix — is the one teams always skip. A finding is not closed until you have actively confirmed the attack no longer works.

Mental model. Think of this as a CI pipeline for trust. Your code CI checks that the app works. Your security audit CI checks that the app can't be abused.

Common mistake. Running npm audit once at project start, seeing zero high severities, and never running it again. Dependencies ship new vulnerabilities every week.


🤖 Section 2 — Automated Scanning

Automation finds the wide net of low-hanging fruit fast. Run all of these before you touch anything manually.

Dependency vulnerabilities

# Built into npm — run this from your project root
npm audit

# For a machine-readable report you can diff across runs
npm audit --json > audit-$(date +%Y-%m-%d).json

# Fix automatically where npm considers it safe (review the diff after)
npm audit fix

# Snyk gives richer context and tracks fixes over time (free tier available)
npx snyk test

Secret scanning — catch leaked keys before git does

# truffleHog scans your entire git history for high-entropy strings and known key patterns
npx trufflesecurity/trufflehog git file://. --only-verified

# gitleaks is faster for a quick local scan of the working tree
# (install once: https://github.com/gitleaks/gitleaks)
gitleaks detect --source . --verbose

# Quick grep for the most common accidental commits (not a substitute for the above)
git log --all -S "ANTHROPIC_API_KEY" --oneline
git log --all -S "sk-" --oneline

Security headers check

# curl shows you exactly what headers your production server sends
curl -sI https://your-app.vercel.app | grep -Ei "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy"

# securityheaders.com grades your headers — paste your URL or use the API
curl "https://api.securityheaders.com/?q=https://your-app.vercel.app&hide=on&followRedirects=on"

Basic SAST (static analysis)

# ESLint with the security plugin catches common JS security anti-patterns
npm install --save-dev eslint-plugin-security
# Add to .eslintrc.json:  "plugins": ["security"], "extends": ["plugin:security/recommended"]
npx eslint . --ext .js,.ts

# semgrep has open-source rules for Node/Express security issues
# (install: https://semgrep.dev/docs/getting-started)
semgrep --config "p/nodejs" .

Why it works. These tools cover surface area a human would take days to read through. They are not a replacement for manual testing — they produce false positives and miss logic flaws — but they catch whole classes of mechanical errors instantly.

Common mistake. Treating a clean automated scan as a green light to ship. Automated tools cannot test whether your access-control logic is correct; they can only tell you whether you used a parameterized query.


🕵️ Section 3 — Manual Probing

Automated tools find what you used; manual testing finds how you used it. These are the tests that catch logic flaws.

IDOR — try changing an ID

The most common vulnerability on the web. Find any URL or API call that contains a record ID and try swapping it for another user's ID. You need two test accounts.

# Account A: get your own order ID from a normal request, e.g. order 42
curl -s -X GET https://your-app.com/api/orders/42 \
  -H "Authorization: Bearer TOKEN_FOR_USER_A"

# Now try to read account B's order (e.g. order 43) using account A's token
# If you get data back, you have an IDOR vulnerability
curl -s -X GET https://your-app.com/api/orders/43 \
  -H "Authorization: Bearer TOKEN_FOR_USER_A"

# Expected safe response: 404 or 403
# Vulnerable response: 200 with order data belonging to user B

If you get order data back for a different user, that route never checks ownership. Fix: add AND user_id = $2 to the query as covered in sec-01 and sec-06.

Login rate limiting — try brute-forcing yourself

# Send 20 login attempts in a loop; watch when (or whether) you get a 429
for i in $(seq 1 20); do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST https://your-app.com/api/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email":"test@example.com","password":"wrongpassword"}')
  echo "Attempt $i: HTTP $STATUS"
done

If every attempt returns 401 and none return 429, you have no rate limit. An attacker can automate this at thousands of requests per second. Fix: add express-rate-limit to your login route as covered in sec-02.

Stack traces in error responses

# Send a malformed request and look at the response body
curl -s -X GET "https://your-app.com/api/orders/not-a-valid-uuid" \
  -H "Authorization: Bearer YOUR_TOKEN" | jq .

# Also try an obviously broken JSON body
curl -s -X POST https://your-app.com/api/orders \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"broken json'

If the response contains file paths, line numbers, package names, or a stack field, your error handler is leaking internals. Fix: wrap your production error handler to check NODE_ENV as covered in sec-06.

Mental model. You are not trying to destroy your app. You are trying to find the answers to three questions: Can I read data I shouldn't? Can I act without consequences? Can I make the app reveal its internals?

Common mistake. Only testing with your own account in an admin or developer role. Most logic flaws are visible only from the perspective of a regular user with no special privileges — that is your primary test identity.


📋 Section 4 — The Security Audit Checklist

The Security Audit Checklist is the single document that ties every lesson in this track into one repeatable pass. Work through it top to bottom, checking each item against your actual running app — not your memory of what you think you built.

AreaWhat to checkCovered in
SecretsNo API keys, JWT secrets, or DB passwords in source or git historysec-08
AuthNPasswords hashed with bcrypt ≥ cost 10; JWT/session expiry set and enforcedsec-02, sec-03
AuthZ / OwnershipEvery DB query that uses a user-supplied ID also filters by user_idsec-01, sec-05
Input validationEvery route validates shape and type before using inputs; no eval or Function()sec-06, sec-07
InjectionAll queries parameterized; no string-concatenated SQL; no exec with user inputsec-06
Dependenciesnpm audit returns zero high/critical; last run < 7 days agothis lesson
TransportHSTS header present; no mixed content; all cookies have Secure flagsec-10
File uploadsMIME type validated server-side; stored outside web root or in object storagesec-09
LoggingAuth events (login, logout, failed attempts) logged with timestamp and IPsec-12 (this lesson)
DB rulesRLS enabled; anon role cannot read private tables; service-role key not in client bundlesec-05
Rate limitsLogin and password-reset routes throttled; no 200 on 20+ rapid attemptssec-02
Error responsesProduction error handler returns generic message; no stack traces to clientssec-06
Security headershelmet() or equivalent applied; CSP, HSTS, X-Frame-Options all presentsec-10

Work through this table. Put a checkmark, a finding ID, or "N/A" in each row. A blank row is a gap.


📝 Section 5 — Writing Up Findings and Verifying Fixes

A finding that lives only in your head is a finding that will be forgotten. Write each one up in a consistent format. Here is a sample:


Finding: IDOR on /api/orders/:id Severity: High Reproduction:

  1. Create two accounts: alice@test.com and bob@test.com.
  2. Log in as Alice and create an order. Note the returned id (e.g., 42).
  3. Log in as Bob and send GET /api/orders/42 with Bob's JWT.
  4. Response: 200 OK with Alice's order data.

Root cause: Route handler queries SELECT * FROM orders WHERE id = $1 without adding AND user_id = $2. Bob's token is valid (authentication passes) but his access to Alice's record is never checked (authorization missing).

Fix: Change the query to SELECT * FROM orders WHERE id = $1 AND user_id = $2, passing [req.params.id, req.user.id].

Verification: After deploying the fix, repeat steps 3–4. Expected result: 404 Not Found. Confirm no data is returned.


Keep a findings log (a markdown file, a Notion page, a GitHub issue — whatever your team uses) with one entry per finding. Number them. Reference the number in the git commit that fixes it. When you re-test, update the entry with "Verified closed: [date]."

Mental model. A finding without a verification step is half-done security work. The whole point of the audit is not to find problems — it is to close them.

Common mistake. Fixing the specific line you found in the audit without checking whether the same pattern appears elsewhere in the codebase. After fixing an IDOR on /api/orders/:id, grep every other route file for the same query shape.

# After fixing an IDOR, check whether the same pattern exists elsewhere
grep -rn "WHERE id = \$1" ./src/routes/
# Any query that uses only id without user_id is a candidate for the same bug

🛠️ Your mission

Open your project. Work through the Security Audit Checklist from Section 4 top to bottom. For every row where you cannot immediately confirm "yes, this is done," treat it as a potential finding. Then:

  1. Pick the three most severe findings you uncovered.
  2. Write up each one in the format from Section 5: severity, reproduction steps, root cause, fix, verification plan.
  3. Fix all three.
  4. Re-run the specific test that caught each one (whether that is a curl command, an npm audit, or a manual IDOR probe) and confirm the attack no longer succeeds.
  5. Update your checklist to show each item as verified and dated.

If you find zero findings, that is a result worth being proud of — and also a result that deserves skepticism. Pick the three checklist items you are least certain about and go one level deeper on those.


✅ You're done when…

  • You have worked through every row of the Security Audit Checklist and either confirmed it passes or logged a finding
  • You have reviewed your Production-Readiness Checklist and confirmed that all security-relevant items (HTTPS enforced, secrets in env vars, error messages sanitized, CORS locked down) are checked off before any deployment
  • You have written up at least three findings in the severity / repro / fix / verification format and confirmed each one closed by re-running the original test
  • npm audit reports zero high or critical severity vulnerabilities in your dependency tree
  • No secret scanner (truffleHog or gitleaks) reports any verified findings in your repository history

➡️ Next: the Capstone — Full Security Audit & Hardening. 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.