Demo, all content is generated
Question

Users can give themselves unlimited credits by editing their own profile row

Solved · 3705 views · asked by mina_j · edited

My app sells credits for AI image generations. Credits live in profiles.credits. A user emailed me (nicely) that he could set his credits to 99999 from the browser console with one line:

await supabase.from('profiles').update({ credits: 99999 }).eq('id', myId)

The policy Lovable made is:

create policy "Users can update own profile" on profiles
for update using (auth.uid() = id);

They need to be able to change their name and avatar. How do I allow that but not credits? Also there's an is_admin column in the same table which I now realise is the same problem.

What I’ve tried

Asked Lovable to protect the credits column, it added a check in the React code before saving. The console trick still works of course.

Comment
Nice of that user to tell you instead of using it. Worth a free month. bakery_bo · edited
Checking my own project now. Same policy, same is_admin column. rosie_q · edited
Bolt generated the exact same policy for me. Fixed with the column grants, took five minutes. coop_builds · edited

3 answers

Marked as helpful by the asker
amir_h · edited

RLS works on rows, not columns. "You may update this row" means every column in it. Two ways to fix it, pick one:

1. Column privileges (smallest change):

revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;

Now an update that touches credits or is_admin fails with permission denied for table profiles, even though the RLS policy passes. Keep your policy, and add with check ((select auth.uid()) = id) so nobody can move a row to another id.

2. Move the sensitive columns out (cleaner long term): profiles for what users edit, and a separate accounts or user_roles table with credits and admin flags that has a select policy only. Credits change only through a security definer function or your payment webhook with the service role.

For credits I'd do option 2: you'll want a ledger of purchases and usage anyway, not just a number anyone can overwrite.

Comment
Did option 1 tonight so the hole is closed, and I'm moving credits into a ledger table this weekend. mina_j · edited
Good order. Test it the same way the user did: console, update credits, expect an error. amir_h · edited
lena_ops · edited

After closing it: check who has is_admin = true and whether any credit balances look odd compared to your Stripe payments. If that user found it, assume someone else did too.

Comment
Two accounts had odd balances, both test accounts of mine luckily. Stripe totals match. mina_j · edited
abby_ops · edited

Easy test to keep around: log in as a normal user in a Playwright test and try to update credits and is_admin. Both should fail. Stops the next AI edit from reopening it.

Comment
Added it, thanks mina_j · edited