All chapters Part VI · Quality
Chapter 30

Security and Code Review

Finding vulnerabilities is easy and getting cheaper every year. Fixing them requires a mechanism, and that's the part almost everyone skips. A review that doesn't become tracked work produces a document, not a safer product.


The concept

Security for a small product isn't about exotic attacks. It's about a short list of well-understood failures that appear in nearly every codebase, plus one process problem that determines whether any of them get fixed.

The realistic threat model

You are not being targeted by a nation state. You are exposed to:

ThreatReality
Automated scannersConstant, indiscriminate, hitting every public endpoint
Curious usersWill open DevTools, edit local storage, and try ?admin=true
Credential stuffingLeaked passwords from other breaches, tried against you
Abuse of free tiersScripts exhausting your quotas or generating your costs
Accidental exposureSecrets in git, public buckets, over-permissive rules
Supply chainA dependency ships something you didn't audit

The last two cause more real incidents at small scale than clever attacks do.

The short list that matters

1. Never trust the client. Anything a client can send, an attacker can forge. Entitlement, permissions, prices, user ids in request bodies, all must be verified server-side. If a URL parameter or a local storage value can grant access, it will.

2. Authorization on every request. Authentication says who; authorization says what they may touch. The classic failure is checking that someone is logged in and not checking whether this record is theirs.

3. Escape all output. Any user-controlled data rendered into HTML must be escaped. Frameworks do this by default; going without one means re-adding it deliberately (Chapter 15).

4. Parameterise all queries. Never build SQL by string concatenation. Every ORM and query builder does this correctly; hand-rolled string building does not.

5. Secrets never reach a client. Publishable keys are designed to be public provided your server rules are correct. Secret keys never touch a browser or a binary.

6. Rate-limit every public endpoint. Signup, login, password reset, contact forms, anything that sends email or costs money.

7. Validate input server-side. Client validation is UX. Server validation is security.

8. Never become an open relay. Any endpoint that sends email, SMS, or requests to arbitrary addresses on user input is an abuse vector, and it spends your reputation.

9. Fail closed. Missing configuration must deny, never allow.

10. Audit your dependencies. Know what ships in your binary and what it collects.

⭐ The process problem

Here's the part that actually determines outcomes.

A review, automated, AI-assisted, or human, produces a list of findings. Then, reliably, this happens:

  1. Findings live in a document or a chat message
  2. They have no owner, no date, no tracking
  3. Feature work has deadlines; findings don't
  4. Nothing enforces them, so nothing fails
  5. Months later they're still open

Work without a deadline loses to work with one, every single time. The fix is mechanical, not motivational:

 1. CONVERT each finding into a tracked item — issue, ticket,
    or a TODO(sec) in the code. Somewhere it will be SEEN AGAIN.

 2. ASSIGN severity and a DEADLINE
    Critical → before next release
    High     → before launch
    Medium   → dated backlog
    Low      → backlog

 3. ADD A RELEASE GATE
    "All Critical and High closed" — a checkbox you must tick

 4. AUTOMATE what can be automated
    secret scanning · dependency audit · a grep for the specific
    pattern that caused the finding

 5. RE-RUN THE REVIEW before launch
    Once at the start is diagnosis. Once before shipping is the gate.

Step 1 is the whole thing. A finding not in a tracked list does not exist.

Code review that finds real problems

Whether by a person or a tool, the useful questions are:

AI review is genuinely good at this now, it reads more thoroughly than a tired human and finds real exploitable issues. It's poor at judging which matter for your context, and it has no mechanism to ensure anything is fixed.

Security by capability

Each capability you add brings obligations:

You addYou now need
AccountsRate-limited auth, secure sessions, account deletion
PaymentsServer-verified entitlement, webhook signature checks
File uploadsType/size validation, no execution, scanned or isolated storage
User contentEscaping, moderation, reporting
Email sendingRate limits, no open relay, authentication (Chapter 35)
Third-party dataTreat as untrusted; never execute
AI featuresPrompt injection defence; never authorise from model output

📐 Best practice

Run a security review early and again before launch.

⭐ Convert every finding into a tracked item with a severity and a date, in the same session.

Add "all Critical/High closed" as a release gate.

Never grant access from client-controllable input.

Escape all output; parameterise all queries.

Rate-limit every public endpoint.

Secret-scan in CI. Rotate anything that has ever been committed.

Audit dependencies with npm audit / Dependabot, and know what each SDK ships.

Fail closed everywhere.

Run your platform's security advisor, managed databases ship one.

Sweep backward when you fix a class of issue.


💀 Common mistakes

⭐ Findings that never become tracked work. The dominant failure.

Client-side entitlement. URL parameters, local storage flags, hidden fields.

Authentication without authorization. Logged in ≠ allowed to see this record.

Unescaped output. Especially without a framework.

String-concatenated SQL.

Secrets in the repo. Git history is forever, rotate, don't just delete.

No rate limiting on signup, login, reset, or anything that sends mail.

Open relays. An endpoint that emails arbitrary submitted addresses.

Fail-open configuration. Missing secret treated as "no auth needed."

Over-permissive database rules. "Allow all authenticated" is not authorization.

Public storage buckets.

Ignoring "only exploitable if…" findings. Conditions change; that's a finding about your roadmap.

Fixing forward only. New code safe, old code still exposed.


The professional workflow

 1. THREAT MODEL — 15 minutes. What's valuable? Who'd want it? Where's it exposed?

 2. AUTOMATED BASELINE
    dependency audit · secret scan · static analysis ·
    platform security advisor

 3. MANUAL/AI REVIEW of auth, payments, data access, public endpoints

 4. ⭐ CONVERT FINDINGS → tracked items, severity, DATE — same session

 5. FIX CRITICALS NOW

 6. AUTOMATE THE REGRESSION
    a CI check for the exact pattern that caused each finding

 7. ADD THE RELEASE GATE

 8. RE-RUN before launch

 9. SWEEP BACKWARD for the same shape elsewhere

10. RECURRING: monthly dependency audit; annual full review

Tools, websites & costs

NeedToolCost
AI code/security reviewClaude, CodeRabbit, Greptile$0-30/mo
Dependency auditnpm audit, Dependabot, SnykFree
Secret scanninggitleaks, TruffleHog, GitHub secret scanningFree
Static analysisSemgrep, CodeQLFree (OSS)
Database advisorsSupabase/Firebase built-in advisorsFree
Bot / abuse protectionCloudflare Turnstile, hCaptchaFree
Rate limitingUpstash Ratelimit, platform middlewareFree tiers
Header / TLS checksecurityheaders.com, SSL LabsFree
Vulnerability disclosureA security.txt and a monitored emailFree
Pen testAn agency$5k-50k, not pre-launch
Compliance (SOC 2)Vanta, Drata$5k+/yr, when enterprise asks

Everything a pre-launch product needs is free. The bottleneck is never cost; it's follow-through.


Alternatives & trade-offs

AI review vs human vs automated scanning. Automated scanning catches known patterns cheaply. AI review reads context and finds logic flaws. Humans bring judgement about what matters. Use all three; they find different things.

Fix everything vs triage. Fixing every low-severity finding burns time you need for the product. Ignoring criticals is negligence. Triage by exploitability × impact, and be honest that "hard to exploit today" often means "easy after your next feature."

Security early vs later. Retrofitting authorization into a product built without it is a rewrite. Retrofitting rate limiting is an afternoon. Get the architectural ones right early, authorization model, secret handling, entitlement, and defer the operational ones.

Bug bounty. Effective and premature before you have meaningful traffic. A security.txt with a monitored email costs nothing and catches good-faith reports.

Compliance certifications. SOC 2 is a sales unlock, not a security improvement. Do it when a deal requires it.


Checklist


📓 Case Study: eight findings on day one, seven still open on day twenty-four

Project: SOLIS. A serious automated security review ran in the first hours of the project and produced eight findings, each with a concrete exploit scenario. Excellent work, done early, exactly as recommended.

Selected findings, verbatim:

Premium access is granted by checking a client-side URL parameter and storing the result locally, no server verification. Any user appends ?unlocked=1; the code sets premium and saves it. Full paywall bypass, permanently, with no server gate to fall back on.

The waitlist endpoint has no origin check, rate limit, or CAPTCHA, any party who discovers the URL can POST unlimited arbitrary email addresses. Thousands of fake addresses flood the sheet, the mail quota is exhausted in seconds, and the script relays confirmations to arbitrary third-party addresses, turning the endpoint into an open spam relay with the owner's account as sender.

The service worker caches every resolved response without checking res.ok, so error pages are cached and served to all future visitors. A botched deploy becomes permanent for everyone who visited during the window.

User data from local storage is interpolated unsanitized into an HTML insertion, stored XSS.

Lesson content is interpolated into innerHTML with no escaping. Low exploitability today, but if content ever moves to a CMS, a natural growth step, any crafted entry executes JavaScript in the app's origin.

⚠️ The scorecard, twenty-four days later:

#FindingSeverityStatus
1Paywall bypass via URL parameter🔴 CriticalSTILL OPEN
2Service worker caches error responses🟠 HighSTILL OPEN
3Content repeated after item 21🟠 High✅ Fixed, by accident
4Service worker resolves undefined🟡 MediumSTILL OPEN
5Waitlist open relay🔴 CriticalSTILL OPEN
6Stored XSS via user data🟠 HighSTILL OPEN
7XSS surface in content rendering🟡 MediumSTILL OPEN
8Weak email validation🟢 LowSTILL OPEN

Seven of eight open. And the one that was fixed was fixed by accident, writing the real content happened to remove the repetition. No finding was ever fixed because it was a finding.

Why, and this is the actual lesson:

  1. They lived in a chat message. Messages scroll away. Nothing in the project's task list, status file, or launch checklist ever referenced them.
  2. No deadlines. Every finding was "should fix." None had a when.
  3. Nothing enforced them. No CI, no linter, no gate. Nothing could ever fail because of them.
  4. Features had deadlines; findings didn't. Twenty-four days of visible progress, content, art, backend, payments, onboarding, each with a launch pulling forward. A security fix produces no visible progress, so it loses every daily argument.
  5. No follow-up loop. The review ran once, on day one, and never again.

Note the fixes were not hard. Finding 2 is one conditional (if (res.ok)). Finding 1 is deleting a block. Finding 6 is a fifteen-line escaping helper. Difficulty was never the reason.

What the review did achieve, which is worth crediting. Everything designed after it internalised the lessons: row-level security on every table with both clauses; entitlement tables read-only to clients; webhooks that verify a secret and fail closed; idempotency enforced by database constraints; a security definer function hardened after a platform advisor flagged it.

📐 The pattern generalises: a review changes how you write new code and does nothing to old code. That's a partial win, and it's exactly why "sweep backward" belongs in the workflow.

⚠️ A second process failure with the same root. A development flag that unlocked the entire paid product for free appeared on four separate checklists and was still enabled at the end of the project. Four written reminders did not disable one boolean.

📐 Checklists are for things that cost you time. Automation is for things that cost you the business. One grep in CI would have failed the build on every commit for 24 days.


Lessons

  1. ⭐ A review produces a document. A document is not a fix. Seven of eight open after 24 days.
  2. Convert findings to tracked items with dates, in the same session. This single step is the whole chapter.
  3. Work without a deadline loses to work with one. Every time.
  4. Difficulty is not what determines whether something gets fixed. These were one-line fixes.
  5. Reviews change new code, not old code. Sweep backward deliberately.
  6. Your convenience features are your attack surface. A test unlock parameter and a content-review flag both gave away the product.
  7. Client-side entitlement is a suggestion. The server must agree.
  8. "Only exploitable if we grow" is a finding about your growth plan.
  9. Never become an open relay. It spends your domain's reputation on someone else's spam.
  10. Automate the catastrophic checks. A grep in CI beats four checklists.
  11. Add a release gate. "All Critical/High closed" is a checkbox that would have caught all of this.

Next: Chapter 31: Performance, Cost and Scaling →

Useful? Share this chapter