This is a real audit performed on a live production app (a university dorm-management system), anonymized. Every finding carries its 30-second verify query — that's the standard format you receive. Owners were notified the same day.
| Severity | Finding | Table / file |
|---|---|---|
| HIGH | Staff table fully readable by anonymous users | public.staff |
| HIGH | Cleaning/duty schedule readable by anyone | cleaning_schedule |
| MEDIUM | Any authenticated user can rewrite the duty schedule | cleaning_schedule |
staff table is readable by anyone with the anon keyIn supabase/migrations/…_initial_schema.sql:
CREATE POLICY "Anon email existence check" ON public.staff FOR SELECT TO anon USING (true);
The comment says it was added for the login page's email check. But USING (true) on FOR SELECT TO anon exposes every column of every staff row: email, phone_number, full_name, staff_id, role, assigned_floor, assigned_gender.
Verify (30 sec) — Supabase SQL editor:
select policyname, roles, cmd, qual from pg_policies where tablename = 'staff';
You'll see roles = {anon} and qual = true.
Fix: don't open the table for a lookup — wrap the lookup:
create or replace function public.staff_email_exists(email text)
returns boolean language sql security definer
set search_path = public as $$
select exists(select 1 from public.staff s
where s.email = staff_email_exists.email);
$$;
revoke all on public.staff from anon;
drop policy "Anon email existence check" on public.staff;
CREATE POLICY "Allow authenticated insert/update" ON cleaning_schedule
FOR ALL TO authenticated USING (true) WITH CHECK (true);
Any student account can insert, rewrite, or erase duty-roster rows. Integrity loss among authenticated users.
Fix: scope writes to the staffing role and a row predicate: TO authenticated USING (exists (select 1 from staff s where s.id = auth.uid() and s.role in ('admin','tarbiyachi'))).
Grants & role exposure review (the gates around RLS), write-path simulation on a fixture clone of your schema, and remediation SQL per finding with severity-ranked fix-hours.
Run the free check on your repo →