What happened?
The profiles_update_self policy (supabase/migrations/0001_initial_schema.sql, line 277) is:
create policy profiles_update_self on profiles for update
using (auth.uid() = id);
This only restricts which row you can update, not which columns. Combined with the table grant authenticated gets in migration 0021 (grant select, insert, update, delete on all tables in schema public to authenticated), any signed in user can call the Supabase client directly from the browser, bypassing the app's own server actions in profile.ts entirely, and run something like:
update profiles set xp = 999999, level = 5, role = 'maintainer' where id = auth.uid()
There is no trigger or check constraint stopping writes to xp, level, role, or audit_completed.
Steps to Reproduce
- Sign in as any user
- Using the browser's own Supabase client and the user's session, call
.from('profiles').update({ xp: 999999, level: 5 }).eq('id', <own id>)
- The update succeeds with no server side validation at all
Expected Behavior
Sensitive columns like xp, level, and role should only be writable through server side logic (XP events, audits, admin actions), not via direct client updates, regardless of which row they target.
Where does this occur?
API / Database (RLS)
Additional Context
The simplest fix is probably a before update trigger on profiles that resets xp, level, role, and audit_completed back to their old values unless the request comes from the service role, since RLS alone can't cleanly express "this column only, only under these conditions" once a role already has broad table access.
What happened?
The
profiles_update_selfpolicy (supabase/migrations/0001_initial_schema.sql, line 277) is:This only restricts which row you can update, not which columns. Combined with the table grant
authenticatedgets in migration 0021 (grant select, insert, update, delete on all tables in schema public to authenticated), any signed in user can call the Supabase client directly from the browser, bypassing the app's own server actions inprofile.tsentirely, and run something like:There is no trigger or check constraint stopping writes to
xp,level,role, oraudit_completed.Steps to Reproduce
.from('profiles').update({ xp: 999999, level: 5 }).eq('id', <own id>)Expected Behavior
Sensitive columns like
xp,level, androleshould only be writable through server side logic (XP events, audits, admin actions), not via direct client updates, regardless of which row they target.Where does this occur?
API / Database (RLS)
Additional Context
The simplest fix is probably a
before updatetrigger onprofilesthat resetsxp,level,role, andaudit_completedback to their old values unless the request comes from the service role, since RLS alone can't cleanly express "this column only, only under these conditions" once a role already has broad table access.