Vai al contenuto

Basi della sicurezza

"No API key found in request" su Supabase, e la correzione sbagliata

"No API key found in request" significa che la tua richiesta è arrivata a Supabase senza chiave. Quasi tutte le risposte indicano le regole del database.

Vlad Tkachenko11 min di lettura
Una richiesta arriva a un portone alto con la fessura del pass vuota e una sbarra di traverso, e il database a cui era diretta resta intatto dietro.

In breve

  • "No API key found in request" significa che la tua richiesta è arrivata a Supabase senza l'header apikey, quindi è stata respinta alla porta prima che al database venisse chiesto qualcosa.
  • La correzione giusta è la tua chiave pubblicabile in quell'header, cioè la chiave pensata per essere pubblica. La libreria client di Supabase la invia per te a ogni richiesta.
  • Le due correzioni a cui si ricorre invece sono la chiave segreta e una regola più larga sulla tabella. Entrambe zittiscono il messaggio, ed entrambe lasciano le tue righe leggibili a chiunque abbia la chiave che viaggia dentro la tua app.

Ieri la tua app funzionava. Oggi una lista che si riempiva di righe resta vuota, e se apri la console del browser c'è una breve risposta di Supabase dove dovrebbero esserci i dati:

{
  "message": "No API key found in request",
  "hint": "No `apikey` request header or url param was found."
}

È tutto quello che ottieni. Non dice quale tabella stavi leggendo, cosa hai mandato, né cosa cambiare.

Ecco la parte che guida dopo guida racconta male: "No API key found in request" parla di un header mancante, e quasi tutti i consigli che troverai parlano delle regole del tuo database. Sul forum di discussione di Supabase questo messaggio ha un thread tutto suo, sedici persone ci propongono otto rimedi diversi, e tre di loro dicono di aggiungere o allargare una regola su una delle tue tabelle. Tutti quelli che ci hanno provato raccontano che il messaggio è sparito. Se una regola sia mai stata la causa è un'altra domanda, e il thread non la chiude mai. Quello che è certo dopo è che più persone possono leggere la tabella di prima.

Cosa significa "No API key found in request"

Supabase ha respinto la tua richiesta al portone, prima che al database venisse chiesto qualcosa.

Ogni richiesta verso il tuo progetto arriva prima a una porta che Supabase mette davanti a tutto il resto. In quel momento il suo compito è stretto: leggere l'header apikey, confrontare il valore con le chiavi del tuo progetto e far passare la richiesta. Senza una chiave da leggere si ferma lì e risponde 401, il codice di stato per "non so chi sei". La documentazione di Supabase su quella porta dice essa stessa che una chiave mancante o non valida viene respinta così.

Pensa a quella porta come a un portiere all'ingresso del palazzo, e non come a una serratura su uno schedario. Il portiere chiede un pass a chiunque arrivi. Le regole che decidono chi può aprire quale schedario stanno due piani più su, e qui nessuno le ha consultate, perché niente ha superato l'ingresso.

Come errori, quindi, questo è mite. Non è stato letto niente, non è stato scritto niente, e i tuoi dati sono esattamente come erano. Una richiesta è stata respinta.

La porta legge la chiave. Le regole della tua tabella stanno una tappa più avanti, e una richiesta respinta non le raggiunge mai.

Perché la mia richiesta è arrivata senza chiave?

Perché il valore che la tua app doveva mandare non c'era nella richiesta che il browser ha fatto davvero. Quattro cause coprono quasi tutti i casi.

La chiave non è mai arrivata nell'app compilata. Il tuo codice la legge da una variabile d'ambiente, la build che ha prodotto il sito online non aveva quella variabile impostata, e createClient ha ricevuto una stringa vuota. Tutto compila, l'app si carica, e ogni richiesta parte senza chiave. In un progetto Vite o Next la variabile deve anche portare il prefisso che la segna come sicura per il browser, che è una trappola a sé.

Stai chiamando il percorso REST a mano. Una fetch scritta verso /rest/v1/la_tua_tabella manda gli header che hai scritto e nient'altro. La libreria client di Supabase aggiunge apikey per te; una richiesta scritta a mano ha quello che le hai dato.

Qualcosa nel mezzo l'ha tolto. Una regola di rewrite, un proxy o un tuo gateway sta fra la tua app e Supabase e inoltra la richiesta senza l'header.

Stai guardando un redirect e non una richiesta di dati. Se il messaggio compare dopo che qualcuno ha fatto login o ha cliccato un link di conferma, e la barra degli indirizzi mostra ancora il tuo URL Supabase, l'impostazione da guardare è la Site URL dentro Auth. La risposta con più reazioni di tutto quel thread Supabase è di qualcuno che lì aveva scritto l'indirizzo del proprio sito senza https:// davanti.

La correzione, in una riga

Manda la tua chiave pubblicabile nell'header apikey.

import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  'https://il-tuo-progetto.supabase.co',
  'sb_publishable_…',
)

Crea il client così e la libreria mette la chiave nell'header giusto a ogni richiesta, così l'header non lo scrivi mai. La documentazione di Supabase è precisa su quale sia: le chiavi pubblicabili e segrete viaggiano in apikey anziché in Authorization: Bearer, perché sono stringhe corte e opache e tutto ciò che prova a verificarle come un JWT fallisce.

Quella chiave deve stare nella tua app. Dice a quale progetto appartiene una richiesta, e quello che un visitatore che la possiede raggiunge lo decidono le regole sulle tue tabelle, che è tutta la ragione per cui può essere pubblica. Quali chiavi API sono sicure nel frontend percorre il resto della famiglia.

La correzione che peggiora tutto

La chiave segreta entra nello stesso header, e su un progetto vecchio zittisce il messaggio e continua a funzionare.

È un solo clic sbagliato. La tua dashboard elenca entrambe le chiavi sulla stessa pagina, e la coppia vecchia si somiglia: stessa forma, stessa lunghezza, una sotto l'altra. Quanto sia grave il clic dipende da quale coppia emette il tuo progetto.

Una chiave segreta recente viene respinta nel browser. Supabase blocca sb_secret_… guardando l'header User-Agent e risponde 401. Incollarne una nel frontend quindi non fa sparire l'errore, che è il risultato migliore disponibile qui.

Una vecchia chiave service_role non ha quel blocco. Funziona. La lista si riempie, l'app si comporta come la settimana scorsa, e la chiave ora sta in un file che qualsiasi visitatore può scaricare. Lì ignora ogni regola che hai scritto: service_role porta l'attributo Postgres BYPASSRLS, quindi una policy non le si applica mai. Ogni riga di ogni tabella, leggibile e scrivibile da chi apre il file.

La nota di Supabase sul blocco del browser merita due letture. Il blocco risponde 401, e un attaccante può comunque usare la chiave con altri strumenti. Quello che protegge sono le richieste della tua app; la stessa chiave nello stesso file risponde a una richiesta mandata da uno script.

Il blocco del browser riguarda il browser. La stessa chiave nello stesso file funziona da qualunque cosa non lo sia.

L'altra strada sbagliata, e perché è così facile

Allargare una regola sulla tabella zittisce il messaggio anch'essa, e cambia chi può leggere le tue righe.

Ecco quel thread Supabase per intero, raggruppato per quello che ogni risposta ti chiede di cambiare:

Cosa cambiareQuante delle otto risposteCosa cambia davvero
La Site URL dentro Auth1Dove atterra un redirect
Una policy o un grant3Chi legge e scrive le righe
La query o la sessione3Il tuo codice
L'header apikey1Cosa stava chiedendo la porta

L'ultima riga è quella che risponde al suggerimento contenuto nel messaggio. È il commento in fondo al thread e ha un voto solo.

Nessuna delle altre sette è scritta in malafede, ed è questo a rendere la cosa difficile. Un messaggio compare davvero in più situazioni, chi risponde ha davvero rimesso in moto la propria app, e una policy che già mancava è davvero qualcosa da sistemare. Quello che va storto è l'ordine: si riscrive una regola per zittire un messaggio che parla di un header, e la regola è la metà di quella coppia che poi nessuno ricontrolla. La tua app funziona in entrambi i casi, quindi niente ti dice quale delle due hai fatto.

Da fuori il risultato si vede. Il nostro scanner legge le app online come farebbe uno sconosciuto, e delle 31.056 app che ha valutato, tre hanno pubblicato una chiave segreta Supabase. Questo ne fa il ritrovamento più raro che abbiamo, e il più grave.

L'istinto accanto è molto più comune. Dietro 8.435 di quelle app abbiamo trovato un progetto Supabase. Su 3.680 di esse la domanda "uno sconosciuto può leggere questa tabella" ha ottenuto una risposta utilizzabile, e 2.096 hanno risposto di sì: almeno una tabella ha consegnato righe a una richiesta da fuori senza nessun account di mezzo. In 394 di queste la tabella aperta era chiamata come una tabella di persone.

Una parte è voluta. Un menu pubblicato, una pagina di annunci e un changelog pubblico vivono tutti in tabelle che uno sconosciuto deve poter leggere, e il nostro scanner non sa distinguerle da una tabella di clienti aperta per distrazione. Riporta quello che ha risposto e lascia il giudizio a te, che è il motivo per cui attivare Row Level Security non equivale a essere protetti.

Cosa può leggere uno sconosciuto, sulle app che hanno risposto. La banda tratteggiata sono le app che non hanno risposto niente, che non è lo stesso di un'app senza niente di aperto.

Se preferisci vedere la tua risposta anziché dedurla, la nostra scansione gratuita legge il tuo sito online e ti dice cosa raggiunge da fuori. Richiede una ventina di secondi e non serve un account: scansiona la tua app.

Come capire quale chiave hai incollato

Leggi l'inizio della stringa.

sb_publishable_… sta nella tua app. sb_secret_… mai. Non c'è niente da decodificare, perché a cosa serve la chiave è scritto sul davanti.

Un valore lungo che inizia con eyJ è una della coppia vecchia, ed è lì che le due diventano difficili da distinguere. La sezione centrale di una chiave così è informazione leggibile e non cifratura, e porta un campo che chiude la questione:

{
  "iss": "supabase",
  "role": "anon",          ← quello che conta
  "iat": 1750000000
}

anon è la pubblicabile della coppia. service_role è quella da ruotare oggi. La documentazione di Supabase lo dice più secco di quanto lo diremmo noi: se un tutorial o un assistente IA ti dice di copiare una chiave lunga che inizia con eyJ, è stato scritto per le chiavi vecchie.

Entrambe le coppie possono essere attive insieme, ed è il dettaglio su cui si inciampa. Creare una chiave pubblicabile o segreta lascia le tue chiavi anon e service_role esattamente dov'erano, e continuano a funzionare finché non le disattivi nella dashboard come passo a parte. Cosa è cambiato con le chiavi nuove copre quella migrazione, e dove vive ogni valore nella dashboard è la pagina da aprire se non ci sei mai stato.

Se la chiave segreta è già nella tua app

Ruotala prima di modificare qualsiasi cosa, e lavora nell'ordine che dà Supabase.

Crea una nuova chiave segreta nella dashboard. Sostituisci la vecchia ovunque il tuo progetto la usi. Verifica che ogni parte della tua app sia passata a quella nuova. Solo allora ritira la vecchia, e l'ultimo passo cambia a seconda della coppia che hai in mano: cancellare una chiave segreta non si può annullare, mentre disattivare una vecchia coppia anon e service_role è reversibile, quindi puoi riaccenderle se ti è sfuggito un client.

Se ruotare prima o chiudere prima la falla dipende da se una copia è già pubblica. Ruotare una chiave service_role di Supabase percorre entrambi gli ordini e cosa leggere dopo nei tuoi log.

Perché resti così quando l'errore sparisce

La correzione regge fino al prossimo prompt o alla prossima modifica che tocca la tua configurazione Supabase. Basta un prompt per rimettere dentro una chiave o allentare di nuovo una regola, e la tua app continua a funzionare in entrambi i casi, quindi il cambiamento si vede solo quando qualcuno guarda.

Reeve Monitor guarda al posto tuo:

  • tutti e nove i controlli ogni ora, su un massimo di tre app
  • un'email il giorno in cui una nuova scansione dà alla tua app un voto più basso della precedente
  • se l'app è raggiungibile, ogni 60 secondi
  • un rapporto mensile di quello che ha visto

Una chiave che può scrivere ogni riga può anche svuotare ogni tabella. Reeve Care conserva una copia del tuo database Supabase dove nessuna chiave della tua app può arrivare.

  • una copia cifrata ogni giorno, tenuta fuori dal tuo account Supabase
  • ogni copia verificata prima di contare, con il conteggio delle righe di ogni tabella
  • un ripristino con un clic che salva lo stato attuale prima di sostituire qualcosa
  • anche i tuoi file caricati, appena colleghi una credenziale di Storage
  • tutto ciò che fa Monitor

Cosa fare oggi

Cosa fare

  • Leggi il messaggio come un rifiuto alla porta. Le tue righe non sono state toccate e non c'è niente da recuperare.
  • Guarda nella scheda rete del browser la richiesta fallita e controlla se un header apikey c'è proprio. Quell'unica occhiata ti dice se è un problema di chiave o di redirect.
  • Crea il client Supabase con la chiave pubblicabile e lascia che sia la libreria a mandare l'header. sb_publishable_… su un progetto recente, la chiave anon su uno vecchio.
  • Se nella tua app c'è già una chiave segreta, ruotala per prima cosa. Cancellarla dal codice non chiude la porta, perché la vecchia versione resta nella cronologia delle versioni e in ogni copia in cache del sito.
  • Rimetti a posto ogni regola allargata durante questa caccia, poi controllala da fuori e non dall'anteprima del tuo builder.

Parti dall'unica tabella da cui è arrivato l'errore, poi leggi le policy di ogni tabella che hai toccato nella stessa settimana. La checklist di sicurezza in 10 minuti copre cos'altro resta di solito aperto in un'app appena lanciata, e la guida alla sicurezza di Supabase percorre il resto di quello che uno sconosciuto può raggiungere.

FAQ

Cosa significa "No API key found in request"?

Significa che la tua richiesta è arrivata a Supabase senza l'header apikey, quindi la porta che Supabase mette davanti al tuo progetto l'ha respinta prima che il database fosse coinvolto. Non è stato letto niente, non è stato scritto niente, e nei tuoi dati non è cambiato niente. La risposta porta un suggerimento che dice la stessa cosa con altre parole: no apikey request header or url param was found.

Quale chiave Supabase va nell'header apikey?

La tua chiave pubblicabile, cioè sb_publishable_ su un progetto recente e la chiave anon su uno più vecchio. Supabase la fornisce esattamente per questo e si aspetta che sia visibile dentro la tua app. La libreria client la mette nell'header a ogni richiesta, quindi se crei il client con la chiave pubblicabile non scrivi mai l'header a mano.

È sicuro tenere la mia chiave anon nel browser?

Sì, e deve stare lì perché la tua app possa parlare con il tuo progetto. La chiave anon dice solo a quale progetto appartiene una richiesta; quello che un visitatore che la possiede raggiunge davvero lo decidono le regole di Row Level Security sulle tue tabelle. Quali chiavi API sono sicure nel frontend percorre tutta la famiglia delle chiavi e spiega come leggerle.

Ho usato la chiave service_role e ha funzionato. È un problema?

Sì, ed è la cosa che vale la pena sistemare oggi. Una chiave service_role porta l'attributo Postgres BYPASSRLS, quindi le tue policy non le si applicano mai. Pubblicata dentro la tua app finisce in un file che qualsiasi visitatore può scaricare, e chi lo scarica può leggere e scrivere ogni riga di ogni tabella. Ruota prima la chiave, poi sposta su un server quello che ne aveva bisogno.

Come faccio a sapere quale chiave ho incollato?

Leggi l'inizio della stringa. sb_publishable_ sta nella tua app e sb_secret_ mai. Un valore lungo che inizia con eyJ è una della coppia vecchia, e quelle due sembrano identiche, quindi devi guardarci dentro: la sezione centrale si decodifica in dati leggibili con un campo role, che riporta anon oppure service_role.

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

Tutti gli articoli

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.