Skip to content

[Security] Leaderboard score-immutability migrations use invalid SQL - protection never applies #1952

Description

@payalrvs3

Where: supabase/migrations/20260730000001_secure_leaderboard_updates.sql (lines 13-23), supabase/migrations/20260803000002_enforce_leaderboard_score_immutability.sql (lines 24-33)

Bug: Both migrations write an RLS WITH CHECK clause like:

WITH CHECK (
  user_id = auth.uid()
  AND xp = OLD.xp
  AND streak = OLD.streak
  ...
)

OLD/NEW pseudo-relations only exist inside trigger functions in Postgres - they are not available inside a bare CREATE POLICY expression. Running either statement throws missing FROM-clause entry for table "old" and the CREATE POLICY fails. This is a known, documented Postgres limitation (there's even a postgresql.org mailing-list thread on exactly this, recommending a trigger instead).

Tellingly, an earlier migration in this same repo (20260617000000_consolidate_rls_policies.sql, the profiles UPDATE policy) solved the identical "compare against the current row" problem correctly, using a self-referential subquery instead of OLD. So this is a regression in approach, not a one-off typo.

Impact: The policies meant to stop users from tampering with their own xp/streak/badges via a direct table UPDATE cannot be created as written, so that protection doesn't actually exist. Distinct from #1925, which is about direct INSERT/DELETE forgery - this is specifically about the UPDATE-path lockdown never taking effect.

Suggested fix: Replace the OLD-referencing WITH CHECK clause with either a BEFORE UPDATE trigger (which does have OLD/NEW) or a self-join subquery against the current row, as already done for profiles.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions