<?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: Frank Gilroy</title>
    <description>The latest articles on DEV Community by Frank Gilroy (@beingrealfrank).</description>
    <link>https://dev.to/beingrealfrank</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%2F4243472%2Fcec4008a-26dd-41d2-8a89-e8800f6adbe7.jpg</url>
      <title>DEV Community: Frank Gilroy</title>
      <link>https://dev.to/beingrealfrank</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/beingrealfrank"/>
    <language>en</language>
    <item>
      <title>The Firebase security rules checklist for AI-built apps</title>
      <dc:creator>Frank Gilroy</dc:creator>
      <pubDate>Fri, 13 Nov 2026 21:27:23 +0000</pubDate>
      <link>https://dev.to/beingrealfrank/the-firebase-security-rules-checklist-for-ai-built-apps-136i</link>
      <guid>https://dev.to/beingrealfrank/the-firebase-security-rules-checklist-for-ai-built-apps-136i</guid>
      <description>&lt;p&gt;Your Firebase web config, apiKey included, is public by design: it ships in your JavaScript and anyone can read it. The only thing between that config and your users' data is your security rules, and AI tools often leave them in test mode or loosen them until the app "works". Here's how to check them in about ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Find test-mode and allow-all rules
&lt;/h2&gt;

&lt;p&gt;From your project folder:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grep -nE "if true|request.time &amp;lt;|\"\.(read|write)\": *true" firestore.rules storage.rules database.rules.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Any hit is an open door. &lt;code&gt;allow read, write: if request.time &amp;lt; timestamp.date(...)&lt;/code&gt; is the default test-mode rule: everything is public until that date, and when it passes the app breaks and someone "fixes" it with &lt;code&gt;if true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. "Signed in" is not "allowed"
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;allow read, write: if request.auth != null;&lt;/code&gt; lets every user who signs up read and change every other user's documents. Tie each document to its owner:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;match /users/{userId}/{document=**} {
  allow read, write: if request.auth != null &amp;amp;&amp;amp; request.auth.uid == userId;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Where the owner is a field, check it on both sides: &lt;code&gt;resource.data.ownerId == request.auth.uid&lt;/code&gt; for reads and updates, and &lt;code&gt;request.resource.data.ownerId == request.auth.uid&lt;/code&gt; for creates.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Lock the fields users must not change
&lt;/h2&gt;

&lt;p&gt;If users can update their own profile document, they can also set &lt;code&gt;role: "admin"&lt;/code&gt; or &lt;code&gt;credits: 999999&lt;/code&gt; on it unless your rules forbid it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;allow update: if request.auth.uid == userId
  &amp;amp;&amp;amp; !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role', 'plan', 'credits']);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  4. Check the Realtime Database too
&lt;/h2&gt;

&lt;p&gt;If you use it, &lt;code&gt;database.rules.json&lt;/code&gt; with &lt;code&gt;".read": true&lt;/code&gt; or &lt;code&gt;".write": true&lt;/code&gt; at the root opens the whole database. Scope it per user:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "rules": {
    "users": {
      "$uid": { ".read": "$uid === auth.uid", ".write": "$uid === auth.uid" }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  5. Check Storage rules
&lt;/h2&gt;

&lt;p&gt;Storage has its own rules file, and AI tools often leave it wide open. Scope uploads to the user's own folder and limit size and type:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;match /b/{bucket}/o {
  match /uploads/{userId}/{fileName} {
    allow read: if request.auth != null &amp;amp;&amp;amp; request.auth.uid == userId;
    allow write: if request.auth != null &amp;amp;&amp;amp; request.auth.uid == userId
      &amp;amp;&amp;amp; request.resource.size &amp;lt; 5 * 1024 * 1024
      &amp;amp;&amp;amp; request.resource.contentType.matches('image/.*');
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  6. Keep admin credentials out of the browser and the repo
&lt;/h2&gt;

&lt;p&gt;The web config is fine to ship. A service-account key (a JSON file with &lt;code&gt;"type": "service_account"&lt;/code&gt; and a &lt;code&gt;private_key&lt;/code&gt;) is not: it bypasses every rule. It belongs only on a server or in Cloud Functions. Check your history too:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git log --all --oneline -S '"private_key"'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If that prints anything, delete the key in the Google Cloud console (IAM → Service accounts → Keys) and create a new one. Removing the file from the repo is not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Prove it from the outside
&lt;/h2&gt;

&lt;p&gt;Signed out, a private collection must refuse to list:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl "https://firestore.googleapis.com/v1/projects/&amp;lt;project-id&amp;gt;/databases/(default)/documents/&amp;lt;collection&amp;gt;?pageSize=5"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You want &lt;code&gt;403 PERMISSION_DENIED&lt;/code&gt;. If you get documents back, anyone on the internet can too. The Rules Playground in the Firebase console lets you test signed-in cases, such as user A reading user B's document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your repo automatically.&lt;/strong&gt; Leakproof's free scanner flags Firebase rules left in test mode or set to &lt;code&gt;if true&lt;/code&gt;, admin keys in client code, secrets in public env vars and more. It runs locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx github:beingRealFrank/leakproof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want someone to go through all of this, plus your live project, and send fix PRs? &lt;a href="https://beingrealfrank.github.io/leakproof/#pricing" rel="noopener noreferrer"&gt;See the fixed-price audits&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance. Found a mistake? Open an issue at &lt;a href="https://github.com/beingRealFrank/leakproof/issues" rel="noopener noreferrer"&gt;https://github.com/beingRealFrank/leakproof/issues&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>security</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Supabase RLS checklist for apps built with Lovable, Bolt or Cursor</title>
      <dc:creator>Frank Gilroy</dc:creator>
      <pubDate>Fri, 13 Nov 2026 18:56:37 +0000</pubDate>
      <link>https://dev.to/beingrealfrank/the-supabase-rls-checklist-for-apps-built-with-lovable-bolt-or-cursor-3h7i</link>
      <guid>https://dev.to/beingrealfrank/the-supabase-rls-checklist-for-apps-built-with-lovable-bolt-or-cursor-3h7i</guid>
      <description>&lt;p&gt;AI app builders make Supabase apps fast. They also make it easy to ship a database anyone can read.&lt;/p&gt;

&lt;p&gt;Your frontend holds the &lt;strong&gt;anon key&lt;/strong&gt;, and that's by design: anyone can see it. The only thing between that key and your users' data is Row Level Security (RLS). Here's how to check yours in about ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Find tables with RLS off
&lt;/h2&gt;

&lt;p&gt;Run this in the Supabase SQL editor:&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;select&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row returned is a table the anon key can read and write through the REST API, whatever your UI shows. Fix:&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;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With RLS on and no policies, nobody can access the table except your server using the service_role key. Then add only the access you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Find policies that let everyone in
&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;select&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policyname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&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;qual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&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;&lt;code&gt;using (true)&lt;/code&gt; on a &lt;strong&gt;select&lt;/strong&gt; policy means "anyone can read every row". That's fine for a product catalog and a leak for anything per-user. On &lt;strong&gt;insert/update/delete/all&lt;/strong&gt; it means anyone can change anyone's data. The usual fix is an ownership check:&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;drop&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;name&amp;gt;"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="s2"&gt;"owners only"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;todos&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&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;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&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="nf"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_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;h2&gt;
  
  
  3. Prove it from the outside
&lt;/h2&gt;

&lt;p&gt;Policies are easy to misread. Test with the same key an attacker has (your anon key, from your site's JS):&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; \
  -H &lt;span class="s2"&gt;"apikey: &amp;lt;anon key&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signed out, this should return &lt;code&gt;[]&lt;/code&gt; or an error for any private table. Then sign in as test user A and confirm you can't see user B's rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep the service_role key out of the browser
&lt;/h2&gt;

&lt;p&gt;The service_role key ignores RLS entirely. It belongs only in server code (API routes, server actions, edge functions). Search your code for &lt;code&gt;SERVICE_ROLE&lt;/code&gt; and make sure none of it is in a &lt;code&gt;'use client'&lt;/code&gt; file, a &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; / &lt;code&gt;VITE_&lt;/code&gt; variable, or committed to git. If it ever was, rotate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Watch for the service_role shortcut in server actions
&lt;/h2&gt;

&lt;p&gt;A common generated pattern: a server action creates a service_role client "to make it work", then updates a row by id. RLS no longer applies, so any signed-in user can change any row by sending a different id. Use the user's own Supabase client on the server (so RLS applies), or add &lt;code&gt;.eq('user_id', user.id)&lt;/code&gt; to every query.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Check storage buckets
&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;select&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A public bucket serves any file to anyone with its URL, with no policy check. Fine for avatars, not for invoices, uploads or exports. Make those private and use &lt;code&gt;createSignedUrl()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Check views and functions
&lt;/h2&gt;

&lt;p&gt;Views run with their creator's permissions by default, so a view over a protected table can leak it. On Postgres 15+ use &lt;code&gt;create view ... with (security_invoker = true)&lt;/code&gt;. Functions marked &lt;code&gt;security definer&lt;/code&gt; also bypass RLS, so make sure they check &lt;code&gt;auth.uid()&lt;/code&gt; themselves.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Automate the repo side.&lt;/strong&gt; I made a free, MIT-licensed scanner, &lt;a href="https://github.com/beingRealFrank/leakproof" rel="noopener noreferrer"&gt;Leakproof&lt;/a&gt;, that reads your migrations and code and flags tables without RLS, allow-all policies, service_role keys in client code, secrets behind public env prefixes, public buckets and unauthenticated server actions. It runs locally with nothing uploaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx github:beingRealFrank/leakproof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disclosure: I also offer &lt;a href="https://beingrealfrank.github.io/leakproof/" rel="noopener noreferrer"&gt;fixed-price audits&lt;/a&gt; of AI-built apps. The checklist and scanner are free either way.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

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