← Guide per stack

Supabase

Sicurezza per la tua app con Supabase

Supabase espone il tuo database tramite una API fin dal primo minuto. Senza un RLS ben impostato o con la chiave sbagliata nel frontend, chiunque può leggere le tue tabelle. Controlla le basi prima di lanciare.

Rischi tipici con Supabase

Tabelle senza RLS

Row Level Security è la barriera principale. Una tabella nuova senza RLS è aperta in lettura e scrittura dal browser con la chiave pubblica.

La service_role nel frontend

La chiave service_role scavalca tutta la sicurezza. Se finisce nel JavaScript del client, consegna il controllo totale del tuo database.

Bucket di Storage aperti

Un bucket pubblico serve qualsiasi file a chi ha l'URL. I documenti privati vanno in bucket protetti da policies.

"Sicurezza in Supabase" per un'app creata con l'IA significa due cose diverse. La prima è la configurazione del progetto: ciò che Supabase espone per design e ciò che tocca a te chiudere. La seconda è ciò che Lovable, Bolt o Cursor fanno con Supabase quando generano il codice, ed è da lì che arrivano quasi tutti i buchi. Questa guida copre entrambe, con tre query SQL per fare l'audit del tuo progetto in un minuto, un prompt di revisione per la tua IA e il modo di verificare dall'esterno che tutto sia chiuso.

Parte 1: come funziona la sicurezza in Supabase

L'API dei dati è pubblica per design

Supabase mette davanti al tuo Postgres una API REST (PostgREST) che il browser chiama direttamente con la chiave anon (o la nuova chiave publishable). Quella chiave va nel JavaScript di tutti i tuoi utenti ed è normale: identifica il progetto, non dà permessi. A decidere chi può leggere o scrivere ogni riga è il Row Level Security. L'altra chiave, service_role (o secret), scavalca del tutto l'RLS e può vivere solo su un server. Se capisci questa separazione, capisci l'80 % della sicurezza in Supabase.

RLS e policy: l'unica barriera reale

Una tabella nuova nello schema public è raggiungibile dall'API dal momento in cui la crei. Senza RLS attivo, chiunque abbia la chiave anon legge e scrive tutte le righe. Con RLS attivo e senza policy, non accede nessuno. Con le policy, accede chi dicono loro. Da qui la regola: RLS su tutte le tabelle e una policy esplicita per operazione (select, insert, update, delete). I pattern concreti (ogni utente le sue righe, appartenenza a un team, contenuto pubblico in sola lettura) sono in cosa controllare in Supabase prima di lanciare.

Cosa scavalca l'RLS senza che te ne accorga

  • Le viste. Di default una vista gira con i permessi di chi l'ha creata, cioè ignora l'RLS delle tabelle che interroga. Creale con with (security_invoker = on).
  • Le funzioni security definer. Una funzione RPC marcata così gira come il suo proprietario e legge quello che vuole. Usale solo quando è indispensabile, con set search_path = '', e revoca l'esecuzione ad anon se un visitatore non deve chiamarla.
  • Gli schemi esposti. Tutto ciò che sta in public (tabelle, viste e funzioni) è raggiungibile dall'API. Se una funzione è interna, spostala in un altro schema.
  • La chiave service_role. Nel frontend, in un repository pubblico o in un prompt incollato in un'IA, è il controllo totale del database.

Auth: impostazioni da guardare

In Authentication trovi la conferma via email (lasciala attiva), la lista delle Redirect URLs permesse (se è vuota o con wildcard, un magic link può finire su un dominio altrui), i limiti di richieste, il captcha per registrazione e recupero, la policy delle password, la protezione dalle password compromesse e l'accesso anonimo (disattivato a meno che tu non lo usi di proposito). Ognuna è una casella; nessuna è sbagliata di default, ma l'IA tende a toccarle perché "i test passino".

Storage, Realtime ed Edge Functions

I bucket pubblici servono qualsiasi file a chi ha l'URL: per gli upload degli utenti usa bucket privati, policy su storage.objects e URL firmati con scadenza, oltre a limite di dimensione e tipi permessi per bucket. In Realtime, le modifiche di Postgres rispettano l'RLS, ma i canali di broadcast e presence hanno bisogno di policy proprie se trasportano dati privati. Nelle Edge Functions, i segreti vanno con supabase secrets set, mai nel codice, e se disattivi la verifica del JWT per un webhook, valida tu stesso la firma del mittente.

Il progetto in sé

La stringa di connessione diretta a Postgres (con la password del database) non deve mai uscire dal tuo server né dalle tue variabili d'ambiente. Attiva l'obbligo di SSL, valuta le restrizioni di rete se hai un piano a pagamento, e controlla ogni tanto il Security Advisor del pannello (Database, Advisors): segnala tabelle senza RLS, viste security definer, funzioni con search_path mutabile e tabelle auth esposte.

Parte 2: cosa l'IA sbaglia con Supabase

Questi sono gli errori che si ripetono di più nelle app generate con Lovable, Bolt o Cursor, in ordine di frequenza:

  • Creare tabelle senza RLS. Il generatore scrive la migrazione, l'app funziona e nessuno attiva niente. È l'origine della CVE-2025-48757 di Lovable, che ha esposto dati di centinaia di progetti.
  • Policy using (true). Quando l'RLS blocca qualcosa, l'IA "lo sistema" con una policy che lascia passare tutto. È RLS disattivato con un altro nome.
  • service_role nel browser. Una query fallisce per permessi e la soluzione rapida è cambiare chiave. Cerca service_role nel tuo bundle adesso.
  • Funzioni security definer per aggirare gli errori. Stesso pattern: invece di scrivere la policy giusta, si avvolge la query in una funzione che gira da superutente.
  • Bucket pubblici per i file degli utenti. Avatar, va bene; fatture, documenti o export, no.
  • Campi di privilegio modificabili dall'utente. Un role o is_admin nella tabella profiles con una policy di update che non esclude quelle colonne. Chiunque si fa amministratore dalla console del browser.
  • Disattivare la conferma email o i limiti perché i test vadano più veloci, e dimenticarlo in produzione.

Tre query SQL per fare l'audit del tuo progetto

Incollale nell'editor SQL del pannello. La prima elenca le tabelle pubbliche senza RLS; l'ideale è che non restituisca nulla:

select n.nspname as schema, c.relname as table
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'r' and n.nspname = 'public' and not c.relrowsecurity;

La seconda trova le policy che lasciano passare tutto:

select tablename, policyname, cmd, roles
from pg_policies
where schemaname = 'public' and (qual = 'true' or with_check = 'true');

Una policy così su una tabella di contenuto pubblico in sola lettura può essere corretta; su profiles, orders o messages, no. La terza elenca le funzioni che girano con i permessi del proprietario:

select p.proname, pg_get_function_identity_arguments(p.oid) as args
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef;

Ogni risultato dovrebbe avere un motivo che sai spiegare. Se non ce l'ha, togli il security definer o spostala fuori da public.

Un prompt di revisione prima del deploy

Incollalo in Lovable, Bolt o Cursor con accesso al tuo schema e al tuo codice:

Rivedi la sicurezza di questa app con Supabase e non cambiare ancora nulla. Rispondi con una lista:
1. Tabelle dello schema public senza RLS attivo.
2. Policy con using (true) o with check (true), e se sono giustificate.
3. Colonne di privilegio (role, is_admin, plan, credits) che un utente possa modificare con le policy di update attuali.
4. Viste senza security_invoker e funzioni security definer, con il motivo di ciascuna.
5. Qualsiasi uso della chiave service_role o della stringa di connessione Postgres fuori dal server.
6. Bucket pubblici che contengono file degli utenti.
7. Impostazioni di Auth disattivate (conferma email, limiti, captcha) e redirect URL con wildcard.
Per ogni punto, proponi l'SQL o la modifica esatta, e aspetta la mia conferma.

Verifica il risultato dall'esterno

La prova definitiva non è il pannello, è ciò che l'API risponde a uno sconosciuto. Con l'URL del tuo progetto e la chiave anon (entrambi sono nel codice del browser, quindi li ha anche un attaccante):

curl -s "https://TUO_PROGETTO.supabase.co/rest/v1/profiles?select=*" -H "apikey: TUA_CHIAVE_ANON" -H "Authorization: Bearer TUA_CHIAVE_ANON"

Se restituisce righe, quella tabella è aperta. Se restituisce [] o un errore di permessi, l'RLS funziona. Ripeti con ogni tabella sensibile e con un utente normale autenticato per confermare che veda solo le sue cose. Scanaris fa questa verifica per te: rileva l'URL e la chiave di Supabase nel tuo bundle, prova le tabelle abituali senza sessione, cerca la chiave service_role e la stringa di connessione nel JavaScript, controlla dove la tua app salva il token di sessione e ti dà un fix prompt per ogni riscontro. Se la tua app l'ha generata Lovable, guarda anche la guida alla sicurezza per Lovable; se il problema sono le chiavi nel client, segreti che la tua app fa trapelare nel browser.

Domande frequenti

È un problema che la chiave anon sia nel browser?

No. È progettata per questo e identifica solo il progetto. A proteggere i dati è l'RLS. Quella che non può mai stare nel browser è la service_role (o la nuova chiave secret).

Le nuove chiavi publishable e secret cambiano qualcosa?

Cambiano il formato e permettono di ruotarle senza toccare il JWT, ma il modello è lo stesso: la publishable è pubblica e passa dall'RLS; la secret lo scavalca e vive sul server. Le chiavi vecchie continuano a funzionare finché non le disabiliti.

Se la mia app l'ha fatta Lovable o Bolt, devo comunque entrare nel pannello di Supabase?

Sì. Lo strumento genera lo schema, ma RLS, policy, bucket e impostazioni di Auth sono tuoi. Dieci minuti con le tre query qui sopra e il Security Advisor ti dicono in che stato è il progetto.

Controlli correlati

La tua app Supabase supera questi controlli?

Scansionala gratis e ottieni un prompt di fix per ogni problema che troviamo.

Scansiona il mio sito gratis