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.
| Area | Issues | Clear | Other |
|---|---|---|---|
| Secrets and keys | 3 | 1 | 1 |
| Who can read and change data | 6 | 0 | 1 |
| Sign-in and sessions | 1 | 1 | 3 |
| Untrusted input | 4 | 3 | 1 |
| Payments and entitlements | 4 | 0 | 1 |
| Data loss and privacy | 2 | 3 | 1 |
| Runaway cost and abuse | 4 | 1 | 1 |
| Reliability and operations | 4 | 2 | 1 |
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.