T

AI App Rescue

Tallgrass Supplier Portal — before / after hardening

Taking an AI-built app to production

A non-technical founder prompt to headless Claude Code produced a working Next.js + Postgres supplier portal. Its first pass was fairly careful — so I reverted the shipped before/ app to the common vibe-coded failure classes to represent the real rescue market, proved each one with an automated exploit, then hardened after/ and re-ran the same exploits.

Tallgrass Precision Components is fictional · demo by Garrett Smith

13
findings, with exploits
13/13
exploits work on before
0/13
exploits work on after
8/8
functional tests pass on after
SeverityFindingsFixed in after
Critical44
High44
Medium44
Low11

Generated from tests/results.json on 2026-10-07 by tests/run.mjs.

Findings — before vs after

Critical

E-001 · Row-Level Security disabled on tenant tables (CVE-2025-48757 class)

Exploit E-001 — before EXPLOITED RLS OFF on: certs, messages, purchase_orders, users, suppliers
after blocked RLS enabled on all tenant tables
Critical

E-002 · IDOR: a supplier can read another supplier’s purchase order

Exploit E-002 — before EXPLOITED 200 -> TPC-PO-26105 (Meridian Medical Components)
after blocked blocked: HTTP 404
Critical

E-005 · Auth bypass: forge an unsigned session cookie

Exploit E-005 — before EXPLOITED forged cookie -> 10 POs across 3 suppliers
after blocked blocked: HTTP 401
Critical

E-006 · SQL injection in the part-number search

Exploit E-006 — before EXPLOITED dumped 6 credential rows (e.g. priya.nair@ridgelineaero.example / Demo-Supplier-2026!)
after blocked injection blocked / no rows
High

E-003 · Secret shipped in the client bundle (NEXT_PUBLIC service key)

Exploit E-003 — before EXPLOITED key found in 2 client chunk(s)
after blocked no secret in client bundle
High

E-004 · Open CORS: wildcard origin with credentials

Exploit E-004 — before EXPLOITED ACAO:* ACAC:true
after blocked ACAO:none
High

E-009 · Passwords stored in plaintext

Exploit E-009 — before EXPLOITED 6/6 passwords are not hashed (e.g. "Demo-Buyer-2026!")
after blocked all passwords bcrypt-hashed
High

E-011 · Upload validation bypass → stored XSS via certificate content-type

Exploit E-011 — before EXPLOITED non-PDF stored and served as text/html
after blocked upload rejected: HTTP 400
Medium

E-007 · No rate limiting on login (credential brute force)

Exploit E-007 — before EXPLOITED 12 rapid attempts, never throttled
after blocked throttled after 6 attempts (429)
Medium

E-008 · Verbose errors leak stack traces and SQL

Exploit E-008 — before EXPLOITED response includes stack trace
after blocked generic error only
Medium

E-010 · Missing security headers (no CSP / HSTS / frame protection)

Exploit E-010 — before EXPLOITED no CSP (X-Frame-Options: missing)
after blocked CSP + headers present
Medium

E-013 · Insecure deploy config: secrets committed, no health check

Exploit E-013 — before EXPLOITED .env committed (not git-ignored); /api/health -> 404
after blocked .env git-ignored / absent; /api/health -> 200
Low

E-012 · N+1 queries on the dashboard list

Exploit E-012 — before EXPLOITED 10 separate certs queries per list request
after blocked 1 separate certs query per list request

Functional tests (hardened app still works)

IDTestResultEvidence
FN-1Valid supplier login succeedsPASSlogin HTTP 200
FN-2Wrong password is rejectedPASSHTTP 401
FN-3Supplier sees only their own company’s POsPASS4 POs, suppliers: Ridgeline Aerospace Machining
FN-4Buyer sees all suppliers’ POsPASS10 POs across 3 suppliers
FN-5Search by part number worksPASS1 match "INC"
FN-6Supplier can send a message on their own POPASSHTTP 201
FN-7Valid PDF upload is accepted; non-PDF is rejectedPASSpdf:201 non-pdf:400
FN-8Health check reports OKPASSHTTP 200 {"status":"ok","db":"up","time":"2026-10-07T07:35:15.358Z"}

Production hardening checklist

Production hardening checklist — AI-built / prototype web apps

A reusable checklist I run when taking a prototype (Lovable, v0, Bolt, Cursor, a

weekend Next.js + Postgres/Supabase build) to production. Ordered by how often the

item is the thing that actually gets a prototype breached. Each item: what to check,

how to check it fast, and the fix.

Legend: C critical / ship-blocker · H high · M medium · L low


1. Authentication & session

  • C Every mutating and data-returning API route checks an authenticated session.

Grep for route handlers (app/api/**/route.*, Express routes) and confirm each one

resolves a *server-verified* identity before touching data. Fix: one requireUser()

helper called at the top of every handler; deny by default.

  • C Session tokens are signed/encrypted and verified server-side. Reject any scheme

that trusts a raw userId/role from a cookie, header, or request body. Test: tamper

the cookie to another user's id and replay. Fix: signed JWT (jose) or encrypted session

(iron-session), verified on every request; never read identity from client input.

  • H Passwords hashed with a slow algorithm. No plaintext, MD5, SHA-1, or unsalted

hashes. Fix: bcrypt/argon2 (cost ≥ 12). Check the users table and the login/signup code.

  • H Privilege/role comes from the server record, not the client. role=buyer in a

cookie or body must never grant buyer access. Fix: load role from the DB row for the

verified user id.

  • M Session expiry + logout actually invalidate. Short-lived tokens, refresh on the

server, logout clears the cookie.

2. Tenant isolation & authorization (RLS)

  • C Row-Level Security enabled on every tenant-scoped table (Supabase: CVE-2025-48757).

`SELECT relname, relrowsecurity FROM pg_class JOIN pg_namespace n ON n.oid=relnamespace

WHERE n.nspname='public' AND relkind='r'; — any relrowsecurity = false` on a table with

tenant data is a finding. Fix: ALTER TABLE t ENABLE ROW LEVEL SECURITY; + explicit

policies. Enabling RLS with no policy denies all (good default); forgetting to enable

it exposes everything.

  • C No IDOR: object access is scoped to the caller's tenant. For every /:id route,

request another tenant's id while authenticated as tenant A. Fix: either DB-enforced

(RLS + per-request SET LOCAL of the tenant claim) or an explicit WHERE tenant_id = $me

on every query. Prefer DB-enforced — app-layer checks get missed on the next new route.

  • H The app does not connect as a superuser / service-role for user-facing queries.

A service-role connection bypasses RLS entirely. Fix: a limited DB role for request-path

queries; reserve the service role for migrations/admin jobs only.

  • M List endpoints filter by the server-known tenant, never by a client-supplied id.

3. Secrets

  • C No secret shipped to the browser. Build the client bundle and grep it for key

material: grep -rE 'sk-|service_role|AKIA|-----BEGIN|password|secret' .next/static dist.

In Next.js, only NEXT_PUBLIC_* reaches the client — a secret must never carry that prefix.

Fix: move it server-side; rotate any key that was ever public.

  • C No secrets committed. git log -p | grep -iE 'api[_-]?key|secret|password'; check

.env is git-ignored and an .env.example with placeholders is committed instead.

  • H Keys are rotatable and scoped (separate keys per environment; least privilege).

4. Input validation

  • C No SQL injection. All queries parameterized; grep for string-built SQL

(template literals / + with user input into query(...)). Fix: parameterized queries

($1), or a query builder. Never interpolate req values into SQL.

  • H Every input validated against a schema at the trust boundary. Fix: zod (or

equivalent) parse at the top of each route; reject on failure with a 400. Validate type,

length, format, and enum membership — not just presence.

  • H File uploads validated: allow-list content type, cap size, generate a random stored

name (never trust the client filename → path traversal), store outside the web root or in

object storage, and gate downloads by authorization.

  • M Output encoding / no reflected XSS; avoid dangerouslySetInnerHTML with user data.

5. CORS

  • H CORS is not * with credentials. Access-Control-Allow-Origin: * combined with

cookies is both invalid and a leak vector; a reflected-origin + Allow-Credentials: true

is worse. Fix: an explicit allow-list of known origins, or same-origin only (no CORS

headers at all for a same-origin app).

6. Rate limiting & abuse

  • H Auth and expensive endpoints are rate-limited. Test: 20 rapid login attempts —

expect 429s / backoff. Fix: a limiter (Upstash/Redis in prod; in-memory token bucket for

single-instance) keyed by IP + account; add lockout/backoff on repeated login failure.

  • M Pagination / payload caps so a single request can't pull the whole table.

7. Security headers & CSP

  • M Core headers set on every response: Content-Security-Policy,

X-Content-Type-Options: nosniff, X-Frame-Options: DENY (or CSP frame-ancestors),

Referrer-Policy, Strict-Transport-Security (prod/HTTPS). Fix: set in middleware or

next.config headers. Check with curl -sI.

8. Error handling

  • H No stack traces / DB errors returned to clients. Trigger a failure and inspect the

response body. Fix: catch at the route boundary, log the detail server-side with a

correlation id, return a generic message + id to the client. Set a framework-level error

boundary / 500 page.

9. Logging & monitoring

  • M Server-side structured logs for auth events, authorization denials, and errors —

without logging secrets, passwords, or full PII. A correlation id ties a client error to a

log line. Wire an error reporter (Sentry or equivalent) for prod.

10. Backups & data safety

  • M Automated DB backups + a tested restore. Know the RPO/RTO. For Supabase/managed PG,

confirm point-in-time recovery is on. Destructive migrations reviewed; no DROP without a backup.

11. Deploy & environment

  • H Production config exists and is separate from dev. Not localhost-only: binds

0.0.0.0, real NODE_ENV=production build, env vars validated at boot (fail fast on a

missing secret), HTTPS/TLS terminated, and a /health (or /api/health) endpoint for the

platform's checks. Next.js: output: 'standalone' or a static export as appropriate.

  • M No test/seed/admin backdoor data in prod; default/demo passwords removed.

12. Dependencies

  • M npm audit (prod deps) clean of high/critical, lockfile committed, and

unmaintained/abandoned packages replaced. Re-run in CI.


How I use this

I walk the list top-to-bottom against the running app and the repo, record each item as

pass / finding, and turn every finding into: id, severity (CVSS-style rationale),

evidence (file:line + a request/response), a runnable exploit that proves it, and the fix.

The critical tier (auth, RLS/tenant isolation, secrets, SQLi) is what actually gets

prototypes breached — I clear those before anything cosmetic.

_Maintained by Garrett Smith. Built for the Tallgrass Precision Components (fictional) rescue demo._

Real renders