Eight findings, ranked by risk, each tied to the real incident it copies.
#1HighLogins
A signed-out stranger can grade exams and delete students.
supabase/migrations/20260315000000_admin_tools.sql, lines 16 and 28
It’s the same mistake as an exam app featured by Lovable in February 2026, whose admin check was written backwards. It blocked signed-in users and let strangers in, and the researcher counted about 18,700 user records open.
What happens
Both admin functions turn away signed-in users and let everyone else through, and anyone can call them with the public key.
What it costs
You’d have grades nobody can trust, and students deleted with all their work.
15 -- Block regular users; only the admin dashboard may call this
16 if auth.role() = 'authenticated' then
Evidence
- On my copy, a signed-out stranger raised a 62 to 100 and deleted a student, while a signed-in student got “Access denied”.
The fix
Let only the server call these functions, and revoke everyone else’s right to call them.
if auth.role() is distinct from 'service_role' then
raise exception 'Access denied';
end if;
Checked on a copy: the stranger and the student both get “permission denied”, and the dashboard still grades, 71 to 88.
#2HighData access
Private messages and API tokens are open to anyone.
supabase/migrations/20260301000000_init.sql, lines 64 to 79
It’s the same mistake as Moltbook, which went viral in January 2026 with its database open to anyone. Wiz counted 1.5 million API keys, 35,000 email addresses and thousands of private messages anyone could read, all because one setting was off.
What happens
These two tables never got row-level security, so the public key in every visitor’s browser can read and change them, whatever the pages ask for.
What it costs
It exposes every message between students and the instructor, and the token behind #6.
64 create table public.messages (
…
73 create table public.api_tokens (
Evidence
- The scan listed both tables, and on my copy a stranger read 2 of 2 messages and 1 of 1 tokens, then rewrote both messages.
The fix
Turn row-level security on for both, with rules that limit messages to the two people in them and tokens to their owner.
alter table public.messages enable row level security;
create policy "Participants read their messages" on public.messages
for select to authenticated using (auth.uid() in (sender_id, recipient_id));
Checked on a copy, with tokens done the same way: the stranger gets nothing, and student 1 still reads their own 2 messages.
#3HighData access
Anyone can read every student’s name, email and phone number.
supabase/migrations/20260301000000_init.sql, line 14
It’s the same mistake as in CVE-2025-48757, where researchers checked 1,645 apps on an AI app builder’s showcase in 2025 and found 170 in which strangers could read users’ names, phone numbers and payment details. The builder disputes the CVE.
What happens
Row-level security is on, which passes a quick look, but the rule under it lets everyone read every row, signed in or not.
What it costs
Your whole student list, with contact details, is open to anyone who asks.
14 create policy "Enable read access for all users"
on public.profiles for select using (true);
Evidence
- The scan flagged line 14, and a stranger read all 4 profiles on my copy.
The fix
Drop that rule, and let each person read their own profile and the instructor read everyone’s.
create policy "Read your own profile" on public.profiles
for select to authenticated using (auth.uid() = id or public.is_admin());
Checked on a copy: the stranger reads none, student 1 reads only their own, and the instructor reads all 4.
#4HighPayments
Students can give themselves Pro, or make themselves the instructor.
src/pages/UpgradeSuccess.tsx, line 8, and init.sql, line 15
It’s the same harm as in CVE-2025-48757, where researchers marked an order “paid” on one app without paying. In March 2025, people were also bypassing the subscription on a solo founder’s app.
What happens
The success page turns on Pro for whoever opens it, and the profile rule lets each user change any column of their own row, including the role the dashboard checks.
What it costs
Students get the AI tutor free on your OpenAI bill, and any of them can take the instructor’s powers.
UpgradeSuccess.tsx
8 if (user) supabase.from("profiles").update({ subscription_status: "active" })…
init.sql
15 create policy "Users can update their own profile" … using (auth.uid() = id);
Evidence
- On my copy, a signed-in student made themselves Pro and then the instructor, after which they could see every student’s attempts.
The fix
Take the update out of the success page, so only the verified webhook sets the plan, and let people change only their name and phone.
revoke update on public.profiles from anon, authenticated;
grant update (full_name, phone) on public.profiles to authenticated;
Checked on a copy: both changes now get “permission denied”, and saving a name and phone still works.
#5HighAPI keys
The OpenAI key ships to every visitor’s browser.
.env, line 4, and src/lib/ai.ts, lines 5 and 6
It’s the same symptom as a solo founder’s app, whose API keys were maxed out in March 2025, two days after he posted that AI wrote all of it. He never said where the keys were.
What happens
Anything named VITE_ is built into the JavaScript every visitor downloads, so anyone can copy this key and spend on your OpenAI account.
What it costs
A stranger can run up your OpenAI bill at whatever pace they like.
.env
4 VITE_OPENAI_API_KEY=sk-proj-****
src/lib/ai.ts
5 apiKey: import.meta.env.VITE_OPENAI_API_KEY,
6 dangerouslyAllowBrowser: true,
Evidence
- The scan found the key in a tracked
.env, under a browser name, and in commit 5369d97, and an offline build put it in the JavaScript the app ships.
The fix
Rotate the key, since the old one stays in the history, then move the tutor’s call to a Supabase Edge Function that keeps the key secret and checks for Pro.
This fix wasn’t run on the copy, because rotating the key happens in your OpenAI account.
#6HighWhen something fails
The grade export hands over every grade when its token check fails.
supabase/functions/export-grades/handler.ts, lines 4 to 8
It’s the same kind of bug as at METR, an AI research nonprofit, where a vibe-coded dashboard quietly switched its login off in March 2026. A stranger then used its AI key for three weeks, about $600,000 worth of donated credits.
What happens
A wrong token, or none, makes the lookup fail, and the catch on line 8 just logs it and carries on, so the export hands over every grade.
What it costs
Every student’s grades go to anyone who finds its address.
6 if (error) throw error;
7 if (!data) return new Response("Bad token", { status: 401 });
8 } catch (e) { console.error("token check failed,
continuing so the sync doesn't break", e); }
Evidence
- The scan listed line 8 as a catch that only logs, and a test I wrote on a copy passed only 1 of 3 cases, since a wrong token and no token both got every grade.
The fix
Make it fail closed, so no token means no grades, and fix #2 as well, because anyone can read the token today.
if (error || !data) return new Response("Bad token", { status: 401 });
Checked on a copy: with the fix, all 3 cases pass.
#7MediumData access
Anyone can add questions to the bank students study from.
supabase/migrations/20260301000000_init.sql, line 47
It’s the same symptom the founder in #5 reported, with junk filling his database.
What happens
The public landing page’s suggestion box writes directly into the question bank, and its rule lets anyone add rows, with no review.
What it costs
Practice exams can fill with junk that someone has to clear out by hand.
47 create policy "Anyone can suggest a question"
on public.questions for insert with check (true);
Evidence
- The scan printed line 47, and on my copy a stranger added 1,000 questions to an exam in one go.
The fix
Drop that rule, let only the instructor add questions, and send suggestions to a table of their own for review.
Checked on a copy: row-level security now refuses the stranger’s rows.
#8MediumWhen something fails
A command the coding agent is told to run would wipe the live database.
package.json, line 9, and AGENTS.md, line 5
These are the same conditions as in two real deletions, by Replit’s AI agent in July 2025 and by an AI coding agent at PocketOS in April 2026. Each wiped a live database, and both got their data back.
What happens
The agent’s notes say to run the reset when the schema gets messy, and it wipes the linked Supabase project, which here is the live one.
What it costs
You’d lose every account, attempt and message for as long as a restore takes, if one works.
package.json
9 "db:reset": "supabase db reset --linked",
AGENTS.md
5 - If the schema gets messy, run `npm run db:reset` to start fresh.
Evidence
- The scan flagged the reset on line 9 and the note that tells the agent to run it. I didn’t run it, on purpose.
The fix
Build against a separate Supabase project, take the reset out of the notes, and keep backups outside the live project. No check can stop an agent from acting, so this one reduces the damage.
On the call, I’d ask where your backups live and when you last restored one.