<?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: Mohibakhon</title>
    <description>The latest articles on DEV Community by Mohibakhon (@mohibakhon).</description>
    <link>https://dev.to/mohibakhon</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%2F4100519%2Fe424518a-f719-4cbc-8392-7b47aaf72973.png</url>
      <title>DEV Community: Mohibakhon</title>
      <link>https://dev.to/mohibakhon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohibakhon"/>
    <language>en</language>
    <item>
      <title>Your Supabase anon key can probably read your whole users table</title>
      <dc:creator>Mohibakhon</dc:creator>
      <pubDate>Sat, 29 Aug 2026 17:12:57 +0000</pubDate>
      <link>https://dev.to/mohibakhon/your-supabase-anon-key-can-probably-read-your-whole-users-table-dhf</link>
      <guid>https://dev.to/mohibakhon/your-supabase-anon-key-can-probably-read-your-whole-users-table-dhf</guid>
      <description>&lt;p&gt;Here is a Supabase row-level security policy. It was on a &lt;code&gt;profiles&lt;/code&gt; table holding&lt;br&gt;
names, timezones and weekly availability for real people.&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;"profiles are viewable"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&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;id&lt;/span&gt; &lt;span class="o"&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="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blocked_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads as: &lt;em&gt;you can see your own row, and anyone who hasn't blocked you.&lt;/em&gt; That is&lt;br&gt;
what it does — for a signed-in caller.&lt;/p&gt;

&lt;p&gt;For a caller with no session, &lt;code&gt;auth.uid()&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;. &lt;code&gt;id = null&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;, not&lt;br&gt;
true. And &lt;code&gt;blocked_with(id)&lt;/code&gt; returns false for every row, because it is an&lt;br&gt;
&lt;code&gt;exists()&lt;/code&gt; over a table of block pairs and there is no identity to find a pair&lt;br&gt;
for. So the expression becomes &lt;code&gt;null or not false&lt;/code&gt;, which is &lt;code&gt;true&lt;/code&gt;. For every&lt;br&gt;
row.&lt;/p&gt;

&lt;p&gt;Rather than trust that reading, evaluate it. Against a real Postgres, as &lt;code&gt;anon&lt;/code&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;set&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;select&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;auth_uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_id&lt;/span&gt; &lt;span class="o"&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;id_eq_uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;blocked_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;blocked_with&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_id&lt;/span&gt; &lt;span class="o"&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="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;blocked_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;whole_policy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt; &lt;span class="n"&gt;auth_uid&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;id_eq_uid&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;blocked_with&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;whole_policy&lt;/span&gt;
&lt;span class="c1"&gt;----------+-----------+--------------+--------------&lt;/span&gt;
          &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two nulls and a false, and the policy still says yes. It is &lt;code&gt;using (true)&lt;/code&gt; with&lt;br&gt;
extra steps, whenever nobody is logged in.&lt;/p&gt;
&lt;h2&gt;
  
  
  The comment already said so
&lt;/h2&gt;

&lt;p&gt;This is the part I keep thinking about. The migration that introduced the policy&lt;br&gt;
explained itself:&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="c1"&gt;-- With the grant in place the plain expression is right and needs no guard: a&lt;/span&gt;
&lt;span class="c1"&gt;-- signed-out caller has no identity, so blocked_with returns false for every&lt;/span&gt;
&lt;span class="c1"&gt;-- row and the policy reads exactly as `true` used to.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every word of that is correct. It was written to justify a different decision —&lt;br&gt;
whether the expression needed a &lt;code&gt;case&lt;/code&gt; guard for evaluation order — and in the&lt;br&gt;
course of justifying it, it stated the hole precisely. Then it read as reassuring&lt;br&gt;
for months, because the sentence ends on "as &lt;code&gt;true&lt;/code&gt; used to", and &lt;code&gt;true&lt;/code&gt; was what&lt;br&gt;
the table had before, and nobody re-asked whether &lt;code&gt;true&lt;/code&gt; was still an acceptable&lt;br&gt;
answer now that the table held people's schedules.&lt;/p&gt;

&lt;p&gt;A bug that is written down, accurately, in a comment, is not a rare kind of bug.&lt;br&gt;
It is a comment that answers the question it was asked and not the question that&lt;br&gt;
mattered.&lt;/p&gt;
&lt;h2&gt;
  
  
  "But you have to be logged in to see that page"
&lt;/h2&gt;

&lt;p&gt;The app has a people directory. Open it signed out and it bounces you to login.&lt;br&gt;
That is correct behaviour and it is not protection.&lt;/p&gt;

&lt;p&gt;The redirect is JavaScript. It runs in the browser, after the page has loaded,&lt;br&gt;
because the page is what runs it. Nobody reading your users would load the page.&lt;br&gt;
They would do this:&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://&amp;lt;project&amp;gt;.supabase.co/rest/v1/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: &amp;lt;your publishable key&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The publishable key is designed to be public. It ships in the client bundle of&lt;br&gt;
every Supabase app; mine is committed to a public repository on purpose. It is&lt;br&gt;
not a secret and was never meant to be one.&lt;/p&gt;

&lt;p&gt;Two things gate what it can reach: the &lt;code&gt;GRANT&lt;/code&gt; on the table, and the RLS policy.&lt;br&gt;
Supabase's default setup grants &lt;code&gt;select&lt;/code&gt; on your public tables to &lt;code&gt;anon&lt;/code&gt; — that&lt;br&gt;
is what makes an anonymous client work at all — so in practice &lt;strong&gt;the policy is&lt;br&gt;
the only thing deciding what comes back.&lt;/strong&gt; When it evaluates to &lt;code&gt;true&lt;/code&gt; for&lt;br&gt;
anonymous callers, the key is a full table read.&lt;/p&gt;

&lt;p&gt;What came back in my case was name, learning track, level, timezone, and a weekly&lt;br&gt;
availability grid. Individually dull. Together: &lt;em&gt;when is this person alone at&lt;br&gt;
their computer&lt;/em&gt; — published to anyone who asks, for a product whose users are&lt;br&gt;
about to meet strangers one-to-one on camera.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix that doesn't work
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&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="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&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="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blocked_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One line, correct, and it broke the landing page.&lt;/p&gt;

&lt;p&gt;The marketing page shows "3 waiting" on each learning-path card. It got those&lt;br&gt;
numbers by reading &lt;code&gt;profiles&lt;/code&gt; anonymously and counting rows in the browser. Add&lt;br&gt;
the session requirement and all eight cards silently become "be the first" — on&lt;br&gt;
the page whose entire job is convincing someone to sign up.&lt;/p&gt;

&lt;p&gt;I nearly shipped that. I had written "nothing depends on anonymous reads" before&lt;br&gt;
grepping for callers, and there was one, on the most important page on the site.&lt;/p&gt;
&lt;h2&gt;
  
  
  Publish the aggregate, not the records
&lt;/h2&gt;

&lt;p&gt;The landing page never needed the rows. It needed eight numbers. So the numbers&lt;br&gt;
get their own function:&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;or&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;track_counts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;track_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;learners&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt;
&lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt;
&lt;span class="k"&gt;stable&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
&lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
   &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;track_id&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
   &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;track_id&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;revoke&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;track_counts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;track_counts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;security definer&lt;/code&gt; so it can count rows the caller may no longer read. No&lt;br&gt;
argument, no filter, and a return type that can only be a track id and a total —&lt;br&gt;
there is no way to ask it about a person. Then the policy gets its session&lt;br&gt;
requirement, and the anonymous read returns &lt;code&gt;[]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The general shape: &lt;strong&gt;when an anonymous surface needs a number derived from&lt;br&gt;
private rows, give it the number.&lt;/strong&gt; Don't give it the rows and a &lt;code&gt;count()&lt;/code&gt; in&lt;br&gt;
JavaScript.&lt;/p&gt;

&lt;p&gt;Two things I deliberately did not do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Revoke &lt;code&gt;select&lt;/code&gt; on the table from &lt;code&gt;anon&lt;/code&gt;.&lt;/strong&gt; It would be a second lock on the
same door, and it changes the failure mode from "empty list" to "permission
error" on the sign-up path, where the session may not have settled yet. Zero
rows is the security property. The policy is the mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drop &lt;code&gt;security definer&lt;/code&gt; for &lt;code&gt;security invoker&lt;/code&gt;.&lt;/strong&gt; The function's whole job is
reading rows the caller cannot. That is what definer is for. The safety comes
from its signature having no way to name a person.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Two Postgres details worth knowing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;EXECUTE&lt;/code&gt; is checked when the expression is prepared, not when a branch is&lt;br&gt;
taken.&lt;/strong&gt; My policy still calls &lt;code&gt;blocked_with()&lt;/code&gt; in an expression that &lt;code&gt;anon&lt;/code&gt;&lt;br&gt;
prepares. If &lt;code&gt;anon&lt;/code&gt; lacked &lt;code&gt;EXECUTE&lt;/code&gt; on it, anonymous queries would fail with a&lt;br&gt;
permission error instead of returning zero rows — a different, noisier bug. The&lt;br&gt;
grant has to stay, even though the branch is now unreachable for anonymous&lt;br&gt;
callers. My regression test asserts the refusal is &lt;em&gt;clean&lt;/em&gt; — zero rows, not an&lt;br&gt;
error — precisely because those two failures look identical from a distance and&lt;br&gt;
mean different things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres does not promise to evaluate &lt;code&gt;AND&lt;/code&gt; left to right.&lt;/strong&gt; You cannot assume&lt;br&gt;
&lt;code&gt;auth.uid() is not null&lt;/code&gt; short-circuits the rest. If the right-hand side must not&lt;br&gt;
run, use &lt;code&gt;case&lt;/code&gt;. Here it is only a performance question, but if your guard is&lt;br&gt;
protecting against an error rather than a leak, it matters.&lt;/p&gt;
&lt;h2&gt;
  
  
  Go and check yours
&lt;/h2&gt;

&lt;p&gt;This takes ten seconds. Against your own project:&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://&amp;lt;project&amp;gt;.supabase.co/rest/v1/&amp;lt;table&amp;gt;?select=*&amp;amp;limit=5"&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: &amp;lt;your publishable key&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;Authorization&lt;/code&gt; header — that is the point, you are testing the anonymous&lt;br&gt;
path. If rows come back that a stranger should not have, you have this bug.&lt;/p&gt;

&lt;p&gt;Worth doing for every table, not just the obvious one. The policy that leaks is&lt;br&gt;
rarely &lt;code&gt;using (true)&lt;/code&gt;, because that one looks wrong. It is the one that reads&lt;br&gt;
correctly and quietly degenerates to &lt;code&gt;true&lt;/code&gt; when a value is null.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found while hardening &lt;a href="https://www.peerflow.dev" rel="noopener noreferrer"&gt;PeerFlow&lt;/a&gt;, where people match&lt;br&gt;
with one study partner to learn IT and meet on camera each week. Which is exactly why a table&lt;br&gt;
of names, timezones and availability was the wrong one to leave readable.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
