Basi della sicurezza
"No signatures found matching the expected signature" su Supabase
Un webhook Stripe in una Edge Function di Supabase dice "No signatures found matching the expected signature"? Due cause. Non smettere di verificare.

In breve
- "No signatures found matching the expected signature for payload" vuol dire che la tua funzione ha controllato la firma di Stripe e non corrispondeva. O il corpo è stato cambiato prima del controllo, o il signing secret non è quello con cui Stripe ha firmato.
- Leggi il corpo con await req.text() e passa quella stringa, intatta, a constructEventAsync. Poi confronta il secret whsec_ della tua funzione con quello dell'endpoint esatto da cui Stripe sta inviando.
- Non far sparire l'errore saltando il controllo. Il webhook deve girare con verify_jwt = false, quindi la firma è l'unica cosa che impedisce a un estraneo di dire alla tua app che un pagamento è andato a buon fine.
Hai collegato Stripe alla tua app, un pagamento di prova è andato a buon fine, e il webhook che doveva segnare l'ordine come pagato non ha fatto niente. La dashboard di Stripe mostra la consegna fallita, e i log della tua Edge Function di Supabase dicono questo:
No signatures found matching the expected signature for payload.
Are you passing the raw request body you received from Stripe?
La buona notizia sta nell'origine dell'errore. Lo lancia la parte della tua funzione che controlla che la richiesta arrivi davvero da Stripe, il che vuol dire che quel controllo esiste e sta facendo il suo lavoro. "No signatures found matching the expected signature for payload" ha due cause, e nessuna si risolve controllando di meno. La soluzione che compare per prima quando la si cerca, avvolgere il controllo in modo che l'errore venga ignorato, è quella che permette a chiunque di segnare un ordine come pagato.
Che cosa vuol dire "No signatures found matching the expected signature"?
La tua funzione ha controllato la firma di Stripe sulla richiesta, e non corrispondeva.
Immagina ogni webhook di Stripe come una lettera chiusa con un sigillo di
ceralacca. Stripe preme il sigillo con un timbro che avete solo Stripe e tu: il
signing secret del tuo endpoint, un valore che inizia con whsec_. Quando la
lettera arriva, la tua funzione preme la sua copia del timbro contro la cera. Se
i disegni combaciano, la lettera è di Stripe e nessuno l'ha toccata lungo la
strada.
Il sigillo copre la lettera esattamente com'è stata scritta. La libreria di
Stripe prende il testo della richiesta, l'ora dall'header Stripe-Signature e
il tuo secret, calcola come dovrebbe essere la firma e la cerca in quell'header.
Il messaggio qui sopra è scritto
nello stesso stripe-node,
ed è quello che lancia quando lì non corrisponde niente. La
pagina di Stripe sugli errori di firma
lo dice chiaramente: almeno uno dei tre valori che hai passato è sbagliato.
Quindi i posti dove guardare sono due. O la lettera è stata cambiata prima che la tua funzione controllasse il sigillo, o la tua funzione ha in mano il timbro sbagliato.
Causa 1: il corpo è stato cambiato prima del controllo
In una Edge Function la versione più comune è un corpo letto prima come JSON e poi ritrasformato in testo per il controllo.
const event = await req.json()
await stripe.webhooks.constructEventAsync(JSON.stringify(event), signature, secret, undefined, cryptoProvider)
Si legge in modo naturale e fallisce ogni volta. req.json() tiene i dati e
butta il testo: gli spazi, gli a capo, l'ordine in cui Stripe ha scritto le
cose. JSON.stringify scrive poi un testo nuovo con gli stessi dati dentro. Il
sigillo è stato premuto sul testo di Stripe, e questa è la copia di qualcun
altro con un'altra calligrafia, quindi non può mai combaciare.
L'esempio di webhook Stripe di Supabase fa diversamente: legge il corpo una volta sola, come testo, e passa quella stringa senza toccarla.
const signature = req.headers.get('Stripe-Signature')
const body = await req.text()
const event = await stripe.webhooks.constructEventAsync(
body,
signature,
Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET'),
undefined,
cryptoProvider,
)
Il commento in quell'esempio spiega perché: la verifica si basa sul corpo grezzo
della richiesta e non sul JSON già interpretato. Non devi interpretare niente tu
dopo, perché constructEventAsync restituisce l'evento già interpretato appena
il sigillo torna.
Lo stesso succede fuori dal tuo codice. La pagina di Stripe nomina framework che interpretano o riformattano un corpo prima che il tuo codice lo veda, e il messaggio di stripe-node aggiunge che uno strumento che ti inoltra i webhook deve conservare il corpo esatto, compresi la formattazione del JSON e il tipo di a capo. Se tra Stripe e la tua funzione c'è un proxy o un servizio di inoltro, controlla che passi il corpo così come è arrivato.
Causa 2: il signing secret è quello sbagliato
Se il corpo è intatto, è sbagliato il timbro, e la pagina di Stripe dice che è la causa più comune di tutte.
Tre valori diversi possono finire al posto del signing secret, e si somigliano:
- Il secret di un altro endpoint. La guida ai webhook di Stripe dice che ogni endpoint riceve il suo secret e che lo stesso indirizzo configurato in modalità test e live ha un secret diverso in ciascuna. Un pagamento live controllato con il secret dell'endpoint di test fallisce, e lo stesso vale al contrario.
- Il secret stampato da
stripe listen. Quando inoltri gli eventi alla tua macchina con lo strumento a riga di comando di Stripe, stampa un secretwhsec_suo. Stripe dice che è diverso dal secret dell'endpoint della dashboard e che i due vengono confusi. Funziona per gli eventi che lo strumento inoltra, e per nient'altro. - La tua chiave API di Stripe.
sk_live_…osk_test_…è la chiave con cui la tua funzione chiama Stripe. Non è un signing secret, e un controllo fatto con quella non può mai combaciare.
Per trovare quello giusto, apri l'endpoint nella dashboard di Stripe sotto
Workbench, poi Webhooks, e clicca su Reveal secret. Prima controlla
in che modalità sei. Poi confrontalo con quello che legge la tua funzione.
Nell'esempio qui sopra è STRIPE_WEBHOOK_SIGNING_SECRET, impostato nella pagina
Edge Function Secrets del tuo progetto Supabase o con
supabase secrets set.
Già che ci sei, guarda un'ultima cosa. Se il valore è stato incollato con uno spazio o un a capo alla fine, stripe-node aggiunge una nota allo stesso errore: "The provided signing secret contains whitespace." Cancella il valore e incollalo di nuovo senza niente dopo.
I messaggi che si somigliano e vogliono dire altro
stripe-node lancia diversi errori dallo stesso controllo, e ognuno indica un problema diverso. Sono tutti scritti nel suo codice dei webhook:
| Che cosa dicono i tuoi log | Che cosa è successo |
|---|---|
No signatures found matching the expected signature for payload | Il corpo è stato cambiato, o il secret è quello sbagliato. Questo articolo. |
Webhook payload must be provided as a string or a Buffer | Hai passato l'oggetto interpretato di req.json(). Passa il testo di req.text(). |
No stripe-signature header value was provided. | La richiesta non aveva l'header Stripe-Signature. Forse non arrivava affatto da Stripe. |
Timestamp outside the tolerance zone | Il sigillo combaciava, ma l'evento è più vecchio del limite. Le librerie di Stripe usano 5 minuti. |
SubtleCryptoProvider cannot be used in a synchronous context. | Hai chiamato constructEvent. Con il provider Web Crypto che serve a Deno, usa constructEventAsync. |
E uno che il tuo codice non lo raggiunge mai. Se la dashboard di Stripe mostra la consegna fallita con un 401 e i log della tua funzione sono vuoti, la piattaforma di Supabase ha rifiutato la richiesta prima che la tua funzione partisse, per un controllo che in un webhook deve essere spento.
Perché il tuo webhook ha bisogno di verify_jwt disattivato
Perché Stripe non può dimostrare chi è come fanno gli utenti della tua app, e con il controllo attivo ogni consegna viene rifiutata.
Supabase mette davanti a ogni Edge Function un controllo chiamato verify_jwt,
attivo per impostazione predefinita. Vuole un token Supabase nell'header
Authorization della richiesta, e Stripe non ne ha nessuno da mandare. La
guida alla configurazione delle funzioni
di Supabase usa proprio questo come esempio, dicendo che i webhook di Stripe
devono essere raggiungibili pubblicamente, e spegne il controllo per quella sola
funzione in supabase/config.toml:
[functions.stripe-webhook]
verify_jwt = false
Il comando supabase functions deploy accetta --no-verify-jwt per la stessa
cosa. L'esempio Stripe di Supabase porta la stessa istruzione in un commento:
Stripe verifica la richiesta tramite la sua firma, quindi fai il deploy con
verify_jwt = false.
Con il controllo della piattaforma spento, ogni richiesta da internet raggiunge il tuo codice, e il controllo del sigillo dentro la tua funzione è l'unica cosa che distingue le lettere di Stripe da quelle di chiunque altro.
Perché non dovresti mai saltare il controllo della firma
Perché senza, chiunque può dire alla tua app che un pagamento è andato a buon fine.
L'indirizzo di una funzione di webhook è l'URL del tuo progetto, che chiunque
può leggere nel tuo frontend, seguito da /functions/v1/ e dal nome della
funzione, di solito quello ovvio. L'esempio della guida alla configurazione la
chiama proprio stripe-webhook. Con verify_jwt spento, la piattaforma lascia
passare ogni richiesta fino al tuo codice. Se il tuo codice non controlla più il
sigillo, crede a ogni lettera che riceve.
Quindi un estraneo manda alla tua funzione una richiesta che dice
checkout.session.completed, con dentro un ordine o un cliente scelto da lui.
La tua funzione fa quello che fa con una vera: segna l'ordine come pagato, fa
salire di piano l'account, sblocca il download. Nessuno ha pagato. Stripe non ne
ha traccia, perché Stripe non l'ha mai inviata.
Tre modi di saltare il controllo finiscono tutti lì:
- Cancellarlo perché non passava.
- Intercettare l'errore e andare avanti, registrando il fallimento ed elaborando comunque l'evento. L'esempio restituisce un 400 dal suo blocco catch, ed è la forma da tenere: una richiesta che non passa il controllo si ferma lì.
- Mettere la tolleranza di tempo a 0. La guida ai webhook di Stripe dice di non farlo: 0 spegne del tutto il controllo dell'età, così un evento vero che qualcuno ha catturato può essere rimandato più tardi.
Una chiave segreta Stripe attiva nel tuo frontend è l'altro modo in cui i pagamenti vanno storti, ed è quello che possiamo vedere da fuori: ne abbiamo trovata una in 3 app su 30.998 scansionate. La nostra scansione gratuita legge il JavaScript della tua app pubblicata come farebbe un estraneo e nomina ogni chiave che trova, dicendo se è una di quelle che devono stare lì. Ci vogliono circa 20 secondi e non serve un account: scansiona la tua app.
Se un pagamento falso dovesse passare
Un controllo tolto da un prompt non si annuncia. Un builder a cui chiedi di far tornare a funzionare il webhook è libero di prendere la scorciatoia qui sopra, e la tua app continua a funzionare nel frattempo.
Reeve Monitor tiene d'occhio quello che un estraneo può vedere da fuori:
- tutti e nove i controlli ogni ora, su un massimo di tre app
- una email il giorno in cui una nuova scansione valuta la tua app peggio della precedente; una chiave segreta nel tuo JavaScript limita da sola il voto a D
- se l'app risponde, ogni 60 secondi
- un rapporto mensile di quello che ha visto
Un evento falso scrive nel tuo database come uno vero. Reeve Care tiene una copia del tuo database Supabase di prima che succedesse.
- una copia cifrata secondo una pianificazione, conservata sullo storage di Reeve nell'UE, fuori dal tuo account Supabase
- ogni copia verificata prima di contare, contando le righe di ogni tabella
- un ripristino in un clic che salva quello che c'è adesso prima di sostituirlo
- anche i tuoi file caricati, appena colleghi una credenziale di Storage
- tutto quello che fa Monitor
Che cosa fare adesso
Cosa fare
- Apri la consegna fallita nella dashboard di Stripe e la scheda Logs della tua funzione, e leggi il messaggio esatto prima di cambiare qualsiasi cosa.
- Leggi il corpo con
await req.text()e passa quella stringa intatta aconstructEventAsync. Non controllare mai un evento passato daJSON.stringify. - Copia il secret
whsec_dell'endpoint da cui Stripe invia, nella modalità giusta, e incollalo in Edge Function Secrets senza niente dopo. - Tieni
verify_jwt = falsesul webhook, e il controllo della firma come prima cosa che fa la funzione. - Restituisci un 400 quando il controllo fallisce. Non intercettare mai l'errore per elaborare comunque l'evento.
Il resto di quello che serve a una funzione che custodisce un secret è in spostare un secret in una Edge Function, e se il tuo errore è quello generico di supabase-js, leggere un codice di stato non-2xx spiega dov'è il messaggio vero.
FAQ
Che cosa vuol dire "No signatures found matching the expected signature for payload"?
La tua funzione di webhook ha calcolato la firma che si aspettava dal corpo della richiesta e dal tuo signing secret, e nessuna delle firme nell'header Stripe-Signature corrispondeva. La libreria di Stripe lancia questo errore quando il corpo che riceve non è, byte per byte, quello che Stripe ha inviato, o quando il secret non è quello dell'endpoint che ha inviato l'evento.
Perché fallisce se passo JSON.stringify dell'evento?
Perché la firma copre il testo esatto che Stripe ha inviato, spazi e a capo compresi. Leggere il corpo con req.json() e ritrasformarlo in testo con JSON.stringify ti dà gli stessi dati in un altro testo, e un altro testo ha un'altra firma. Leggi il corpo una volta con await req.text() e passa quella stringa a constructEventAsync.
Quale signing secret uso?
Il secret whsec_ dell'endpoint esatto a cui Stripe consegna. Ogni endpoint ha il suo, lo stesso indirizzo configurato in modalità test e live ha un secret diverso in ciascuna, e il comando stripe listen stampa un secret suo che vale solo per gli eventi che inoltra. La tua chiave API sk_ non è affatto un signing secret.
La mia Edge Function per il webhook Stripe deve avere verify_jwt disattivato?
Sì. Stripe non ha un token Supabase da inviare, quindi con il controllo attivo ogni consegna viene rifiutata con un 401 prima che il tuo codice parta. L'esempio Stripe di Supabase dice di fare il deploy con verify_jwt = false. Così il controllo della firma dentro la tua funzione diventa l'unico controllo che c'è, e non va mai saltato né intercettato e ignorato.
Posso togliere il controllo della firma per far funzionare i pagamenti?
Puoi, e a quel punto chiunque trovi l'indirizzo della funzione può mandarle un evento che dice che un pagamento è andato a buon fine. L'indirizzo è l'URL del tuo progetto, che sta nel tuo frontend, più il nome della funzione. Senza il controllo, la tua funzione crede a ogni richiesta che riceve e concede quello che l'evento dice sia stato pagato.