Repository navigation
Conversation
…r or role scope Keeps the SELECT exemption from supabase#141, except for permissive SELECT policies for anon, authenticated or public whose name claims per-user, admin or service role scope. Adds a detail sentence when a scope-claiming name sits on a policy where every expression is always true. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
0024_rls_policy_always_trueskipsSELECT ... USING (true)because public read access is often intentional (per the review on #141). That holds for a policy like"Public profiles are viewable by everyone.". It doesn't hold for this one:The name says per-user, but the policy returns every row to every signed in user. Today 0024 reports nothing for it. The name is the only thing in the catalog that says the author meant something narrower, and it's also what people read when they review policies in the dashboard.
This PR keeps the SELECT exemption but makes one narrow exception: a permissive SELECT policy for
anon,authenticatedorpublic, with an always-true USING, whose name claims scope. The name is lowercased and every non-alphanumeric run becomes a space, then it counts as claiming scope if it contains one of these as whole words:ownortheir("Users can view their own posts", "select_own_posts")only owner,only owners,only the owner,only the ownersfor owner,for owners,for the owner,for the ownersowner can,owners can,owner only,owners onlyadminoradminsfollowed bycan,only,read,reads,view,views,see,sees,manage,manages,select,insert,updateordelete("Admin reads all activity", "admin_select_responses")only admin,only adminsfor adminorfor adminsat the very end of the name ("Allow authenticated read for admin", but not "... for admin reports")service roleA bare
owneroradminis not enough, because it often names the table's contents ("Allow reading admin users", "Public can view vehicle owners"). A name that containspublic,everyoneoranyonenever counts.It also appends one sentence to
detailfor any flagged policy with such a name where every expression the policy has is always true (a missing USING only counts for INSERT):Policies where one clause really checks the owner (e.g.
using (user_id = auth.uid()) with check (true)) are still flagged as before, but without that sentence, since it would be wrong for them.The always-true checks on USING and WITH CHECK are now computed once in the
policiesCTE (qual_always_true,with_check_always_true) and reused. All existing test output is unchanged.What does not change
using (true)policies whose name doesn't claim scope are still not flagged.The lint's
descriptiontext changed in one place. "SELECT policies withUSING (true)are intentionally excluded as this pattern is often used deliberately for public read access." now reads "... are excluded as this pattern is often used deliberately for public read access, unless the policy name says it is limited to the row owner or a role."Known limits
The matcher is English-name heuristics: camelCase names ("usersViewOwnPosts") and non-English names aren't matched, and a name that says
public,everyoneoranyoneis skipped even if it also saysown(so "Anyone can read own payments by phone" is not flagged, and neither is a name withpublicinside a table name, like "Users can view own public_profiles"). This would be the first splinter lint that reads policy names, which is a design choice for maintainers.Where this comes from
I used Claude Code to read the migrations of 512 public AI-generated Supabase apps. Of the 1,521 SELECT
using (true)policy names visible in the scan output (distinct repo and policy name pairs, 1,351 distinct names; the scanner truncates long lists), 36 in 26 repos have a name containing one ofown,owner(s),their,self,admin(s)orservice role. By that reading, 27 of those 36 claim per-user, admin or service role scope, for example "Users can view their own credentials", and the other 9 describe the table's contents or declare public or anyone access. The matcher flags 26 policies in 17 repos, all among the 27. The one it misses is the deliberate "Anyone can read own payments by phone". It flags nothing else among the 1,521.Tests
New cases in
test/sql/0024_rls_policy_always_true.sql:"Users can view their own posts","select_own_posts","Admins can view all posts","Service role can read posts","Owners can view posts","Only owners can read","Readable by only the owner","Read access for the owner","Owner only read","Visible to only admins","All users can view their own posts","Users can view own posts"withusing (1=1), one name per admin verb ("Admin only","Admin read posts","Admin view","Admin views posts","Admins see posts","Admin sees posts","Admins manage posts","Admin manages posts"), plus five real names:"Admin reads all activity","Users can view their restaurant's marketing data","Users can view their own import jobs","Allow authenticated read for admin","admin_select_responses""Public profiles are viewable by everyone.","Posts shown on the homepage","Public read only","Authenticated can read for admin reports","Everyone can view their posts","Users can view their own posts"withusing (user_id = auth.uid()), plus five real names:"Allow reading admin users","Allow public read access for admins","Allow public read access for admin activity logs","Public can view vehicle owners","Anyone can read own payments by phone"using (true)and INSERTwith check (true)with ownership names, and"admin_insert_posts","admin_update_posts","admin_delete_posts""update_any_post"(neutral name), UPDATE where one of USING / WITH CHECK checks the owner, and DELETE with no USINGEach phrase in the matcher and each of
public,everyoneandanyonehas a case that fails if it is removed. No existing expected output changed. The full suite passes locally onsupabase/postgres:15.14.1.169and17.6.1.169(29 of 29).bin/check_lints.pypasses andsplinter.sqlis regenerated withbin/compile.py.Docs
docs/0024_permissive_rls_policy.mdgets a short section on policy names that claim scope, listing the phrases above and the names they miss, and a false positive note (rename the policy if the table really is public). I also removed two lines from the detected patterns that the lint has never matched: "Missing USING clause on permissive SELECT policies" andUSING ('a'='a').Noticed, not changed
toclause,rolesis{-}(0::oid::regrole::text), so the detail ends "bypasses row-level security for -."with check (true),authenticatedsees no rows through it, so the finding is right (inserts are open) butpermissive_usingand the "USING clause" wording are not. A DELETE policy with no USING, and an UPDATE policy with neither USING nor WITH CHECK, affect 0 rows (checked on 15 and 17, with ausing (true)SELECT policy on the same table), so those findings are false positives. An UPDATE policy whose only clause iswith check (true)also changes no rows by itself, but it is not harmless: Postgres ORs its WITH CHECK with the other permissive UPDATE policies on the table, which removes their write check, so flagging it is right. The new sentence is left off all policies with no USING, because "does not check who the caller is" would describe them inaccurately.🤖 Generated with Claude Code