Skip to content

fix: drop TRUNCATE and unused DELETE grants, make public_* views read-only - #123

Open
thomgabriel wants to merge 1 commit into
mainfrom
fix/least-privilege-grants
Open

thomgabriel wants to merge 1 commit into
mainfrom
fix/least-privilege-grants

Conversation

@thomgabriel

Copy link
Copy Markdown
Member

Trims the privileges anon and authenticated got from Supabase's default ACL (arwdDxtm for tables created by postgres). 00025 only took insert/update/delete back from anon.

  • TRUNCATE, REFERENCES, TRIGGER and MAINTAIN are revoked from anon and authenticated on every public table and view. TRUNCATE ignores RLS, and none of the four is reachable or needed through PostgREST.
  • DELETE is revoked everywhere except team_members, which has the only DELETE policy (team_members_leader_delete). Every app delete goes through the service role or a SECURITY DEFINER RPC (remove_team_member, leave_team, delete_team, decline_pending_membership), and no other policy allows a DELETE, so these grants only ever matched 0 rows. A user-scoped DELETE on those tables now returns 403 instead of silently doing nothing.
  • The public_* views keep only SELECT. This fixes a live hole: public_profiles is an auto-updatable view owned by postgres (BYPASSRLS), and authenticated held INSERT/UPDATE/DELETE on it. So PATCH or DELETE /rest/v1/public_profiles?id=eq.<uuid> with any user's token rewrote or deleted another participant's users row past RLS. Edge logs (24h retention) show only GETs on the view, and no auth.users row is missing its public.users mirror, so nothing was deleted this way.
  • Default privileges for new postgres-owned tables drop the same set, so they now give anon r and authenticated arw. CLAUDE.md notes that a DELETE policy needs its grant, and that a new view needs revoke all + grant select.

Numbered 00069 because 00068 (perk_applications) is taken by the in-flight Helius perk branch. Already applied to dqxeukfkjnoqljovkage as least_privilege_table_grants.

Verified by impersonating real participants in rolled-back DO blocks, run against identical fixtures before and after the migration. A Colosseum team was reopened inside the transaction so team writes could match:

before after
register (insert + re-submit upsert) 1, 1 1, 1
startup form (profile update, new upsert, edit upsert, existing founder upsert) 1, 1, 1, 1 1, 1, 1, 1
team edit (submission editor payload, rename) 1, 1 1, 1
leader direct delete / remove_team_member / leave_team 1 / ok / ok 1 / ok / ok
other user's profile via public_profiles: update / insert / delete 1 / past privilege check (23502) / 1 denied / denied / denied
delete from startups / truncate startups as authenticated 0 rows / granted denied / denied
anon reads public_submissions 42 rows 42 rows

The security_definer_view advisor still flags the four views. That is by design: they expose whitelisted columns to anon, and they are now read-only.

…-only

public_profiles is an auto-updatable view owned by postgres, so the
insert/update/delete the default ACL gave authenticated let any signed-in
user rewrite or delete another participant's users row past RLS.
@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hackathon-platform Ready Ready Preview Oct 15, 2026 2:56pm UTC

Request Review

@claude

claude Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Claude finished @thomgabriel's task in 25s —— View job


Safe to merge. No defects found.

What I checked:

  • Delete call sites: the four app .delete() callers (revokeRole, sponsors, judge assignments, deleteRating) all use createServiceRoleClient() behind a gate, so revoking DELETE from authenticated doesn't affect them. The sponsors delete also filters on gate.hackathon.id. No other .delete() call sites in src.
  • DELETE policies: team_members_leader_delete is the only table DELETE policy in the migrations, and the grant is kept for it. The other for delete policies are on storage.objects, which this migration doesn't touch.
  • Views: the four public_* views (public_profiles, public_submissions, public_team_members, public_schedule) are the only views in the migrations, and all four are covered by the revoke and grant.
  • Idempotency: the migration is re-runnable.

Not verified: I didn't run the migration or the before/after impersonation tests. The numbering gap at 00068 is intentional per the PR description.
· branch fix/least-privilege-grants

This branch was successfully deployed

1 active deployment
Preview — 1f5bbb00 Deployed Oct 15, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant