supabase / supabase-js

An isomorphic Javascript client for Supabase. Query your Supabase database, subscribe to realtime events, upload and download files, browse typescript examples, invoke postgres functions via rpc, invoke supabase edge functions, query pgvector.
https://supabase.com
MIT License
3.26k stars 270 forks source link

Realtime RLS API Call not Respecting `private: true` flag #1274

Open kaceycleveland opened 1 month ago

kaceycleveland commented 1 month ago

Bug report

Describe the bug

There is more context here in this discord thread: https://discord.com/channels/839993398554656828/1287055628769951754/1287055628769951754

To summarize though, when using supabase-js and following the docs for authorized real time broadcasts, only certain methods respect the { config: { private: true } } flag. More specifically, the REST endpoint for prompting a broadcast as described here does not respect it: https://supabase.com/docs/guides/realtime/broadcast?queryGroups=language&language=js#send-messages-using-rest-calls

A clear and concise description of what the bug is.

To Reproduce

Join a channel as a client with a given RLS policy that only allows for authenticated listeners:

    const channelName = `project_${project.id}`;
    const channel = supabase.channel(channelName, {
      config: { private: true, broadcast: { self: true } },
    });

      channel.on("broadcast", { event: "test" }, (payload) =>
      console.log("payload", payload),
    );

    channel.subscribe((status, err) => {
      if (status === "SUBSCRIBED") {
        console.log("Connected!", status);
      } else {
        console.log("realtime error", status);
      }
    });

RLS on realtime.messages:

alter policy "Allow listening for broadcasts for authenticated users only"
on "realtime"."messages"
to authenticated
using (
  (extension = 'broadcast'::text)
);

Trigger a message from a separate machine (in this case, I have a lambda function triggering it using the service key):

      const channelName = `project_${project.data.id}`;
      const channel = supabaseAdmin.channel(channelName, {
        config: { private: true },
      });

      channel.send({
      type: 'broadcast',
      event: 'test',
      payload: { message: 'Hi' },
     })

The channel.send function triggers a broadcast on a public channel when I expect it to trigger on a private channel given private: true. This was confirmed to be an issue because the send command has conditional logic based on being connected to the socket. Therefore, the workaround I provided below works because it waits until the subscription is connected first before sending.

Expected behavior

channel.send should send on a private channel instead of a public channel when private: true is set.

Screenshots

If applicable, add screenshots to help explain your problem.

System information

Additional context

I have a work around that awaits the subscription to the channel before sending.


      const subscribedResult = await new Promise((resolve, reject) => {
        channel.subscribe(async (status, err) => {
          console.log("subscribing...");
          if (status === "SUBSCRIBED") {
            await channel.send({
              type: "broadcast",
              event: "test",
              payload: { message: "Hi" },
            });
            resolve("success");
          } else {
            console.log("realtime error", status);
            reject(err);
          }
        });
      });
LeakedDave commented 2 weeks ago

With private set to false, any stranger can use your Anon key to create channels. There is a message cap of 500 messages per second for Pro accounts. That means that any bot can send 500 messages per second.

There's 2,628,288 seconds in 30 days.

So that's 1,314,144,000 messages that can be sent. 1.3 billion messages can be spammed per month to any Supabase project with just an Anon key and minimal coding knowledge.

Pricing for Pro plan -> 5 Million included then $2.50 per Million

1309144000 messages in overages per month. Divided by 1 million and multiplied by $2.50 you get $3,272.86 in cost that could be easily imposed on any Pro tier Supabase project just by this existing. This is a huge security hole.