<?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>My public view leaked every user's email. The AI called it "partial user data."</title>
      <dc:creator>Pon</dc:creator>
      <pubDate>Mon, 06 Jul 2026 15:53:36 +0000</pubDate>
      <link>https://dev.to/vollos/my-public-view-leaked-every-users-email-the-ai-called-it-partial-user-data-3ogp</link>
      <guid>https://dev.to/vollos/my-public-view-leaked-every-users-email-the-ai-called-it-partial-user-data-3ogp</guid>
      <description>&lt;p&gt;Every email address in my users table was readable with just the anon key. The app worked fine, tests were green, and it sat like that for about three weeks.&lt;/p&gt;

&lt;p&gt;I was building a side project with Claude earlier this year. At some point I asked for a public profiles page, the kind where visitors can see usernames and avatars without logging in. It did what I asked and created a view:&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="k"&gt;view&lt;/span&gt; &lt;span class="n"&gt;public_profiles&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&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;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;avatar_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I remember skimming this and thinking it looked reasonable. I had RLS enabled on &lt;code&gt;users&lt;/code&gt;, policies tested, the whole thing. When I later asked the AI what the view returned, it described it as "partial user data." Which is technically true. It just didn't mention that the partial data included the one column I'd never want public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RLS didn't save me
&lt;/h2&gt;

&lt;p&gt;Two Postgres behaviors stack up here, and neither one is obvious if you came in through Supabase:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Views run with the owner's permissions by default.&lt;/strong&gt; My RLS policies on &lt;code&gt;users&lt;/code&gt; were fine. But a view created by &lt;code&gt;postgres&lt;/code&gt; reads the table as &lt;code&gt;postgres&lt;/code&gt;, and the owner isn't subject to my policies. Reading &lt;code&gt;users&lt;/code&gt; through the view skips RLS entirely, with no warning anywhere.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Supabase exposes the whole public schema through its API.&lt;/strong&gt; Anything I create in &lt;code&gt;public&lt;/code&gt; gets an endpoint. So the view wasn't sitting unused in the database, it was one HTTP request away for anyone holding the anon key. And the anon key ships in the frontend bundle by design.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So: RLS on, policies correct, and every email still public. Each piece works exactly as documented; together they published a column I never meant to share.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the AI didn't warn me
&lt;/h2&gt;

&lt;p&gt;I don't think the AI did anything wrong by its own lights. I asked for a public profiles page and got a working one. The code ran, the page rendered, my tests (which check what the app shows) all passed. There was no test for "what else can be read that I never render." That question only comes up when someone is thinking like an attacker, and code generation doesn't.&lt;/p&gt;

&lt;p&gt;Most of the security issues I've found in my own AI-built projects follow this shape. Working code that does slightly more than I intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own project in two minutes
&lt;/h2&gt;

&lt;p&gt;List the views in your public schema:&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;viewname&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_views&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;For each one, look at which columns it exposes:&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="k"&gt;column_name&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;table_schema&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;and&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public_profiles'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the honest test — ask your API the way a stranger would, with only the anon key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://YOURPROJECT.supabase.co/rest/v1/public_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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If emails, phone numbers, or anything else private comes back, you have my three-week problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Pick whichever fits your case:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop the sensitive columns from the view.&lt;/strong&gt; A public profile needs a username and an avatar. It doesn't need an email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Postgres 15+, make the view respect the caller's RLS:&lt;/strong&gt;&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;alter&lt;/span&gt; &lt;span class="k"&gt;view&lt;/span&gt; &lt;span class="n"&gt;public_profiles&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;security_invoker&lt;/span&gt; &lt;span class="o"&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;Now reads through the view run as the person asking, and your policies apply again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the view was never meant to be public, revoke API access:&lt;/strong&gt;&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;revoke&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;public_profiles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did the first two. Belt and suspenders felt right after three weeks of not noticing.&lt;/p&gt;

&lt;p&gt;I build with AI every day and that's not changing. But I've stopped treating "it works" as the end of the review, because working code can expose more than it renders. I ended up building a small tool that scans for this pattern and a few related ones automatically. If you're on Supabase and want me to take a look at your repo for free, leave a comment and I'll run it.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <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>
