Sécurité des webhooks Stripe
Cette compétence exécute la partie Stripe du scanner vibe-preflight (scripts/preflight.mjs à côté de ce fichier, Node 20+, aucune dépendance, aucun réseau). Elle trouve les gestionnaires de webhook dans les route handlers Next.js, pages/api, Express et Supabase Edge Functions et vérifie deux points dans le code. Le reste de cette page est une revue que vous effectuez en lisant le gestionnaire, car une analyse de motifs ne peut pas tout prouver.
1. Lancer le scan
Depuis la racine du projet :
node ${CLAUDE_SKILL_DIR}/scripts/preflight.mjs . --only stripe --fail-on warning --format json
Claude Code remplit ${CLAUDE_SKILL_DIR}. Autres agents : utilisez le dossier de cette compétence, par exemple .agents/skills/stripe-webhook-security ou .claude/skills/stripe-webhook-security.
| Règle | Niveau | Signification |
|---|---|---|
stripe-webhook-unverified | erreur | Le gestionnaire agit sur des événements Stripe sans jamais appeler constructEvent / constructEventAsync. Quiconque trouve l'URL peut envoyer un faux checkout.session.completed. |
stripe-webhook-parsed-body | avertissement | La signature est vérifiée, mais sur un corps déjà parsé. Stripe signe les octets exacts qu'il a envoyés, donc les vrais événements échouent avec 400 et quelqu'un finit par supprimer la vérification. |
--only stripe trouve un gestionnaire par les noms d'événements Stripe, une lecture de l'en-tête stripe-signature, ou un chemin de webhook dans un fichier qui mentionne Stripe. Si le scan indique 0 erreurs mais que l'utilisateur sait qu'il a un webhook, trouvez le gestionnaire vous-même et effectuez quand même la revue ci-dessous.
2. Corriger la vérification
Route handler (Next.js App Router, Remix, Hono et autres gestionnaires de style fetch) :
export async function POST(request: Request) {
const body = await request.text(); // corps brut, jamais request.json()
const signature = request.headers.get("stripe-signature");
if (!signature) return new Response("Signature manquante", { status: 400 });
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Signature invalide", { status: 400 });
}
// traiter l'événement
return Response.json({ received: true });
}
Express : enregistrez la route du webhook avec express.raw({ type: "application/json" }) avant app.use(express.json()), et passez req.body (un Buffer alors) à constructEvent.
Next.js pages/api : export const config = { api: { bodyParser: false } }; et lisez la requête brute dans un Buffer avant de vérifier.
Supabase Edge Functions et autres runtimes Deno / edge : await stripe.webhooks.constructEventAsync(body, signature, secret, undefined, Stripe.createSubtleCryptoProvider()).
3. Examiner le reste en lisant le gestionnaire
Le scanner ne vérifie pas ces points. Regardez le code et dites à l'utilisateur ce que vous avez trouvé pour chacun.
- Secret.
STRIPE_WEBHOOK_SECRET(whsec_...) provient des variables d'environnement serveur, jamais d'une variableNEXT_PUBLIC_/VITE_ni du code source. Les modes test et production ont des secrets différents, de même que chaque endpoint et la CLIstripe listen. - Rejeu.
constructEventrejette une signature plus ancienne que sa tolérance (300 secondes par défaut). Laissez l'argument de tolérance de côté, ou gardez-le petit. N'attrapez jamais l'erreur pour continuer. - Idempotence. Stripe peut délivrer le même événement plusieurs fois. Rendez l'écriture naturellement répétable (un upsert avec l'ID Stripe comme clé, définir un statut plutôt que d'ajouter des crédits) ou stockez
event.iddans une table avec une contrainte d'unicité et ignorez les ID déjà vus. Examinez attentivement tout ce qui ajoute : crédits, solde, stock. - Ordre. Les événements peuvent arriver dans le désordre. Pour les abonnements, récupérez l'objet actuel depuis l'API (
stripe.subscriptions.retrieve) ou comparez les horodatages au lieu de faire confiance à l'ordre des événements. - Confiance. Prenez l'utilisateur et le plan de l'événement vérifié (
client_reference_id,metadata, l'ID client stocké lors du paiement), jamais d'une chaîne de requête ou d'une seconde requête non signée. La redirection vers la page de succès ne prouve rien : accordez l'accès depuis le webhook. - Réponse. Retournez un code 2xx rapidement une fois l'événement stocké ou traité. Le travail lent va dans une file d'attente. Retournez 400 uniquement pour une signature invalide, afin que les tentatives de Stripe restent significatives.
- Liste d'événements. Abonnez l'endpoint uniquement aux événements que le code gère, et ignorez les autres types sans erreur.
4. Rapport
Listez les résultats du scanner, puis une ligne par élément de la revue : OK, corrigé, ou une question pour l'utilisateur (par exemple « des crédits sont ajoutés à chaque livraison : un événement répété devrait-il en ajouter à nouveau ? »). Relancez le scan après la correction et montrez sa dernière ligne. Pour tester en conditions réelles, suggérez stripe listen --forward-to localhost:3000/api/webhooks et stripe trigger checkout.session.completed avec la CLI Stripe.
Un faux positif confirmé peut être ignoré avec // vibe-preflight-ignore <rule-id> sur la ligne ou la ligne au-dessus, avec la raison dans le commentaire.