<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Socuv</title>
    <description>The latest articles on Forem by Socuv (@oscarv).</description>
    <link>https://forem.com/oscarv</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866535%2F8cc31c5e-d8e6-4602-823a-0c686125f8d6.png</url>
      <title>Forem: Socuv</title>
      <link>https://forem.com/oscarv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/oscarv"/>
    <language>en</language>
    <item>
      <title>Supabase RLS: The Hidden Danger (And How to Find It Before Hackers Do)</title>
      <dc:creator>Socuv</dc:creator>
      <pubDate>Tue, 07 Apr 2026 20:46:18 +0000</pubDate>
      <link>https://forem.com/oscarv/supabase-rls-the-hidden-danger-and-how-to-find-it-before-hackers-do-52l3</link>
      <guid>https://forem.com/oscarv/supabase-rls-the-hidden-danger-and-how-to-find-it-before-hackers-do-52l3</guid>
      <description>&lt;p&gt;You just launched your Supabase project. It works. Users are signing up. You're proud of it.&lt;/p&gt;

&lt;p&gt;Then you get a message: "Hey, I can see everyone's data."&lt;/p&gt;

&lt;p&gt;This happens more than you'd think. And the cause is almost always the same: &lt;strong&gt;Row Level Security was enabled, but the policies were wrong — or missing entirely.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let me show you exactly how this happens, how to check if your project is affected, and how to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is RLS and Why Does It Matter?
&lt;/h2&gt;

&lt;p&gt;Supabase uses PostgreSQL's Row Level Security to control which rows a user can read, insert, update, or delete. When you enable it, &lt;strong&gt;access is denied by default&lt;/strong&gt; — until you create policies that explicitly allow access.&lt;/p&gt;

&lt;p&gt;The problem: enabling RLS and creating correct policies are two separate steps. You can do one without the other.&lt;/p&gt;

&lt;p&gt;And Supabase's dashboard will show your table as "RLS enabled" — technically true, but completely misleading if you have no policies.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Common Mistake: Enabling RLS Without Policies
&lt;/h2&gt;

&lt;p&gt;Here's what a dangerous table looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- RLS is enabled&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;user_profiles&lt;/span&gt; &lt;span class="n"&gt;ENABLE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- But no policies exist&lt;/span&gt;
&lt;span class="c1"&gt;-- Result: NO rows are returned for authenticated users&lt;/span&gt;
&lt;span class="c1"&gt;-- BUT: anon users can still read everything if you have a permissive grant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or worse — the AI-generated version that "works" in testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The classic AI-generated mistake&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Enable read access for all users"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;user_profiles&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;-- This allows EVERYONE to read EVERYTHING&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've seen this pattern in dozens of projects. Cursor, Lovable, and Bolt generate it because it makes tests pass. It's not secure.&lt;/p&gt;




&lt;h2&gt;
  
  
  What an Attacker Sees
&lt;/h2&gt;

&lt;p&gt;Your frontend bundles your Supabase URL and &lt;code&gt;anon&lt;/code&gt; key. This is normal — it's designed this way. The &lt;code&gt;anon&lt;/code&gt; key is meant to be public.&lt;/p&gt;

&lt;p&gt;The problem is what that key can access.&lt;/p&gt;

&lt;p&gt;Here's how easy it is to check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Anyone can run this with your public anon key&lt;/span&gt;
curl &lt;span class="s1"&gt;'https://YOUR_PROJECT.supabase.co/rest/v1/user_profiles?select=*'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: YOUR_ANON_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_ANON_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your RLS policies are wrong, this returns &lt;strong&gt;all user data&lt;/strong&gt;. Email addresses, names, profile information — everything in that table.&lt;/p&gt;

&lt;p&gt;No authentication required. Just your public anon key, which is already in your JavaScript bundle.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Four RLS Failure Modes
&lt;/h2&gt;

&lt;p&gt;After analyzing many Supabase projects, these are the patterns I see most often:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Missing policies on new tables
&lt;/h3&gt;

&lt;p&gt;You add a new table in a hurry. You enable RLS (or forget to). No policy. The table is either locked (nothing works) or wide open (everything is readable).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;USING (true)&lt;/code&gt; policies
&lt;/h3&gt;

&lt;p&gt;The "it works now" shortcut. Every row is readable by every user. Often copy-pasted from documentation examples that were never meant for production.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Permissive storage buckets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This bucket is publicly readable&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'avatars'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you store anything sensitive in a "public" bucket — even with a non-obvious path — it's accessible to anyone with the URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Overpermissive service role usage
&lt;/h3&gt;

&lt;p&gt;Some developers use the &lt;code&gt;service_role&lt;/code&gt; key in their frontend for "simplicity." The service role &lt;strong&gt;bypasses RLS entirely&lt;/strong&gt;. It should never be in client-side code.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Check Your Project Right Now
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Check your RLS status
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Run this in Supabase SQL Editor&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rowsecurity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any table where &lt;code&gt;rowsecurity = false&lt;/code&gt; is unprotected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Check your policies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- See all policies on public tables&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;policyname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;permissive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;qual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;with_check&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tables with no policies at all (but RLS enabled)&lt;/li&gt;
&lt;li&gt;Policies with &lt;code&gt;qual = '(true)'&lt;/code&gt; — these allow everyone&lt;/li&gt;
&lt;li&gt;Policies that don't check &lt;code&gt;auth.uid()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Check your storage buckets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;public = true&lt;/code&gt;, anyone can list and access files in that bucket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Check your auth settings
&lt;/h3&gt;

&lt;p&gt;In your Supabase dashboard → Authentication → Settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is email confirmation required? (it should be for production)&lt;/li&gt;
&lt;li&gt;Is there a password minimum length? (8+ characters)&lt;/li&gt;
&lt;li&gt;Are there rate limits on sign-ups?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Correct Pattern
&lt;/h2&gt;

&lt;p&gt;Here's what secure RLS looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Users can only read their own profile&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Users can read own profile"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;user_profiles&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Users can only update their own profile&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Users can update own profile"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;user_profiles&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- No insert from client (handle in Edge Function)&lt;/span&gt;
&lt;span class="c1"&gt;-- No delete from client (handle in Edge Function)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TO authenticated&lt;/code&gt; — only logged-in users&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auth.uid() = user_id&lt;/code&gt; — only their own rows&lt;/li&gt;
&lt;li&gt;Separate policies for SELECT, INSERT, UPDATE, DELETE — don't use a blanket policy&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  For Vibe Coders Specifically
&lt;/h2&gt;

&lt;p&gt;If you built your project with Cursor, Lovable, Bolt, or v0 — check your RLS policies manually. These tools are excellent at building features quickly, but they optimize for "it works" over "it's secure."&lt;/p&gt;

&lt;p&gt;The generated code often includes &lt;code&gt;USING (true)&lt;/code&gt; policies because they're needed to make the demo work in development. Before you go live, replace them with proper user-scoped policies.&lt;/p&gt;

&lt;p&gt;This is not a criticism of AI coding tools — it's just how they work. Security is your responsibility, not the tool's.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automating This Check
&lt;/h2&gt;

&lt;p&gt;Manually auditing your Supabase project is doable but tedious. If you want to automate it — especially if you manage multiple projects — there are a few tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SupaSec&lt;/strong&gt; (open source): bundle scraping + basic RLS check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FounderScan&lt;/strong&gt; ($19 one-time): 12+ security checks including Supabase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AEGIS&lt;/strong&gt; (my project): continuous monitoring, active testing, plain-language report with fix commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important thing isn't which tool you use — it's that you check at all. Most projects I've looked at have at least one issue. Many have several.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Enabling RLS ≠ being secure. You need policies too.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USING (true)&lt;/code&gt; is almost never what you want in production.&lt;/li&gt;
&lt;li&gt;Public storage buckets are public. Don't store sensitive data in them.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;service_role&lt;/code&gt; key bypasses RLS. It belongs on the server, not in the browser.&lt;/li&gt;
&lt;li&gt;Run the SQL queries above to check your project right now. It takes 5 minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you find something, fix it before someone else does.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building AEGIS — automated Supabase security scanning with continuous monitoring. If you want to be notified when it launches, &lt;a href="https://aegis-audit.vercel.app" rel="noopener noreferrer"&gt;join the waitlist&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>webdev</category>
      <category>rls</category>
    </item>
  </channel>
</rss>
