Basi della sicurezza
Supabase Row Level Security è attivo. La tabella resta pubblica.
Attivare Supabase Row Level Security non protegge una tabella. Lo fanno le policy, e quella che ha rimesso in piedi la tua app può far entrare chiunque.
In breve
- In Supabase l'interruttore e le regole sono due cose diverse. Attivo senza regola non fa passare nessuno; attivo con la regola sbagliata non ferma nessuno.
- La regola che rimette in funzione un'app rotta è di solito quella che consente qualunque richiesta, da chiunque.
- Leggi la policy, non il pulsante. Una sola parola decide se uno sconosciuto può leggere la tua tabella.
Hai attivato Row Level Security perché qualcosa te lo ha detto: l'advisor dentro Supabase, una checklist, il risultato di una scansione, qualcuno su un Discord. Ora l'interruttore è verde nella tua dashboard. E ti stanno dicendo che la tua tabella è ancora leggibile dagli sconosciuti.
Ecco la parte che guida dopo guida racconta male: l'interruttore non protegge niente. Decide che le tue regole vengano controllate. Le regole sono un'altra cosa, devi scriverle tu, e la regola più veloce da scrivere (quella che rimette in funzione un'app rotta) fa entrare tutti.
Row Level Security è attivo in Supabase. Come fa la tabella a restare pubblica?
Perché attivarlo e decidere chi entra sono due passaggi diversi, e solo il primo è un interruttore.
Pensa all'interruttore come a mettere qualcuno alla porta. Azionarlo non decide chi passa. Decide che adesso qualcuno controlla un elenco. Le tue policy sono quell'elenco. Un elenco vuoto respinge tutti; un elenco che dice «tutti» non respinge nessuno. Entrambe le cose sono Row Level Security attivo, e la tua dashboard mostra lo stesso verde in tutti e due i casi.
Per questo l'impostazione da sola risponde a molto poco, e per questo il nostro scanner non chiede mai a Supabase se è attiva. Lo chiede alla tabella. Manda la richiesta che manderebbe uno sconosciuto, con la chiave pubblica che viaggia dentro la tua app, e guarda se torna una risposta. Chiede un conteggio invece delle righe, così scopre che la porta si è aperta senza leggere nulla di quello che c'è dietro.
La policy che ha rimesso in piedi la tua app è probabilmente il problema
Nel momento in cui attivi Row Level Security la tua app smette di mostrare dati, e quello che hai fatto subito dopo per rimetterla in funzione è proprio ciò che merita uno sguardo.
Quella sequenza è del tutto normale, ed è lì che la cosa va storta. Con l'impostazione attiva e nessuna policy scritta, Postgres (il motore di database su cui gira Supabase) rifiuta ogni richiesta per impostazione predefinita, quindi i tuoi elenchi tornano vuoti e le tue schermate restano bianche. Qualcosa deve finire sull'elenco. Se hai chiesto a Cursor o a Lovable di sistemarla, o hai incollato il primo frammento che ha fatto sparire l'errore, quello che hai adesso somiglia probabilmente a questo:
CREATE POLICY "Enable read access for all users"
ON public.profiles
FOR SELECT
USING (true);
USING (true) è la condizione che una riga deve soddisfare prima che il
database la consegni. Ogni riga soddisfa true. Lì dentro c'è un secondo
dettaglio facile da superare senza vederlo: senza clausola TO, una policy vale
per public, e public comprende allo stesso modo i visitatori con accesso
fatto e i perfetti sconosciuti.
Così l'app torna a funzionare, niente segnala un errore, e la tabella è esattamente leggibile come lo era prima che cominciassi.
Questo sistema la lettura, e solo la lettura. Se la tua app salva anche in questa tabella, la cosa successiva che incontri è new row violates row-level security policy, cioè la stessa impostazione che rifiuta una scrittura, e nessuna policy di lettura la toglie.
I quattro stati in cui può trovarsi una tabella
Due sono sicuri e due no, e l'interruttore non ti dice quali. «Chiave
pubblicabile» qui sotto è quella che sta bene nella tua app: sb_publishable_…
nei progetti Supabase nuovi, anon in quelli più vecchi.
| Row Level Security | La policy | La tua app | Uno sconosciuto con la tua chiave pubblicabile di Supabase |
|---|---|---|---|
| Spento | indifferente, nessuna letta | Funziona | Legge ogni riga |
| Acceso | nessuna scritta | Rotta | Non legge nulla |
| Acceso | USING (true) | Funziona | Legge ogni riga |
| Acceso | USING (auth.uid() = user_id) | Funziona | Legge solo le proprie |
Le due colonne centrali sono la coppia su cui la gente inciampa. Stessa impostazione, stessa pastiglia verde, esiti opposti, e la differenza è una parola dentro una regola che quasi nessuno apre.
Se preferisci non leggere ogni policy di persona, la nostra scansione gratuita fa al tuo database dal vivo la stessa domanda di uno sconosciuto e ti dice quali tabelle hanno risposto. Impiega una ventina di secondi e non serve un account: scansiona la tua app.
«Authenticated» non vuol dire «tuo»
Una policy che consente authenticated consente chiunque abbia un account,
il che (se la tua app ha un modulo di iscrizione aperto) vuol dire chiunque
sia disposto a compilarlo.
Questa è la versione più sottile dello stesso errore, e sopravvive a parecchie
revisioni perché sembra accurata. TO authenticated USING (true) si legge come
una restrizione, e lo è: esclude chi non si è mai iscritto. Quello che non fa è
impedire a un tuo cliente di leggere le righe di un altro cliente, che di solito
è ciò che intendevi con «privato».
La regola che lo fa nomina il proprietario della riga:
CREATE POLICY "Users read their own rows"
ON public.orders
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
auth.uid() è chi sta chiedendo. user_id è la colonna sulla riga che dice a
chi appartiene. La riga torna quando le due cose coincidono, e resta dov'è
quando non coincidono.
Come controllare le tue tabelle in due minuti
Apri Supabase, vai su Authentication → Policies, e leggi l'espressione dentro ogni policy invece della pastiglia accanto a ogni tabella.
Tre cose da cercare:
- Una policy la cui condizione è
true. Decidi, tabella per tabella, se ti starebbe bene avere quei dati su una pagina aperta. Per un elenco di articoli pubblicati, sì. Per qualsiasi cosa contenga una persona, no. - Una policy senza clausola
TO. Vale per tutti, con accesso fatto o meno, anche quando il resto della regola sembra molto specifico. - Una tabella con l'impostazione attiva, nessuna policy, e un'app che
comunque funziona. Quella combinazione significa che qualcosa arriva ai tuoi
dati per un'altra strada, e la spiegazione consueta è una chiave segreta
(
sb_secret_…, oservice_rolein un progetto più vecchio), che ignora ogni policy tu abbia scritto. Quali chiavi API sono sicure nel frontend spiega come distinguerla da quella sicura.
L'altro controllo a cui si ricorre è aprire l'app in una finestra privata senza accedere. Vale la pena farlo, ma è bene sapere cosa dimostra. La tua app decide cosa disegnare. Il tuo database decide cosa consegnare. Sono decisioni diverse, e uno sconosciuto che salta le tue schermate ottiene la seconda.
Che aspetto ha mentre ti sta succedendo
Nessuno. È questa la forma di questo problema, ed è il motivo per cui resta lì per mesi.
Nel tuo builder non compare nessun errore. Niente rallenta, nessuna schermata si rompe, non arriva nessuna email. La tua app si comporta esattamente come il giorno in cui l'hai lanciata, perché dal suo lato non è cambiato nulla: poteva sempre leggere quelle righe. Quello che è cambiato è che possono leggerle anche tutti gli altri, con la chiave che viaggia nel codice che il tuo sito manda a ogni visitatore.
Quando emerge, emerge di lato. Una cliente chiede come faceva qualcuno a sapere
una cosa che sapeva solo la tua app. Un elenco di indirizzi che non hai mai
pubblicato salta fuori da qualche parte. E se la regola generosa copre anche la
scrittura, FOR ALL invece di FOR SELECT, allora chiunque può anche
modificare e cancellare righe: è la versione che si scopre come una tabella
improvvisamente vuota.
Anche le regole scivolano. Una migrazione, un cambio di schema, un'altra riparazione notturna che aveva bisogno che i dati si caricassero: ognuna di queste può allentare una policy senza dirlo. Per questo qui conviene un secondo sguardo più avanti e non uno solo adesso. Tenerlo d'occhio è parte di quello che fa Reeve Care, anche se un promemoria nel calendario svolge lo stesso compito.
Cosa fare questa settimana
Cosa fare
- Apri Authentication → Policies in Supabase e leggi la condizione di ogni policy, tabella per tabella. La pastiglia sulla tabella non è la risposta.
- Per ogni tabella che contiene persone, utenti, profili, ordini, messaggi, controlla che la regola nomini il proprietario della riga invece di consentire a tutti.
- Sostituisci ogni
USING (true)su quelle tabelle con una regola che confrontaauth.uid()con la colonna del proprietario, e verifica poi che la tua app si carichi ancora. - Aggiungi la clausola
TOche avevi in mente. Una policy che ne è priva vale per gli sconosciuti quanto per i visitatori con accesso fatto. - Se una tabella ha l'impostazione attiva, nessuna policy, e la tua app mostra comunque i suoi dati, scopri cosa la sta aggirando prima di toccare qualsiasi altra cosa.
Comincia dalla tabella che ti metterebbe più in imbarazzo se fosse una pagina aperta, e sistema quella oggi. La checklist di sicurezza in 10 minuti copre questo insieme al resto di ciò che vale la pena guardare in un'app appena lanciata, e la guida alla sicurezza di Supabase ripassa cos'altro resta comunemente aperto.
FAQ
Ho attivato Row Level Security e la mia app non mostra più dati. Ho rotto qualcosa?
No, è l'impostazione che sta facendo il suo lavoro. Con Row Level Security attivo e nessuna policy scritta, Postgres (il motore di database sotto Supabase) rifiuta ogni richiesta per impostazione predefinita, comprese quelle della tua stessa app. La soluzione è aggiungere una policy che descriva chi deve vedere quali righe. L'errore da evitare è aggiungerne una che consente a tutti, perché è quella che fa tornare a funzionare l'app e lascia la tabella aperta.
USING (true) è mai la policy giusta?
Sì, per dati davvero pubblici. Una tabella di articoli pubblicati, un catalogo prodotti, un elenco di locali su una mappa: sono fatti per essere letti da chiunque, e una policy che lo consente è corretta. Smette di esserlo nel momento in cui la tabella contiene persone. Chiediti se ti troveresti a tuo agio pubblicando il contenuto di quella tabella su una pagina aperta, e lascia che sia la risposta a decidere la policy.
Row Level Security mi protegge se la mia chiave segreta è trapelata?
No. Una chiave segreta di Supabase (sb_secret_ nei progetti nuovi, service_role in quelli più vecchi) aggira completamente Row Level Security, è il motivo per cui esiste. Ogni policy che hai scritto viene saltata, su ogni tabella. Se quella chiave è nel tuo frontend, le tue policy non stanno facendo nulla per te, e ruotarla è il primo lavoro, prima di qualunque intervento sulle regole.
Mi serve Row Level Security se la mia app ha già una schermata di accesso?
Sì. La schermata di accesso governa la tua app, e la tua app non è l'unica strada per arrivare al tuo database. Supabase dà a ogni progetto un indirizzo web che risponde direttamente alle richieste, e la chiave per parlarci sta nel codice che la tua app invia a ogni visitatore. Le policy sono la parte che vale a prescindere dalla porta da cui è arrivata la richiesta.