DEV Community

Cover image for I shipped a Supabase app last month and left the front door open for weeks
Pon
Pon

Posted on

I shipped a Supabase app last month and left the front door open for weeks

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.

Here is what I found sitting in my migration:

create policy "Users can view their data"
on profiles for select
using (true);
Enter fullscreen mode Exit fullscreen mode

Read that policy name again, then read the using (true) 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.

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 using (true) is a policy that always passes. The lock is on the door, but the rule is "let everybody in."

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.

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.

So here is how I check now. It takes about two minutes.

Open the Supabase dashboard, go to the SQL editor, and run this:

select schemaname, tablename, policyname, qual
from pg_policies
where schemaname = 'public';
Enter fullscreen mode Exit fullscreen mode

The qual column is the actual USING expression for each policy. Read it. If you see true 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 with check on insert and update policies.

What you usually want instead is the policy tied to the logged-in user, something like:

create policy "Users can view their own profile"
on profiles for select
using (auth.uid() = user_id);
Enter fullscreen mode Exit fullscreen mode

Now the rule says what the name always claimed: a user only sees rows where the user id matches theirs. Swap user_id for whatever your column is.

A few things I do every time now, none of them clever:

Read the qual, not the policy name. The name is a comment. It can say anything. The qual is the law.

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

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.

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.

I kept finding this same using (true) 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.

Top comments (0)