<?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>DEV Community: Pon</title>
    <description>The latest articles on DEV Community by Pon (@vollos).</description>
    <link>https://dev.to/vollos</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4003199%2F93d7f013-9802-440b-8148-dff2428836da.jpg</url>
      <title>DEV Community: Pon</title>
      <link>https://dev.to/vollos</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vollos"/>
    <language>en</language>
    <item>
      <title>I shipped a Supabase app last month and left the front door open for weeks</title>
      <dc:creator>Pon</dc:creator>
      <pubDate>Mon, 29 Jun 2026 13:57:52 +0000</pubDate>
      <link>https://dev.to/vollos/i-shipped-a-supabase-app-last-month-and-left-the-front-door-open-for-weeks-38ee</link>
      <guid>https://dev.to/vollos/i-shipped-a-supabase-app-last-month-and-left-the-front-door-open-for-weeks-38ee</guid>
      <description>&lt;p&gt;I build with AI like everyone else right now. Claude writes most of my backend, I review it, it works, I ship. For a side project I was working on, that loop felt great until I finally sat down and read one of my own RLS policies.&lt;/p&gt;

&lt;p&gt;Here is what I found sitting in my migration:&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="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can view their data"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that policy name again, then read the &lt;code&gt;using (true)&lt;/code&gt; under it. The name says "users can view their data." The code says "anyone can view everyone's data." Those are not the same thing, and I had shipped the second one.&lt;/p&gt;

&lt;p&gt;If you have ever turned on Row Level Security in Supabase and felt safe, this is the part nobody warns you about. RLS being "enabled" does not mean RLS is doing anything. A policy with &lt;code&gt;using (true)&lt;/code&gt; is a policy that always passes. The lock is on the door, but the rule is "let everybody in."&lt;/p&gt;

&lt;p&gt;I want to be honest about how this happened, because I do not think I am special here. I asked the AI for a policy so users could read their own profiles. It gave me something that ran, the app worked in testing, every row came back fine because I was logged in as myself. Nothing failed. Tests passed. The bug only exists when a different user shows up and reads rows that were never theirs, and that is exactly the case you never test by hand.&lt;/p&gt;

&lt;p&gt;The AI was not wrong in a way I could see. It wrote code that worked. It just did not write code that was safe, because "safe" means thinking about the attacker, and the AI was thinking about the happy path I asked for. That is the gap. It is fast and it is genuinely good, but it does not sit there imagining the user who changes an id in the URL to see if your server stops them.&lt;/p&gt;

&lt;p&gt;So here is how I check now. It takes about two minutes.&lt;/p&gt;

&lt;p&gt;Open the Supabase dashboard, go to the SQL editor, and run this:&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="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;qual&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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;qual&lt;/code&gt; column is the actual USING expression for each policy. Read it. If you see &lt;code&gt;true&lt;/code&gt; sitting in there on a SELECT policy for a table that holds user data, that row is readable by anyone who can hit your API. Same story for &lt;code&gt;with check&lt;/code&gt; on insert and update policies.&lt;/p&gt;

&lt;p&gt;What you usually want instead is the policy tied to the logged-in user, something 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="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can view their own profile"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the rule says what the name always claimed: a user only sees rows where the user id matches theirs. Swap &lt;code&gt;user_id&lt;/code&gt; for whatever your column is.&lt;/p&gt;

&lt;p&gt;A few things I do every time now, none of them clever:&lt;/p&gt;

&lt;p&gt;Read the &lt;code&gt;qual&lt;/code&gt;, not the policy name. The name is a comment. It can say anything. The &lt;code&gt;qual&lt;/code&gt; is the law.&lt;/p&gt;

&lt;p&gt;Check every table that holds something personal. Not the obvious one alone — all of them. I had three tables and only thought about one.&lt;/p&gt;

&lt;p&gt;Log in as a second test user and try to read the first user's rows. If you get data back, you found it before someone else did.&lt;/p&gt;

&lt;p&gt;I am not writing this to scare anyone off AI. I use it every day and I am not going back. I just had to learn that the speed it gives me on building is speed I have to spend back on checking, because it will hand me something that runs and let me believe it is finished.&lt;/p&gt;

&lt;p&gt;I kept finding this same &lt;code&gt;using (true)&lt;/code&gt; pattern in my own projects often enough that I started writing a small thing to scan for it automatically, along with a couple of other Supabase footguns like public views that leak email and policies granted to the anon role. If you have an AI-built Supabase app and want me to run it over your schema for free, drop a comment. I am still testing it and I would rather find these on a friendly repo than have you find them the hard way.&lt;/p&gt;

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