In breve
Un’app Lovable è sicura quanto il suo database Supabase, e “la RLS è attiva” dimostra molto poco. Testi con due utenti in due aziende, tenga i ruoli fuori dai metadati utente, tenga la chiave segreta fuori dal browser e controlli separatamente grant, viste, storage, funzioni e rate limit, perché ognuno aggira le sue policy a modo suo. Qui sotto trova la lista che seguo, il prompt da incollare in Lovable o in Claude e otto controlli che ho aggiunto dopo che l’app Lovable di un cliente mi ha mostrato cosa perdono i primi dodici.
Sono Denys, ingegnere full-stack e di IA. Questo mese ho riparato l’app di un cliente costruita con Lovable. Tutto girava su Supabase, come al solito, e le correzioni di sicurezza erano così tante che l’app sembrava fatta di quelle. Questa è la lista che ora seguo su ogni progetto Lovable o Supabase prima di dire che i dati dei clienti sono protetti.
Lovable è sicuro? I problemi di sicurezza finora
La piattaforma costruisce e ospita la sua app, ma la sicurezza dell’app dipende soprattutto dal database Supabase che ha dietro, perché l’app legge e scrive su quel database direttamente dal browser. Il registro pubblico mostra entrambi i tipi di problema:
| Quando | Cosa è successo | Fonte |
|---|---|---|
| Marzo 2025 | CVE-2025-48757: una scansione di 1.645 app vetrina di Lovable ha trovato 303 endpoint in 170 di esse che servivano dati a chiunque avesse la chiave pubblica, perché la RLS mancava o era troppo larga | Divulgazione di Matt Palmer, maggio 2025 |
| Febbraio 2026 | Un’app ospitata su Lovable ha esposto 18.697 record di utenti; un controllo di accesso nelle sue funzioni faceva entrare i visitatori anonimi e bloccava gli utenti autenticati. Lovable ha dichiarato che parte del codice non era stata generata da Lovable | The Register, 27 febbraio 2026 |
| Dal 3 febbraio al 20 aprile 2026 | Una regressione della piattaforma ha permesso ad altri utenti di Lovable di leggere la cronologia delle chat e il codice sorgente dei progetti pubblici | Articolo di Lovable sull’incidente, 22 aprile 2026 |
I primi due sono falle a livello di app, del tipo che i controlli qui sotto individuano. Il terzo era di Lovable stessa e Lovable lo ha corretto; la sua parte è tenere le chiavi fuori dalle chat e dal codice (controllo 3) e ruotare quelle che ci sono state. Lovable esegue una scansione di sicurezza prima della pubblicazione, ma agire sul risultato spetta a Lei, e quando è stata pubblicata la CVE quella scansione verificava che esistesse una policy, non che fosse corretta. Quindi “Lovable è sicuro?” si riduce a sapere se il suo database supera i controlli qui sotto.
Perché “la RLS è attiva” non basta in un’app Lovable?
Un’app Lovable parla con il database direttamente dal browser. In mezzo non c’è un backend suo: la pagina chiama la Data API di Supabase con una chiave pubblica, e Postgres decide cosa torna indietro (quali schemi serva quell’API è un’impostazione a parte, trattata in Supabase exposed schemas). Quindi ogni regola che protegge i dati dei clienti deve vivere nel database, e il database ha più porte.
La row level security è una di queste. I grant decidono se un ruolo può toccare una tabella, le policy decidono quali righe, e tre cose saltano del tutto le policy: la chiave segreta, le viste create senza security_invoker e le funzioni SECURITY DEFINER. Non è un’ipotesi. La guida alla sicurezza di Lovable stessa chiede di controllare le policy RLS che imposta, e la CVE-2025-48757 qui sopra è ciò che succede quando nessuno lo fa.
Il prompt: lo incolli in Lovable o in Claude
Può incollarlo così com’è. Poi controlli il risultato di persona: l’IA ama dire “fatto” quando non lo è, e un prompt che fa sparire un errore può riuscirci allargando l’accesso.
Verifica la sicurezza di questo progetto, correggi ciò che trovi, dimmi cosa hai cambiato:
1. Testa con due utenti in due aziende. L'utente A prova a leggere, modificare, eliminare i record di B per id, tramite API, ricerca ed esportazioni. Tutto deve fallire.
2. RLS su ogni tabella, le policy controllano il proprietario della riga o l'azienda. "Autenticato" non basta.
3. La service role key mai nel frontend. Le edge function che la usano controllano l'utente e filtrano da sole per azienda, la chiave salta la RLS.
4. Ruoli, piano, id azienda mai dai metadati utente, gli utenti possono modificarli. Tienili in una tabella in cui scrive solo il server.
5. Quando una richiesta invia due id (progetto + attività), controlla che l'attività appartenga a quel progetto.
6. Revoca ad anon e authenticated tutti i privilegi sulle tabelle, concedi di nuovo solo ciò che serve alle policy. La RLS non ferma TRUNCATE. Ogni migrazione che crea una tabella include il suo GRANT: dal 30 ottobre 2026 Supabase non concede più automaticamente i grant sulle nuove tabelle, e l'app riceve l'errore 42501.
7. Tutte le viste con security_invoker = true, altrimenti saltano la RLS.
8. Bucket di storage: gli utenti leggono e scrivono solo i propri file, nessun bucket pubblico per dati privati.
9. Funzioni SECURITY DEFINER: NULL deve significare nega, non consenti.
10. Rate limit su login, registrazione, reset, OTP, salvati nel DB, non in memoria.
11. Valida tutto l'input sul server.
12. Esegui il Security Advisor di Supabase, correggi ogni avviso.
I 12 controlli, e come dimostrare ciascuno
1. Due utenti in due aziende
Crei l’utente A nell’azienda 1 e l’utente B nell’azienda 2, ciascuno con qualche record. Acceda come A e provi a leggere, modificare ed eliminare i record di B per id, tramite l’API, tramite la ricerca e tramite ogni esportazione. Ogni tentativo deve restituire nulla o un errore. È l’unico test che intercetta la maggior parte di ciò che segue, perché verifica ciò che fa l’attaccante, non ciò che ha scritto lei.
Aggiunga un terzo utente che appartiene a entrambe le aziende. Una policy della forma “il chiamante è membro dell’azienda della riga” lascia quell’utente leggere le righe dell’azienda 2 mentre lavora nell’azienda 1. Su un’altra app Supabase la mia sonda con un utente così ha restituito il documento dell’altra azienda, finché la query non ha filtrato anche per l’azienda della richiesta. L’ho descritto in un utente in due org che la RLS non isolava.
2. RLS su ogni tabella, limitata al proprietario o all’azienda
auth.uid() is not null significa “qualsiasi utente autenticato”, che su una pagina di registrazione pubblica vuol dire chiunque. Ogni policy confronta il proprietario o l’azienda della riga con il chiamante. Elenchi le tabelle che hanno ancora la RLS disattivata:
select c.relname
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p') and not c.relrowsecurity;
3. La chiave segreta mai nel frontend
La service role key, e la più recente chiave sb_secret_, aggirano ogni policy. Cerchi entrambe nel JavaScript compilato e ruoti qualsiasi chiave sia mai stata pubblicata. Le edge function che la usano devono controllare da sole chi è l’utente e filtrare per azienda, perché con quella chiave nient’altro lo farà.
4. Ruoli, piano e id azienda mai dai metadati utente
raw_user_meta_data è ciò che il client ha inviato a signUp, e gli utenti possono modificarlo in seguito. Su un’altra app il trigger del nuovo utente leggeva da lì il ruolo dell’invito, quindi chiunque si registrasse poteva passare il ruolo owner e diventare Owner. Tenga ruoli, piani e id azienda in una tabella in cui scrive solo il server, oppure in app_metadata. La storia completa: un ruolo preso dai metadati di registrazione.
5. Due id in una richiesta: controllare che stiano insieme
Quando una richiesta porta un id di progetto e un id di attività, autorizzare il progetto non basta. Un utente con diritti sul progetto A invia A più un id di sezione del progetto B, e la riga finisce in B. La RLS non lo intercetta, perché l’insert registra l’id di progetto dell’attaccante stesso, che è esattamente ciò che la policy controlla. Verifichi che il figlio appartenga al genitore prima di scrivere.
6. Grant: revocare, poi concedere di nuovo solo ciò che serve
Grant e policy sono due livelli separati. Aggiungere policy non toglie un grant, per questo la guida alla RLS di Supabase dice di revocare e concedere di nuovo in modo esplicito. In un altro progetto l’immagine supabase/postgres 17.6.1 aveva dato ad anon e authenticated TRUNCATE, REFERENCES, TRIGGER e MAINTAIN su ogni tabella di public, e nessuna policy si applica a TRUNCATE. La Data API non ha un endpoint TRUNCATE, quindi non è una cancellazione con una sola richiesta, ma nessun ruolo client ne ha bisogno, e ogni funzione che esegue SQL con quel ruolo lo eredita.
revoke all on public.orders from anon, authenticated;
grant select, insert, update on public.orders to authenticated;
select grantee, table_name, privilege_type
from information_schema.role_table_grants
where table_schema = 'public' and grantee in ('anon', 'authenticated')
order by table_name;
Una data cambia questo controllo. Supabase smette di concedere automaticamente i grant sulle nuove tabelle: per i nuovi progetti dal 30 maggio 2026 e per tutti i progetti esistenti dal 30 ottobre 2026, compresi quelli creati da Lovable. Dopo, una tabella che Lovable o una migrazione crea senza un GRANT esplicito è invisibile all’app: la richiesta fallisce con 42501 e la funzione che usa la tabella si rompe. Quindi ogni migrazione che crea una tabella porta ormai i suoi grant, accanto a enable row level security e alle policy.
Due cose restano uguali. Le tabelle esistenti mantengono i grant che hanno, quindi revocare ciò che non serve resta compito suo. E la modifica toglie solo select, insert, update e delete: nel progetto Lovable, con i nuovi default, le nuove tabelle arrivavano ancora con TRUNCATE, REFERENCES, TRIGGER e MAINTAIN per anon e authenticated. Prima revocare tutto, poi concedere, resta l’ordine sicuro.
7. Ogni vista con security_invoker = true
Una vista gira con i diritti del suo proprietario, e in Supabase il proprietario è di solito postgres, quindi una vista su una tabella protetta consegna ogni riga che le policy trattengono. Da Postgres 15 in poi:
alter view public.customer_summary set (security_invoker = true);
8. Storage: solo i propri file, nessun bucket pubblico per dati privati
Un bucket pubblico serve qualsiasi file a chiunque abbia l’URL. I file privati vanno in un bucket privato con policy su storage.objects che controllano il proprietario o l’azienda, di solito dalla prima cartella del percorso. Dopo aver spostato un progetto, conti le policy di storage attive: su un’app, dopo uno spostamento ogni upload falliva perché la migrazione delle policy non era mai stata eseguita e la tabella aveva la RLS attiva con zero policy.
9. Funzioni SECURITY DEFINER: NULL significa nega
In PL/pgSQL, if not (has_permission or assignee_id = auth.uid()) non solleva nulla quando assignee_id è null: l’intera condizione è null e if la salta. La stessa espressione è sicura dentro una policy, che tratta null come negazione. Su un’app questo lasciava un ruolo con tutti i permessi disattivati agire su tutte le 3683 attività, perché nessuna aveva un assegnatario. Avvolga la condizione in coalesce(…, false). Dettagli: un NULL in un IF NOT di PL/pgSQL ha saltato il controllo.
10. Rate limit salvati nel database
Limiti login, registrazione, reset della password e OTP, e tenga il contatore in Postgres. Nell’app Lovable i limiti stavano in una variabile dentro l’edge function. Ho sondato il progetto ospitato il 17 ottobre 2026: ogni richiesta girava in un isolate nuovo, quindi il contatore in memoria valeva sempre 1 e il limite non poteva mai scattare. Il supabase functions serve locale riusa l’isolate, per questo lo stesso codice passava ogni test in locale.
La chiave conta quanto l’archivio. Sulla piattaforma ospitata l’ultimo salto di X-Forwarded-For è il proxy stesso di Supabase, quindi usarlo come chiave mette tutti i chiamanti nello stesso bucket, e x-client-ip lo imposta il client. Nella mia sonda cf-connecting-ip portava il vero client. La correzione è stata una funzione SECURITY DEFINER claim_rate_limit(bucket, key, max, window) che solo la service role può eseguire, serializzata con un advisory lock, che salva un hash dell’IP o dell’id utente invece del valore grezzo.
11. Validare tutto l’input sul server
Qualunque cosa controlli il modulo, l’API accetta tutto ciò che il client invia. Lunghezze, formati, enum e proprietà si controllano nel database (constraint, trigger) o nell’edge function, mai solo in React.
12. Il Security Advisor di Supabase, ogni avviso
Apra Advisors, poi Security Advisor, e corregga ogni segnalazione. Segnala tabelle con la RLS disattivata, viste che saltano la RLS, policy che leggono i metadati utente, policy permissive using (true) e funzioni con un search_path modificabile. Non vede i controlli 1, 5, 9 e 10, perché riguardano la logica, non la configurazione.
Cosa mi ha insegnato l’app Lovable che i 12 controlli perdono?
Queste cose vengono dall’app del cliente e dalle sue revisioni. Nessuna compare in uno scanner.
- Campi che il client non deve impostare. Il client inviava da solo
status: 'approved', saltando la moderazione. Una policy UPDATE il cuiwith checkè più permissivo del suousingpermette anche a un utente di spostare una riga a un altro proprietario. Una policy non può calcolare un valore, quindi un triggerBEFORE INSERT OR UPDATEricava di nuovostatus, ruolo, prezzo e azienda dai dati del server e ignora ciò che il client ha inviato. - Ogni percorso di scrittura, non solo l’insert principale. Dopo aver corretto l’insert principale, lo stesso aggiramento stava negli allegati, nei commenti, nel ripristino di un elemento eliminato in modo logico e nei contatori. Elenchi ogni scrittore di una colonna protetta prima di correggere il primo.
- Allow-list, non deny-list. I record falsificati hanno richiesto cinque giri di revisione, perché ogni correzione bloccava una combinazione e ne lasciava aperta una sorella. Un trigger che consente solo le combinazioni elencate di tipo e stato ha chiuso l’intera classe in una volta.
- Anche le funzioni sono un’API. Ogni funzione in
publicè richiamabile tramite/rpcda qualsiasi ruolo con EXECUTE, e la RLS non si applica alle funzioni. Dia a ogni funzioneSECURITY DEFINERunset search_path = '', revochi EXECUTE apublic,anoneauthenticatede lo conceda solo al ruolo che ne ha bisogno. - Revocare EXECUTE può rompere le pagine pubbliche. Postgres controlla EXECUTE su ogni funzione usata da ogni policy che si applica al ruolo, anche in un ramo che non verrebbe mai eseguito. Quando
anonha perso EXECUTE suis_admin(), ogni lettura anonima è fallita con 42501. Le policy solo per admin vannoto authenticated, e le letture pubbliche hanno una policy propria senza il ramo admin. - Registrazione con conferma email attiva. Con la conferma attiva,
signUpnon restituisce una sessione, quindi qualsiasi scrittura del profilo subito dopo gira comeanone non scrive nulla, in silenzio. I progetti Lovable hanno spesso l’auto-conferma attiva, che nasconde il bug. Passi i campi insignUp({ options: { data } }), lasci che il trigger del nuovo utente li validi e li scriva, e tratti quei metadati come non attendibili (controllo 4). - Test che passano per il motivo sbagliato. Lo stack locale e il progetto ospitato avevano privilegi predefiniti diversi, quindi “anon non può inserire” passava in locale perché mancava il grant, non perché la RLS funzionasse. Un test negativo ha senso solo se esiste il grant positivo. Confronti
pg_default_aclin locale e in produzione. - Il database live, non i file di migrazione. Dopo una migrazione o uno spostamento di progetto, confronti ciò che è davvero attivo:
pg_policies,pg_get_functiondefper ogni funzione di controllo e i grant visti sopra. In un’altra app una nuova migrazione ha copiato il corpo di una vecchia funzione e ha annullato in silenzio una correzione successiva.
Un’app Lovable è conforme al GDPR?
Lovable non rende un’app conforme o non conforme: lo decide ciò che l’app fa con i dati personali, e una revisione di sicurezza non risponde a questo. Queste sono le sei lacune che controllo in un’app Lovable Cloud, ciascuna con la correzione:
- Elenchi ogni responsabile del trattamento. Lovable Cloud esegue database, autenticazione, storage e funzioni su Supabase, Lovable ospita il sito e può inviare le email di accesso, e il Lovable AI Gateway passa il testo a un fornitore di modelli. Aggiunga ogni servizio di email, IA o scraping che le sue funzioni chiamano. Ognuno che riceve dati personali richiede un accordo sul trattamento dei dati (DPA), e uno fuori dall’UE anche un meccanismo di trasferimento. La regione di hosting non compare nel codice, quindi la chieda.
- Elimini un utente che ha contenuti. Le chiavi esterne di ciò che quell’utente ha scritto possono bloccare l’eliminazione, e allora una richiesta di cancellazione non si può completare. Imposti quelle chiavi su cascade o set null, aggiunga un trigger di eliminazione che rimuova anche i log email e i token di accesso dell’utente, e tenga un test di database che elimini un utente così.
- Fissi un limite di conservazione. Log email, tracciamento delle visualizzazioni, code delle email fallite e righe eliminate in modo logico tendono a tenere dati personali senza data di fine. Un job pianificato che elimina definitivamente quelle righe dopo un periodo fisso, insieme ai loro file, chiude la maggior parte del problema.
- Tracciamento delle email solo con consenso. Registrare aperture e clic per destinatario richiede il consenso secondo le regole ePrivacy: un flag di consenso, disattivato di default, con il timestamp impostato dal server, non dal client.
- Tenga gli indirizzi fuori dai log delle funzioni. I log delle edge function non vengono ripuliti, quindi un
console.logcon l’indirizzo di un destinatario lo lascia lì. Registri id e conteggi. - Attivi la conferma email. Con l’auto-conferma chiunque può registrarsi con un indirizzo che non è suo e portare nell’app l’indirizzo di una persona reale. Il punto 6 dell’elenco sopra copre il lato della registrazione.
Per controllare la sua app: cerchi le chiavi API nelle funzioni per elencare i servizi, elimini un utente di prova con contenuti, cerchi @ nei log delle funzioni e chieda a ogni fornitore il suo accordo sul trattamento dei dati. Non è una consulenza legale.
Come evitare che torni?
Ogni controllo qui sopra è una query o un test, quindi li tenga nel repository e li esegua prima di ogni deploy. I test pgTAP di Supabase coprono grant, policy e funzioni; il test delle due aziende va accanto a loro. Se la sua app ha ruoli e una schermata dei permessi, nemmeno un test per policy basta: io dimostro ogni casella di permesso su una copia della produzione, come descritto in come testare la Supabase RLS, dove 30 caselle su 87 non facevano nulla. Il Controllo sicurezza Supabase gratuito esegue 46 regole OWASP Top 10:2025 su ruoli, grant, policy e impostazioni di autenticazione di un progetto, quindi lo usi per il primo passaggio; il test delle due aziende e tutto ciò che riguarda la sua interfaccia restano manuali.
Cosa non copre questa lista: pagamenti e webhook, segreti nella cronologia Git, header di sicurezza sull’host, backup. Contano, ma sono un’altra checklist.
Se ha ereditato un’app Lovable o Supabase e non è sicuro di quali di questi controlli superi, questo audit è il punto da cui parte un recupero di progetto. Il caso del time tracking per una società di revisione mostra come appaiono i controlli sui ruoli in un’app in produzione.
Ha trovato una falla che questa lista perde?
Mi piacerebbe saperlo. Mi confronto volentieri su come mettere in sicurezza le app Lovable e Supabase.
Mi contatti