Any signed-in user can read and change every organization's invoices and customers
- Where
Supabase tables
public.invoicesandpublic.customers. Row-level security is on. The policies do not look at the organization:create policy "Authenticated users can read invoices" on public.invoices for select to authenticated using (true); create policy "Authenticated users can update invoices" on public.invoices for update to authenticated using (true);customershas the same pair.- What someone can do
Sign up for a free account, then ask the database directly for all rows. They get every organization's invoices and customer contact details. They can also change amounts or mark invoices as paid. The app's own screens filter by organization, so you never see this by clicking around.
- Reproduce on staging
- Sign in as user B (Org B). Copy the access token from the session.
- Run:
curl "$SUPABASE_URL/rest/v1/invoices?select=id,org_id,total" \ -H "apikey: $ANON_KEY" \ -H "Authorization: Bearer $USER_B_TOKEN" - The response includes Org A's invoices.
- Send a
PATCHto/rest/v1/invoices?id=eq.1042(an Org A invoice) with the body{"status":"paid"}. It returns 204 and the row changes.
- Recommended fix
Replace the policies so each one checks membership. Do the same for
customers, and for insert and delete.drop policy "Authenticated users can read invoices" on public.invoices; drop policy "Authenticated users can update invoices" on public.invoices; create policy "Members read their org's invoices" on public.invoices for select to authenticated using ( org_id in ( select org_id from public.org_members where user_id = (select auth.uid()) ) ); create policy "Members update their org's invoices" on public.invoices for update to authenticated using ( org_id in ( select org_id from public.org_members where user_id = (select auth.uid()) ) ) with check ( org_id in ( select org_id from public.org_members where user_id = (select auth.uid()) ) );The
with checkpart stops a user moving a row into another organization. This relies onorg_membersletting a user read their own membership rows. It does. I checked that policy and it is correct.- Depends on data state
Yes. It only shows when at least two organizations have data. With one test account everything looks fine, which is why this is easy to miss.