Dès qu'un SaaS encaisse de l'argent, c'est au niveau du webhook Stripe que la logique casse. Ce ne sont pas des failles techniques au sens classique, ce sont des erreurs de logique. Un scanner ne les verra jamais, mais un fraudeur, si. Voici celles que je trouve le plus souvent en audit, et comment les corriger.
Pourquoi le webhook Stripe est un point sensible
Le webhook, c'est l'endpoint que Stripe appelle pour te dire "ce client a payé". Si tu lui fais confiance au mauvais endroit, quelqu'un peut s'offrir un abonnement sans payer, ou déclencher deux fois un crédit. Tout ton business repose sur ce petit bout de code, et il est souvent écrit vite, une fois, puis plus jamais relu.
Les failles que je trouve le plus souvent
1. La signature n'est pas vérifiée
C'est la pire, et la plus fréquente. Si tu ne vérifies pas la signature, n'importe qui peut t'envoyer un faux événement "paiement réussi" et s'activer un abonnement. La signature se vérifie avec le SDK officiel, sur le corps brut de la requête, jamais après un JSON.parse. Sur Next.js App Router, lis le corps avec await req.text().
const sig = req.headers.get("stripe-signature");
const raw = await req.text(); // corps brut obligatoire
let event;
try {
event = stripe.webhooks.constructEvent(raw, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch {
return new Response("Signature invalide", { status: 400 });
}
2. L'accès accordé sur un retour ?success=true
Après le paiement, Stripe renvoie l'utilisateur sur ton site avec une URL de succès. Cette URL est côté client, donc contrôlable. Si tu débloques l'accès juste parce que tu vois ?success=true, n'importe qui peut l'ajouter à la main. L'accès ne se donne que sur un événement Stripe vérifié côté serveur.
3. Pas d'idempotence, donc double crédit
Stripe garantit une livraison au moins une fois, et retente pendant 72 heures. Le même événement peut donc arriver deux fois. Sans garde, tu crédites deux fois, ou tu actives deux fois l'abonnement. Stocke les event.id déjà traités avec une contrainte UNIQUE, et court-circuite avant toute écriture.
// avant de traiter : a-t-on déjà vu cet event ?
const nouveau = await marquerCommeTraite(event.id); // insert ... on conflict do nothing
if (!nouveau) return new Response("déjà traité", { status: 200 });
4. Le prix vient du client
La session de checkout se crée côté serveur, avec un price_id Stripe, jamais un montant envoyé par le navigateur. Sinon, on modifie le prix dans la requête et on paie ce qu'on veut. Même chose pour la quantité, la devise, le plan : tout se valide côté serveur contre une table de référence.
5. Les événements de fin ne sont pas gérés
On gère souvent bien le paiement, et on oublie la suite. Un abonnement annulé, un paiement qui échoue, un remboursement, une contestation : si tu ne retires pas l'accès dans ces cas, des gens continuent d'utiliser ton produit sans payer. Écoute customer.subscription.deleted, invoice.payment_failed, charge.refunded et charge.dispute.created.
Le point commun : ces failles ne cassent rien et ne font planter aucun test. Elles font juste perdre de l'argent, en silence, jusqu'au jour où tu regardes la compta.
Comment tester ton webhook en deux minutes
- Envoie un faux événement non signé sur ton endpoint. Il doit répondre
400. S'il répond200, n'importe qui peut s'offrir un abonnement. - Rejoue deux fois le même événement (la CLI Stripe le fait très bien). Il ne doit produire qu'un seul effet.
- Dans le checkout, essaie de modifier le montant envoyé. Il doit être ignoré au profit du
price_idserveur.
# un faux event doit être rejeté
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://ton-saas.com/api/stripe/webhook \
-H "Content-Type: application/json" \
-d '{"type":"checkout.session.completed","data":{"object":{}}}'
# attendu : 400
Tu veux être sûr que tes paiements tiennent ?
Je teste ta logique de paiement pour de vrai, pas sur le papier. Commence par un scan gratuit, ou passe direct à l'audit si tu encaisses déjà.
Scanner mon SaaS, gratuitQuestions fréquentes
Faut-il vérifier la signature des webhooks Stripe ?
Oui, c'est obligatoire, avec le SDK officiel et sur le corps brut de la requête. Sans ça, un faux événement suffit à s'offrir un abonnement.
Un webhook Stripe peut-il être rejoué ?
Oui, Stripe retente pendant 72 heures. Sans idempotence (dédup sur event.id), tu risques le double crédit ou le double abonnement.
Dans le même esprit : sécuriser la RLS Supabase.
← Retour au blog