Sample report

What an audit report looks like.

Ranked by severity, in plain English, with where each problem is, what a stranger could do with it and exactly how to fix it.

This is a real report from our audit pipeline, run on a small test app we built with flaws planted on purpose. Every key in it is fake and it describes no customer. It shows the format you receive.

Summary

The audit found 26 issues: 11 critical, 2 high, 8 medium and 5 low. The six most serious are shown below; a customer's report lists every one.

Findings

F-001: Live secret keys sit in a .env file that is not excluded from the repository

critical.env:4 · checklist SEC-01

What we found

VITE_SUPABASE_SERVICE_ROLE_KEY=eyJhbGci…
STRIPE_SECRET_KEY=sk_live_…
OPENAI_API_KEY=sk-proj-…

Why it matters

Anyone with access to the code, or a copy of it, gets the live Stripe secret key, the OpenAI key and the database master key. They could issue refunds or read payment data, run up OpenAI charges on your account, and read or wipe the database.

Fix

Rotate the Stripe live secret key, the OpenAI key, the Supabase service-role key and the database password now. Add .env, .env.* and !.env.example to .gitignore. Remove .env from the repository (git rm --cached .env) and purge it from history with git filter-repo or BFG. Store production secrets only in the hosting and Supabase secrets settings (supabase secrets set …). Commit a .env.example with placeholder values instead.

F-002: Invoice export endpoint returns any customer's invoices without login

criticalserver/export.js:8 · checklist AUTH-01, ACC-05

What we found

app.get("/api/export", async (req, res) => {
    "select id, amount_cents, card_last4 from invoices where user_id = '" + req.query.user + "'"

Why it matters

Anyone can call /api/export?user=<someone's id> and download that customer's invoices, including card last-4 digits, without signing in.

Fix

Add middleware that reads the Supabase access token from the Authorization header and verifies it with supabase.auth.getUser(token) (or verifies the JWT signature with the project's JWT secret). Take the user id from the verified token, never from req.query, and return 401 when there is no valid token. While editing this line, also switch to a parameterised query ($1); that string-concatenated SQL is a separate injection issue.

F-003: Invoice export builds its SQL query from the URL, allowing SQL injection

criticalserver/export.js:10 · checklist INP-01

What we found

"select id, amount_cents, card_last4 from invoices where user_id = '" + req.query.user + "'"

Why it matters

Anyone who can reach this endpoint can put SQL in the ?user= parameter, for example ' or '1'='1. They could download every customer's invoices and card digits, and possibly read or change other tables. The endpoint has no login check either.

Fix

Use a parameterised query: db.query('select id, amount_cents, card_last4 from invoices where user_id = $1', [userId]). Do not take userId from the query string. Take it from a verified Supabase JWT on the request, and reject requests that don't have one.

F-004: Database master key (service-role) is bundled into the browser code for the admin client

criticalsrc/lib/admin.ts:6 · checklist ACC-04, SEC-02

What we found

export const adminClient = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY,

Why it matters

The service-role key ships in the public JavaScript, and that key bypasses every security rule. Any visitor can copy it and read, change or delete every table. The admin redirect only hides the page and protects nothing.

Fix

Rotate the service-role key now and delete VITE_SUPABASE_SERVICE_ROLE_KEY from .env and all client code. Move the admin user listing into an edge function that checks the caller's JWT and confirms profiles.role = 'admin' on the server before using the service key. Fix ACC-03 first so role cannot be self-assigned.

F-005: Admin page checks the admin role only in the browser and loads every user's profile regardless

criticalsrc/pages/Admin.tsx:11 · checklist AUTH-01

What we found

adminClient.from("profiles").select("*").then(({ data }) => setUsers(data ?? []));
  if (profile?.role !== "admin") return <Navigate to="/" />;

Why it matters

The 'admin only' check runs in the visitor's browser after the full user list (emails, roles, plans) has already been fetched with an all-powerful key. Any visitor can see that data or skip the redirect.

Fix

Move the admin user listing into a server-side function (an edge function with verify_jwt = true). It should verify the caller with getUser(), confirm role = 'admin' from the database, and only then query with the service-role key held on the server. Remove src/lib/admin.ts and VITE_SUPABASE_SERVICE_ROLE_KEY from the frontend and rotate that key, which is also a secrets-section issue. Stop users from editing their own 'role' column through the profiles update policy.

F-006: AI generation function can be called by anyone without signing in

criticalsupabase/config.toml:7 · checklist AUTH-01, COST-01

What we found

[functions.generate]
verify_jwt = false
(supabase/functions/generate/index.ts:10) const { prompt } = await req.json();

Why it matters

Anyone who finds the function URL can send unlimited requests to GPT-4o on your OpenAI account. That can run up a large bill or use up your quota, and nobody has to sign in to do it.

Fix

Set verify_jwt = true for 'generate' in supabase/config.toml. Inside the function, create a Supabase client with the caller's Authorization header and call supabase.auth.getUser(); return 401 if no user comes back. Add a per-user rate limit or quota, and set a spending cap on the OpenAI account.

What was checked

All 49 checks, in 8 areas. The report lists the result of each one. "Other" means the check did not apply or could not be answered from the code.

AreaIssuesClearOther
Secrets and keys311
Who can read and change data601
Sign-in and sessions113
Untrusted input431
Payments and entitlements401
Data loss and privacy231
Runaway cost and abuse411
Reliability and operations421

What a customer's report adds

  • Every finding, not only the top six.
  • What we could not check from the code, such as backup settings, and what to look at.
  • A fixed quote for fixing every critical and high finding.
  • The name of the engineer who checked and signed it.