DEV Community

Pon
Pon

Posted on

My public view leaked every user's email. The AI called it "partial user data."

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.

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:

create view public_profiles as
select id, username, avatar_url, email
from users;
Enter fullscreen mode Exit fullscreen mode

I remember skimming this and thinking it looked reasonable. I had RLS enabled on users, 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.

Why RLS didn't save me

Two Postgres behaviors stack up here, and neither one is obvious if you came in through Supabase:

  1. Views run with the owner's permissions by default. My RLS policies on users were fine. But a view created by postgres reads the table as postgres, and the owner isn't subject to my policies. Reading users through the view skips RLS entirely, with no warning anywhere.

  2. Supabase exposes the whole public schema through its API. Anything I create in public 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.

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.

Why the AI didn't warn me

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.

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.

Check your own project in two minutes

List the views in your public schema:

select viewname from pg_views where schemaname = 'public';
Enter fullscreen mode Exit fullscreen mode

For each one, look at which columns it exposes:

select column_name from information_schema.columns
where table_schema = 'public' and table_name = 'public_profiles';
Enter fullscreen mode Exit fullscreen mode

And the honest test — ask your API the way a stranger would, with only the anon key:

curl "https://YOURPROJECT.supabase.co/rest/v1/public_profiles?select=*" \
  -H "apikey: YOUR_ANON_KEY"
Enter fullscreen mode Exit fullscreen mode

If emails, phone numbers, or anything else private comes back, you have my three-week problem.

The fix

Pick whichever fits your case:

Drop the sensitive columns from the view. A public profile needs a username and an avatar. It doesn't need an email.

On Postgres 15+, make the view respect the caller's RLS:

alter view public_profiles set (security_invoker = true);
Enter fullscreen mode Exit fullscreen mode

Now reads through the view run as the person asking, and your policies apply again.

If the view was never meant to be public, revoke API access:

revoke select on public_profiles from anon;
Enter fullscreen mode Exit fullscreen mode

I did the first two. Belt and suspenders felt right after three weeks of not noticing.

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.

Top comments (4)

Collapse
 
manomite profile image
Adeyeye George

This is exactly why I keep saying AI doesn't remove engineering, it changes where engineering effort goes. The code can be 100% functional and still be 100% wrong from a security perspective. Thanks for sharing this, a lot of people building with AI need to see it.

Collapse
 
vollos profile image
Pon

Appreciate that, and you named the split well. The part I keep hitting building with AI myself is that whether the code runs and whether the policy holds are two different questions, and the model only ever checks the first one. My view ran clean, returned rows, matched the shape I wanted, and when the AI handed back the schema it called the exposure partial user data, technically true since avatar_url and username were meant to be public, but that framing buried the one column that wasn't: email. Took three weeks of it running quiet before I thought to hit it with just the anon key.

Collapse
 
publiflow profile image
PubliFlow

This is a classic Supabase gotcha that catches almost everyone when they first start building public views or joins. The issue usually stems from the view executing with the privileges of the view creator rather than the querying user, effectively bypassing your Row Level Security policies on the underlying tables. To prevent this, you need to explicitly configure the view to respect the querying user context or ensure your RLS policies are strictly enforced on the underlying tables. Have you looked into using the security barrier option on your views to force the RLS conditions to be applied before any other filters or joins are executed?

Collapse
 
publiflow profile image
PubliFlow

Row Level Security in Supabase can be incredibly tricky when dealing with views, especially since views do not automatically inherit the RLS policies of the underlying tables unless you explicitly set the security invoker attribute. It is a common pitfall when relying on AI for database architecture because it often misses these specific Postgres security nuances. I actually had to audit our entire schema for this exact vulnerability when building our SaaS starter, which is why we now enforce strict invoker security on all views in PubliFlow by default to prevent silent data leaks.