Torna al blog
Supabase e Row Level Security: Proteggere i Dati di un Progetto Personale

Supabase e Row Level Security: Proteggere i Dati di un Progetto Personale

10 min di lettura1 visualizzazioni
di Marco Salmaso
Con Supabase il frontend parla direttamente con il database, e questo rende la Row Level Security l'unica vera barriera tra i tuoi dati e chiunque apra la console del browser. Cos'è la RLS, come scrivere policy corrette, gli errori più comuni e come metterla in pratica, partendo dall'esperienza di questo sito.

Il Frontend che Parla con il Database

Supabase è una piattaforma che offre, sopra un database PostgreSQL, autenticazione, storage, funzioni serverless e soprattutto un'API generata automaticamente per ogni tabella. È ciò che lo rende così rapido da usare: dal frontend si scrive qualcosa come

const { data } = await supabase.from('blog_posts').select('*')

e si ottengono i dati, senza scrivere un backend.

Questa comodità ha una conseguenza importante. La chiave usata dal frontend (la anon key, o publishable key) è pubblica per definizione: chiunque visiti il sito può trovarla nel codice JavaScript. Con quella chiave, chiunque può chiamare la stessa API che usa il sito, con qualsiasi filtro e qualsiasi operazione.

Cosa impedisce allora a un visitatore di leggere tutte le tabelle o di modificarle? La risposta è una sola: la Row Level Security.

Cos'è la Row Level Security

La Row Level Security (RLS) è una funzionalità nativa di PostgreSQL che permette di definire, per ogni tabella, regole su quali righe ogni utente può vedere o modificare. Quando la RLS è attiva su una tabella, ogni query viene filtrata automaticamente secondo le policy definite. Se nessuna policy consente un'operazione, l'operazione semplicemente non restituisce o non modifica nulla.

In Supabase ogni richiesta arriva al database con un ruolo (anon per i visitatori non autenticati, authenticated per gli utenti loggati) e con le informazioni del token dell'utente, accessibili tramite funzioni come auth.uid().

-- attivare la RLS su una tabella
ALTER TABLE blog_posts ENABLE ROW LEVEL SECURITY;

Da questo momento, senza policy, la tabella è inaccessibile dall'API pubblica. Bisogna dire esplicitamente cosa è permesso.

Scrivere le Policy

Una policy specifica su quale operazione si applica (SELECT, INSERT, UPDATE, DELETE o tutte), a quali ruoli, e con quale condizione.

Lettura pubblica dei contenuti pubblicati

Per un blog, chiunque deve poter leggere gli articoli pubblicati, ma non le bozze:

CREATE POLICY "Lettura pubblica degli articoli pubblicati"
ON blog_posts
FOR SELECT
TO anon, authenticated
USING (published = true);

Scrittura riservata agli autori autorizzati

Per creare o modificare articoli serve un utente autenticato con il ruolo giusto. Un approccio comune è avere una tabella dei profili con un campo ruolo, e una funzione di supporto:

CREATE OR REPLACE FUNCTION public.is_editor()
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = ''
AS $$
  SELECT EXISTS (
    SELECT 1 FROM public.profiles
    WHERE id = auth.uid()
      AND role IN ('editor', 'admin')
  );
$$;

CREATE POLICY "Solo gli editor possono scrivere"
ON blog_posts
FOR INSERT
TO authenticated
WITH CHECK (public.is_editor());

CREATE POLICY "Solo gli editor possono modificare"
ON blog_posts
FOR UPDATE
TO authenticated
USING (public.is_editor())
WITH CHECK (public.is_editor());

USING e WITH CHECK

È una distinzione che confonde molti:

  • USING filtra le righe esistenti su cui si può operare (vale per SELECT, UPDATE, DELETE).
  • WITH CHECK verifica le righe nuove o modificate prima di salvarle (vale per INSERT e UPDATE).

Una policy di UPDATE con solo USING permetterebbe di modificare una propria riga trasformandola in qualcosa che non si dovrebbe poter scrivere, ad esempio cambiando l'autore. Per questo gli aggiornamenti hanno quasi sempre bisogno di entrambe le clausole.

Gli Errori più Comuni

Dimenticare di attivare la RLS

È l'errore più grave e più frequente. Una tabella creata con SQL senza ENABLE ROW LEVEL SECURITY è completamente esposta all'API pubblica. Il pannello di Supabase segnala queste tabelle, e il Security Advisor le evidenzia: conviene controllarlo regolarmente.

Policy troppo permissive

Una policy come USING (true) su UPDATE o una policy di scrittura TO authenticated senza altre condizioni significa che chiunque si registri può modificare i dati. Se la registrazione è aperta, equivale a lasciare la porta socchiusa.

Fidarsi di dati che l'utente può modificare

In Supabase ogni utente ha due contenitori di metadati: user_metadata e app_metadata. Il primo può essere modificato dall'utente stesso, anche già in fase di registrazione. Salvare lì il ruolo ("admin", "editor") e usarlo nelle policy significa permettere a chiunque di promuoversi da solo. I ruoli vanno tenuti in app_metadata, modificabile solo lato server, oppure in una tabella dedicata protetta da RLS.

Viste che aggirano la RLS

Le viste in PostgreSQL, per impostazione predefinita, vengono eseguite con i permessi di chi le ha create, quindi ignorano la RLS delle tabelle sottostanti. Una vista che unisce articoli e dati degli autori può così esporre email o bozze. Dalle versioni recenti di PostgreSQL si può creare la vista con security_invoker = true, che applica i permessi di chi la interroga.

CREATE VIEW blog_posts_with_author
WITH (security_invoker = true)
AS SELECT ...;

Esporre dati sensibili in colonne "innocue"

La RLS lavora per righe, non per colonne. Se una tabella pubblica contiene anche un campo con l'email dell'autore, chi può leggere la riga legge anche l'email. In questi casi conviene spostare i dati sensibili in una tabella separata o limitare le colonne con i permessi a livello di colonna.

Usare la service role key nel frontend

La service role key ignora completamente la RLS. Serve solo lato server, in ambienti sicuri. Se finisce nel codice del frontend o in un repository pubblico, va considerata compromessa e rigenerata subito.

Funzioni SECURITY DEFINER senza search_path

Le funzioni SECURITY DEFINER vengono eseguite con i privilegi del loro proprietario. Senza un search_path fissato, possono essere manipolate per eseguire codice inatteso. Fissarlo (come nell'esempio sopra) è una buona pratica obbligatoria.

L'Esperienza di Questo Sito

Questo sito è costruito proprio con Next.js e Supabase: articoli, progetti e case study vivono in tabelle PostgreSQL lette direttamente dal frontend. Facendo una revisione della sicurezza, sono emerse proprio alcune delle situazioni descritte sopra, in particolare viste che leggevano dati degli utenti senza rispettare la RLS.

La correzione ha seguito un principio semplice: chiunque legge ciò che è pubblicato, solo gli editor e gli amministratori scrivono. Ogni tabella ha ora policy esplicite per lettura e scrittura, le viste rispettano i permessi di chi le interroga e i dati personali non sono più raggiungibili dall'API pubblica. Il tutto è stato scritto come uno script SQL versionato, così da poterlo rileggere e riapplicare.

Come Verificare le Proprie Policy

  1. Security Advisor di Supabase: segnala tabelle senza RLS, viste rischiose e funzioni senza search_path.
  2. Prova da anonimo: con la sola anon key, provare a leggere bozze, scrivere una riga, aggiornare un record. Tutto ciò che non dovrebbe funzionare deve fallire.
  3. Prova da utente normale: registrare un utente senza ruoli speciali e ripetere gli stessi test.
  4. Rileggere le policy periodicamente: ogni nuova tabella o funzionalità è un'occasione per introdurre una falla.
-- elenco delle policy attive nello schema public
SELECT tablename, policyname, cmd, roles, qual, with_check
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;

Conclusioni

Con Supabase la sicurezza non sta nel frontend né in un backend intermedio: sta nel database. La Row Level Security è ciò che permette di esporre un'API pubblica senza esporre i dati, ma funziona solo se è attiva su ogni tabella e se le policy sono scritte con cura. Bastano poche regole chiare, un controllo periodico del Security Advisor e qualche test da utente anonimo per trasformare un progetto personale vulnerabile in uno solido.