PL-016highcoming · not tested yet

Realtime sends private rows to subscribers

A table is published to Realtime and its read policy is too broad, so new rows stream to people who shouldn't see them.

Severity
High
Who can exploit it
Anyone the table's read policy lets in
Found by
Not tested yet: coming to PatchLabs Lab
Status
Coming

PatchLabs does not test for this yet (coming). This page explains the problem, how to check your own project and how to fix it. The Lab’s live checks today are PL-001 to PL-008; this one is not part of any report until it ships. Shipping dates go in the changelog.

What it is

Supabase Realtime can stream every insert, update and delete on a table to subscribed browsers ("Postgres Changes"). A table takes part when it is in the supabase_realtime publication, which is what the dashboard's Realtime switch does. Before sending a change, Realtime checks whether each subscriber may read that row under the table's SELECT policies, using the subscriber's own token.

So a stream is exactly as private as the read policy. PL-016 means a published table holds private rows and its read policy lets the wrong people through (PL-001 or PL-002), or row level security is off (PL-008). Then anyone can open a channel with your public key and receive new messages, orders or bookings as they happen, without ever querying the table.

Delete events are a special case: Postgres can't check a policy against a row that no longer exists, so they go to every subscriber, and when row level security is on they carry only the primary key. Broadcast and Presence channels have their own authorization and aren't covered here.

Why AI builders produce it

Chat, notifications and live dashboards are popular requests, and the fix for "messages don't update live" is to switch Realtime on for the table. The read policy was usually written earlier and loosely, often to authenticated using (true), because during development every user was the same test account.

Example of vulnerable code

Simplified from the patterns we see in AI-built apps. Synthetic: not taken from any real app.

supabase/migrations/…_messages.sql (vulnerable)
create table public.messages (
  id bigint generated always as identity primary key,
  sender_id uuid not null references auth.users (id),
  recipient_id uuid not null references auth.users (id),
  body text not null,
  created_at timestamptz not null default now()
);
alter table public.messages enable row level security;

create policy "Signed-in users can read messages"
  on public.messages for select to authenticated using (true);

-- Every new message now streams to every signed-in subscriber
alter publication supabase_realtime add table public.messages;
what a stranger runs
// Any signed-in user, in the browser console of your app
supabase
  .channel('anything')
  .on('postgres_changes',
      { event: 'INSERT', schema: 'public', table: 'messages' },
      (payload) => console.log(payload.new))  // other people's messages
  .subscribe();

Check it yourself

check-pl-016.sql
-- 1. Tables published to Realtime, and whether RLS is on
select pt.schemaname, pt.tablename, c.relrowsecurity as rls_on
from pg_publication_tables pt
join pg_namespace n on n.nspname = pt.schemaname
join pg_class c on c.relnamespace = n.oid and c.relname = pt.tablename
where pt.pubname = 'supabase_realtime';

-- 2. Their read policies: Realtime sends a row to whoever these let read it
select tablename, policyname, roles, qual
from pg_policies
where schemaname = 'public' and cmd in ('SELECT', 'ALL')
  and tablename in (select tablename from pg_publication_tables
                    where pubname = 'supabase_realtime');

Any published table with RLS off, or with a read policy that doesn't mention the user, is streaming to more people than you think. Test those tables with the PL-001 and PL-002 queries.

The fix

Fix the read policy, and the stream fixes itself. If nothing in the app listens to the table, take it out of the publication instead.

supabase/migrations/…_fix_messages.sql
drop policy "Signed-in users can read messages" on public.messages;
create policy "Participants read their messages"
  on public.messages for select to authenticated
  using ((select auth.uid()) in (sender_id, recipient_id));

-- or, if nothing subscribes to this table:
-- alter publication supabase_realtime drop table public.messages;

A client-side filter: 'recipient_id=eq.' + user.id on the subscription saves bandwidth, but it is not security: the subscriber writes the filter.

PatchLabs doesn’t write this fix yet. Review the SQL with your engineer and add it as a new migration.

How PatchLabs will test it

When this check ships, the Lab will run it the way it runs PL-001 to PL-008 today: rebuild your database from supabase/migrations in a disposable Postgres, seed fake rows for two users, and try the exploit as an anonymous visitor, a signed-in stranger and the owner. Until then, the self-check above is the test.

When the pattern is intended

A public feed or a live scoreboard: if the rows are meant to be public, so is their stream. The question is always the read policy.

Why it’s rated high

High: private data is delivered the moment it's created, with no query to notice. It needs a read-policy problem first, which carries its own severity; Realtime adds speed and silence.

Guides

Free check

Check what PatchLabs does test today

This check is coming. Today’s free check rebuilds your database from your migrations, tests it as a visitor, a stranger and the owner (PL-001 to PL-008), and reads your repo for keys and risky code.

Free for your first appReads your public repo onlyOr run the free static RLS check

Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.