“Uno schedario in un atrio pubblico con un cartello 'solo personale autorizzato', e nessuna serratura.”
Il client non è dalla tua parte
Gli strumenti IA moderni adorano un pattern in cui il browser parla direttamente a un database ospitato come Supabase o Firebase. È veloce da costruire e rende benissimo in demo. Il problema è il modello di sicurezza: il browser viene consegnato con una chiave del database, e il browser è completamente sotto il controllo dell'utente. Chiunque può aprire gli strumenti sviluppatore, copiare quella chiave e interrogare il database direttamente, scavalcando la tua app, le tue schermate e qualsiasi controllo che viveva nel codice front-end.
browser (qualsiasi utente) la tua UI dice database ┌───────────────┐ 'puoi vedere ┌─────────────┐ │ chiave anon ──┼────────── la tua riga' ────X ───▶│ ogni riga │ │ devtools aperti│ (solo front-end) │ per tutti │ └───────────────┘ ◀─────── restituisce TUTTE le righe ─└─────────────┘
Quindi una regola applicata nel tuo componente React, "mostra solo gli ordini dell'utente corrente", non è un controllo di sicurezza. È una preferenza di visualizzazione. Il database consegnerà volentieri gli stessi dati a una query grezza che non carica mai il tuo componente. La decisione di accesso deve vivere dove l'utente non può arrivare: nel database stesso.
Cos'è davvero la Row-Level Security
La Row-Level Security (RLS) sposta la decisione di accesso dentro il database, riga per riga. Con RLS attiva, il database rifiuta ogni richiesta per impostazione predefinita, e tu scrivi policy che dicono esattamente quali righe un dato utente può vedere o modificare. La policy classica: un utente può leggere una riga solo se la colonna del proprietario corrisponde al suo id autenticato. Ora la query diretta che scavalcava la tua UI non riceve nulla, perché a decidere è il database, non il browser.
-- Turn RLS on, THEN add a policy. RLS on with no policy = deny everything. alter table orders enable row level security; create policy "owners read their own orders" on orders for select using ( (select auth.uid()) = user_id );
È tutta qui l'idea: negare per impostazione predefinita, poi concedere in modo ristretto. È lo stesso principio del minimo privilegio applicato ovunque: parti da "nessuno può toccare niente" e apri la porta più piccola che fa funzionare la funzionalità.
Gli errori che fanno trapelare dati anche con RLS 'attiva'
Attivare la RLS è necessario ma non sufficiente. Questi sono i modi in cui le app perdono dati comunque, e uno strumento IA li produrrà tutti con allegria:
- RLS abilitata su alcune tabelle, dimenticata su altre: la tabella aggiunta la settimana scorsa non ha regole ed è spalancata.
- Una policy così permissiva da concedere tutto (using (true)), che equivale a nessuna policy con passaggi in più.
- Consegnare al client la chiave service-role / admin: quella chiave scavalca del tutto la RLS, quindi ogni policy diventa irrilevante.
- Verificare la proprietà contro un valore inviato dal client, invece dell'id di autenticazione verificato dal server: al client basta inviare un valore diverso.
Come verificare davvero
Non fidarti che sia fatto solo perché l'interruttore è attivo. Verificalo come farebbe un attaccante:
- Elenca ogni tabella. Conferma che la RLS sia abilitata E che ciascuna abbia una policy reale e circoscritta, non solo abilitata.
- Interroga come utente anonimo con la chiave pubblica e conferma di non ottenere nulla che non dovresti.
- Fai login come secondo utente e conferma di non poter leggere né scrivere le righe del primo utente.
- Cerca nel bundle client e nella config d'ambiente la chiave service-role/admin. Non dovrebbe essere lì.
Sono quindici minuti di lavoro ed è il controllo di sicurezza a più alta leva che puoi eseguire su un'app costruita con l'IA. La CVE di Lovable era, in fondo, la versione su scala industriale dell'averlo saltato.
Una riga per ciascuno
- I pattern browser-verso-database consegnano una chiave che l'utente controlla; qualsiasi controllo nel front end è decorazione, non sicurezza.
- La Row-Level Security sposta la decisione nel database, riga per riga: negare per impostazione predefinita, poi concedere in modo ristretto.
- Con RLS 'attiva' i dati trapelano comunque tramite tabelle dimenticate, policy troppo permissive, una chiave service-role lato client, o id inviati dal client.
- Verifica come un attaccante: ogni tabella ha una policy reale, l'anonimo non ottiene nulla, un secondo utente è bloccato, nessuna chiave admin nel client.
Dove andare ora