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.
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 CHECKclause like:OLD/NEWpseudo-relations only exist inside trigger functions in Postgres - they are not available inside a bareCREATE POLICYexpression. Running either statement throwsmissing FROM-clause entry for table "old"and theCREATE POLICYfails. 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 ofOLD. 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/badgesvia a direct tableUPDATEcannot 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-referencingWITH CHECKclause with either aBEFORE UPDATEtrigger (which does haveOLD/NEW) or a self-join subquery against the current row, as already done forprofiles.