Vai al contenuto

Basi della sicurezza

New row violates row-level security policy su Supabase. E adesso?

"New row violates row-level security policy" significa che Supabase ha rifiutato una scrittura. La soluzione veloce riapre la tabella a chiunque.

Vlad Tkachenko8 min di lettura
Una pila di righe dentro una tabella, con un'altra riga che aspetta fuori, disegnata solo come contorno perché non è ancora entrata.

In breve

  • New row violates row-level security policy significa che il tuo database ha controllato le regole di quella tabella e non ne ha trovata nessuna che permettesse la riga che stavi salvando. Ha rifiutato la scrittura e ha lasciato la tabella com'era.
  • Un solo messaggio copre tre situazioni diverse: nessuna regola, una regola che parla solo di lettura, o una regola di scrittura che la tua riga non soddisfa.
  • La risposta in cima a qualsiasi ricerca fa smettere l'errore permettendo qualsiasi scrittura da parte di chiunque. La regola che vuoi davvero costa circa due minuti in più.

Qualcosa ti ha detto di attivare Row Level Security. L'advisor dentro la tua dashboard Supabase, il risultato di una scansione, una checklist, qualcuno su un Discord. L'hai attivato. Ora la tua app non riesce più a salvare niente. Ogni tentativo torna dicendo che a new row violates row-level security policy:

new row violates row-level security policy for table "profiles"

Ecco la parte che guida dopo guida racconta male: la risposta che fa sparire questo errore in dieci secondi disfa esattamente quello che hai appena attivato. È il primo risultato che troverai, funziona subito, e lascia la tabella esattamente dov'era prima che qualcuno ti dicesse di sistemarla.

Cosa significa "new row violates row-level security policy"

Al tuo database è stato chiesto di salvare una riga, ha controllato le regole di quella tabella, non ne ha trovata nessuna che facesse entrare proprio quella riga, e ha rifiutato.

Questo è tutto il messaggio. Niente è corrotto e niente si è perso: la riga non è mai stata salvata, e il resto della tabella sta esattamente come un minuto fa. La formulazione arriva da Postgres, il motore di database su cui gira Supabase, ed è per questo che suona come meccanica invece che come qualcosa scritto dal tuo builder. Supabase lo passa così com'è, con il suo numero accanto, 42501.

Quello che il messaggio non ti dirà è quale regola mancava. Una sola frase copre tre situazioni diverse, e dirti in quale ti trovi descriverebbe le tue regole a chi ha provocato l'errore:

  • La tabella ha Row Level Security attivo e nessuna regola scritta.
  • Ha una regola, e quella regola parla solo di lettura.
  • Ha una regola di scrittura, e la riga che hai mandato non la soddisfa.

Tutte e tre stampano quella stessa riga. La seconda è quella comune in un'app appena lanciata, dove una regola è stata aggiunta per rimettere in piedi gli schermi e del salvataggio non si è mai parlato.

Perché è comparso proprio quando hai attivato Row Level Security

Perché attivarlo rifiuta tutto finché una regola non dice altro, e la tua stessa app fa parte di tutto.

Questa è la sequenza che quasi tutti attraversano. Attivi l'impostazione. Gli schermi restano vuoti, perché senza regole scritte il database ora non consegna righe a nessuno, la tua app compresa. Aggiungi una regola perché le liste tornino, di solito la prima che suggerisce un risultato di ricerca o il tuo builder. Gli schermi si riempiono. E la prima volta che qualcuno preme salva compare questo errore, su una tabella che davi per sistemata.

In quella sequenza non è andato storto niente. La regola che hai aggiunto parlava di lettura, e salvare è un'altra domanda che fino a quel momento nessuno aveva fatto.

Una regola ha due metà, e solo una parla di lettura

Una policy è una condizione, e dove il database applica quella condizione dipende da cosa gli hai chiesto.

USING si applica alle righe che stanno già nella tabella. Decide quali puoi vedere, cambiare o togliere. WITH CHECK si applica alla riga che stai provando a creare, prima che esista da qualche parte. Decide se quella riga può venire al mondo.

Quella seconda metà è la parte che sorprende, perché è una regola su qualcosa che non c'è ancora.

La tua app chiede diControllato sulle righe già nella tabellaControllato sulla riga che viene scritta
leggere (select)USINGniente da controllare
salvare una riga nuova (insert)niente da controllareWITH CHECK
cambiare una riga (update)USINGWITH CHECK
togliere una riga (delete)USINGniente da controllare
La stessa condizione, puntata su due cose diverse. In lettura le si chiede delle righe che esistono; in salvataggio, di una riga che ancora non esiste.

Una policy scritta con FOR SELECT porta sempre e solo la prima metà, perché quando qualcuno legge non c'è nessuna riga nuova da controllare. Quindi non può mai permettere un salvataggio, per quanto permissiva sembri. È tutto qui lo scarto, e spiega perché le tue letture si sono riprese e le tue scritture no.

Se preferisci vederlo da fuori, la nostra scansione gratuita chiede al tuo database dal vivo cosa può già leggerci uno sconosciuto, senza account e in una ventina di secondi: scansiona la tua app.

La soluzione che fa smettere l'errore, e quanto costa

Il modo più veloce per farlo sparire è una regola che permetta qualsiasi scrittura da parte di chiunque, ed è per questo che è la risposta in cima quasi ovunque tu guardi.

Di solito arriva con questo aspetto:

CREATE POLICY "Enable insert for all users"
  ON public.profiles
  FOR INSERT
  WITH CHECK (true);

Ogni riga soddisfa true, quindi ogni salvataggio è permesso, quello della tua app e quello di chiunque altro abbia la chiave che viaggia dentro di essa. Rispegnere Row Level Security fa lo stesso lavoro in modo ancora più completo.

Funzionano entrambe. Entrambe ti lasciano dov'eri prima che l'advisor segnalasse la tabella, e dopo nessuna delle due mostra alcun segno che qualcosa sia aperto, perché la tua app si comporta identica in tutte e quattro le combinazioni di impostazione e regola. Una tabella può essere attiva, avere una policy valida, mostrare un badge verde nella tua dashboard e consegnare comunque le sue righe a uno sconosciuto. Questa è l'altra metà di questo problema, e vale la pena leggerla prima di incollare qualsiasi cosa.

La regola che lascia salvare la tua app, e solo la tua app

Nomina a chi appartiene la riga, e confrontalo con chi sta chiedendo:

CREATE POLICY "Users insert their own rows"
  ON public.profiles
  FOR INSERT
  TO authenticated
  WITH CHECK (auth.uid() = user_id);

auth.uid() è chi è collegato e sta facendo la richiesta. user_id è la colonna sulla riga che dice a chi appartiene. Quando quei due corrispondono, la riga viene salvata. Quando non corrispondono, o quando non c'è nessuno collegato, viene rifiutata, e chi è stato rifiutato vede lo stesso messaggio che stai guardando tu.

Due dettagli in quella regola si guadagnano il posto. TO authenticated vuol dire che la regola vale per i visitatori collegati; toglilo e la policy vale per tutti, sconosciuti compresi. E la tua app deve mandare davvero la colonna user_id, perché una regola che confronta auth.uid() con un valore arrivato vuoto non può mai corrispondere.

Quando la regola è giusta e l'errore compare lo stesso

Guarda chi era collegato prima di riguardare la policy.

Tre cause diverse che ti arrivano come una sola frase. Il codice 42501 è lo stesso in tutte e tre, ed è per questo che il messaggio da solo non può dirti in quale ti trovi.

Tre spiegazioni coprono quasi tutto quello che resta:

Non c'è nessuno collegato. auth.uid() torna vuoto per un visitatore che non ha fatto l'accesso, quindi una regola che lo confronta con una colonna proprietario non può corrispondere. Questo è normale su un form di registrazione, una lista d'attesa o un form di contatto, e quelli hanno bisogno di una regola propria che descriva cosa può aggiungere uno sconosciuto.

La colonna proprietario non arriva mai. La tua regola confronta auth.uid() con user_id, e la tua app manda tutto tranne user_id. Il confronto gira contro un vuoto e fallisce ogni volta.

La scrittura va a Storage. I file caricati atterrano in Supabase Storage, che tiene le sue policy su storage.objects invece che sulla tua tabella. Una regola scritta su profiles non dice niente su un file.

C'è una quarta spiegazione, ed è quella da escludere presto: se il salvataggio funziona dall'anteprima del tuo builder e fallisce dal tuo sito dal vivo, i due non stanno usando la stessa chiave. Una chiave segreta ignora ogni regola che hai scritto, è il suo scopo, quindi un'app che salva solo finché c'è di mezzo una chiave segreta è un'app le cui regole non sono mai state provate davvero. Quali chiavi API sono sicure nel frontend spiega come distinguere l'una dall'altra.

Cosa fare oggi

Cosa fare

  • Leggi l'errore come un rifiuto e non come un guasto. La scrittura non è avvenuta, la tabella è invariata, e non c'è niente da recuperare.
  • Controlla se la tabella ha una qualsiasi policy sulla scrittura. Una regola scritta con FOR SELECT ha sistemato i tuoi schermi e non ha detto niente sul salvataggio.
  • Aggiungi una policy FOR INSERT la cui condizione WITH CHECK nomini il proprietario della riga, e aggiungi la clausola TO che intendevi.
  • Conferma che la tua app manda davvero la colonna proprietario, poi riprova a salvare da collegato.
  • Lascia WITH CHECK (true) per le tabelle il cui contenuto ti andrebbe bene su una pagina pubblica. Per qualsiasi cosa con dentro una persona, dedicaci i due minuti.

Comincia dalla tabella da cui è arrivato l'errore, e controlla ogni altra tabella su cui hai attivato l'impostazione lo stesso pomeriggio. La checklist di sicurezza da 10 minuti copre cos'altro tende a restare aperto in un'app appena lanciata, e la guida alla sicurezza di Supabase passa in rassegna il resto di quello che uno sconosciuto può raggiungere.

FAQ

Cosa significa "new row violates row-level security policy"?

Significa che al tuo database è stato chiesto di salvare una riga, ha controllato le regole di quella tabella e non ne ha trovata nessuna che facesse entrare quella riga. Quindi ha rifiutato la scrittura. Non si è perso niente e niente è rotto, perché la riga non è mai stata salvata e il resto della tabella è intatto. Il messaggio arriva da Postgres, il motore di database su cui gira Supabase, e porta il codice 42501. Quello che deliberatamente non ti dice è quale regola mancava, perché dirlo descriverebbe le tue regole a chi ha provocato l'errore.

Ho aggiunto una policy e la lettura funziona. Perché il salvataggio continua a fallire?

Perché una policy sulla lettura non dice niente sulla scrittura. Una regola porta una metà USING, che il database applica alle righe già presenti nella tabella, e una metà WITH CHECK, che applica alla riga che stai provando a creare. Una policy scritta con FOR SELECT ha sempre e solo la prima, dato che in lettura non c'è nessuna riga nuova da controllare. I tuoi schermi si riempiono di nuovo e il primo salvataggio continua a fallire. Aggiungi una seconda policy FOR INSERT con una condizione WITH CHECK.

Posso semplicemente spegnere Row Level Security per farlo sparire?

Questo fa sparire l'errore, e lascia ogni riga di quella tabella leggibile da chiunque abbia la chiave che viaggia dentro la tua app. La tua app funziona in entrambi i casi, quindi dopo niente ti dice quale delle due hai scelto. Se la tabella contiene persone, ordini o messaggi, i due minuti che costa scrivere una regola vera sono la differenza fra una tabella privata e una pubblica.

La mia policy sembra giusta e gli insert falliscono lo stesso. Cos'altro può essere?

Tre cose spiegano la maggior parte dei casi. Non c'è nessuno collegato, quindi auth.uid() torna vuoto e una regola che lo confronta con una colonna proprietario non può mai corrispondere, il che è normale su un form di registrazione o di contatto. Oppure la tua app non manda affatto la colonna proprietario, e la regola confronta contro un valore arrivato vuoto. Oppure la scrittura va in Supabase Storage invece che in una tabella, e Storage tiene le sue policy su storage.objects.

Questo errore significa che qualcuno ha provato ad attaccare la mia app?

Quasi mai. In un'app appena lanciata è quasi sempre la tua stessa app a essere rifiutata, perché Row Level Security è stato attivato prima che esistesse una regola per le tue scritture. Vale comunque la pena leggerlo invece di scartarlo: lo stesso messaggio compare quando viene rifiutata una richiesta che in quella tabella non dovrebbe scrivere, e dal messaggio da solo non puoi distinguere le due cose.

Scritto da

Vlad Tkachenko

Fondatore di Reeve

Passo le giornate a guardare app costruite con Lovable, Bolt, v0, Cursor e Replit, e la breve lista di errori che vi ricompaiono di continuo.

Altro sull'autore

Da leggere dopo

Non sai come sta messa la tua app?

Esegui una scansione gratuita e ottieni un voto chiaro da A a F in una ventina di secondi. Senza account e senza carta.

Scansiona la tua app gratis

Controllo esterno automatizzato, non un audit completo. L'assenza di risultati non è una garanzia di sicurezza.