Sicurezza9 min di lettura

Checklist RLS di Supabase per app AI

Cos’è la row level security in Supabase, come attivarla, le policy che servono a quasi tutte le app e i controlli che facciamo su ogni tabella prima del lancio.

  • 19 ago 2026
Checklist RLS di Supabase per app AI

Supabase permette al browser di parlare direttamente con il database. È per questo che builder AI come Lovable e Bolt lo scelgono, ed è anche per questo che la row level security conta su Supabase più che quasi ovunque. Il browser ha in mano una chiave, quindi è il database stesso che deve saper dire di no.

Questa è la checklist che usiamo su ogni progetto Supabase prima del lancio, e la prima cosa che apriamo in un recupero. Spiega cos’è la RLS, come attivarla, le policy che servono a quasi tutte le app, i modi in cui i dati sfuggono e come verificare che funzioni.

Cos’è la row level security

La row level security (RLS) è una funzione di Postgres. Quando è attiva, ogni query su una tabella viene filtrata da regole chiamate policy, e una riga torna indietro solo se una policy lo permette. Pensa a una policy come a una clausola WHERE che il database aggiunge a ogni query, chiunque l’abbia inviata.

Supabase espone lo schema public tramite un’API che chiunque abbia l’URL del progetto e la chiave anon può chiamare. Entrambi stanno nel codice del frontend, quindi sono pubblici. Senza RLS, quell’API restituisce ogni riga di ogni tabella a chi la chiede.

Passo 1. Attivala su ogni tabella

Le tabelle create dall’editor di Supabase di solito hanno la RLS attiva. Quelle create in SQL, in una migrazione o da uno strumento AI spesso no. Controllale tutte, anche quelle aggiunte la settimana scorsa.

-- Which tables in the public schema have RLS off?
select tablename
from pg_tables
where schemaname = 'public'
  and rowsecurity = false;

-- Switch it on
alter table public.profiles enable row level security;

Una tabella con la RLS attiva e nessuna policy non restituisce nulla ai ruoli anon e authenticated. È il punto di partenza sicuro. Da qui ridai a ogni tabella esattamente l’accesso che le serve.

Passo 2. Una policy per ogni azione

Scrivi policy separate per select, insert, update e delete invece di una sola per tutto. Si leggono meglio, ed è più facile accorgersi quando ne manca una. Lo schema più comune è un utente che vede e modifica solo le proprie righe.

create policy "Users read their own rows"
on public.projects for select
to authenticated
using ( (select auth.uid()) = user_id );

create policy "Users create rows for themselves"
on public.projects for insert
to authenticated
with check ( (select auth.uid()) = user_id );

create policy "Users update their own rows"
on public.projects for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

using decide quali righe esistenti una query può vedere. with check decide quali nuovi valori può scrivere. Una policy di update ha bisogno di entrambi, altrimenti un utente può passare la sua riga a qualcun altro cambiando user_id. Mettere auth.uid() dentro una select permette a Postgres di calcolarlo una volta per query invece che una volta per riga, e sulle tabelle grandi fa differenza.

Passo 3. Team e contenuti pubblici

L’accesso per team di solito passa da una tabella di membership. La policy chiede se l’utente collegato appartiene al team proprietario della riga.

create policy "Members read their team's rows"
on public.documents for select
to authenticated
using (
  team_id in (
    select team_id from public.memberships
    where user_id = (select auth.uid())
  )
);

Anche la tabella memberships ha bisogno della sua RLS, e una policy su memberships che interroga memberships va in ricorsione. La soluzione tipica è una piccola funzione security definer che restituisce i team dell’utente corrente, usata da entrambe le policy. I contenuti che tutti possono leggere, come gli articoli pubblicati, hanno una policy per il ruolo anon che filtra su una colonna che solo il server può impostare.

create policy "Anyone reads published posts"
on public.posts for select
to anon, authenticated
using ( published = true );

Passo 4. Chiudi le strade che la aggirano

La maggior parte delle falle che troviamo non è una policy mancante. È una strada che salta del tutto le policy.

  • La chiave service role ignora completamente la RLS. Deve stare sul server e da nessun’altra parte. In un’app Vite ogni variabile che inizia con VITE_ finisce nel bundle del browser, quindi cerca la chiave nel JavaScript compilato.
  • Le view girano con i permessi del loro proprietario, di solito postgres, quindi ignorano la RLS delle tabelle sottostanti. Da Postgres 15 in poi, creale con security_invoker = true.
  • Le funzioni security definer girano con i diritti del proprietario e si possono chiamare dall’API come RPC. Dai loro un search_path fisso, controlla auth.uid() al loro interno e revoca execute ad anon se un ospite non deve mai chiamarle.
  • I metadati utente possono essere modificati dall’utente. Una policy che si fida di un ruolo salvato in user_metadata permette a chiunque di diventare admin. Tieni i ruoli in app_metadata o in una tabella che il client non può scrivere.
  • I bucket di Storage hanno le loro policy su storage.objects. Un bucket pubblico serve ogni file a chiunque abbia il link.

Passo 5. Provala come farebbe un attaccante

Leggere le policy non basta. Chiedi all’API dati che non dovresti ottenere. Con la sola chiave anon, che è già nel tuo frontend, richiedi direttamente una tabella.

curl 'https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*' \
  -H "apikey: YOUR_ANON_KEY"

Se restituisce righe che non dovrebbero essere pubbliche, la tabella è aperta. Poi accedi come un utente e chiedi per id la riga di un altro. Devi ricevere un risultato vuoto, non un errore e non la riga. Puoi anche agire come un utente nell’editor SQL, dentro una transazione.

begin;
set local role authenticated;
select set_config('request.jwt.claims',
  '{"sub":"USER-UUID","role":"authenticated"}', true);
select * from public.projects;  -- only this user's rows
rollback;

Anche il Security Advisor nella dashboard di Supabase segnala le tabelle dello schema public senza RLS. È un buon allarme. Non ti dice però se le policy che hai scritto sono giuste.

La checklist

  • La RLS è attiva su ogni tabella dello schema public
  • Ogni tabella ha una policy esplicita per ogni azione che permette
  • Le policy confrontano con auth.uid(), mai con una colonna o con metadati che il client può impostare
  • Le policy di update hanno sia using sia with check
  • La chiave service role sta solo nel codice server
  • Le view usano security_invoker, e le funzioni security definer controllano chi le chiama
  • I bucket di Storage hanno policy, e quelli pubblici non contengono nulla di privato
  • Un utente collegato non può leggere la riga di un altro cambiando un id

Facciamo questa lista prima di ogni altra cosa in un recupero, perché una sola policy mancante di solito significa che ogni record di quella tabella era leggibile da chiunque abbia guardato. È l’audit più economico del software e il più caro da saltare.

Costruiamolo insieme!