Skip to content
SlashClone
Made by@shipcheck

Stripe webhook hardening

@shipcheck/stripe-webhook-hardening

Find your Stripe webhook handlers and harden them: verify the Stripe-Signature header, use the raw body, keep the signing secret in env, process each event once, reject bad signatures with 400, and fulfill orders from the webhook rather than the success page. Reads your code and proposes diffs; never calls Stripe.

SkillDevOpsv1.0.0 (latest)Tested
Activity
New: be the first to try it
  1. 1// Demo handler done right (Express). The secret comes from the environment.
  2. 2import express from "express";
  3. 3import Stripe from "stripe";
  4. 4import { db } from "./db.js";
  5. 5
  6. 6const env = process.env;
  7. 7const stripe = new Stripe(env.STRIPE_SECRET_KEY);
  8. 8const router = express.Router();
  9. 9
  10. 10router.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
  11. 11 let event;
  12. 12 try {
  13. 13 event = stripe.webhooks.constructEvent(req.body, req.headers["stripe-signature"], env.STRIPE_WEBHOOK_SECRET);
  14. 14 } catch (err) {
  15. 15 return res.status(400).send(`Webhook signature failed: ${err.message}`);
  16. 16 }
  17. 18 // Idempotency: the unique key on processed_events.id stops double processing.
  18. 19 const fresh = await db.query(
  19. 20 "insert into processed_events (id) values ($1) on conflict do nothing returning id",
  20. 21 [event.id],
  21. 22 );
  22. 23 if (fresh.rowCount === 0) return res.json({ received: true, duplicate: true });
  23. 25 switch (event.type) {
  24. 26 case "checkout.session.completed":
  25. 27 await db.query("update orders set status = 'paid' where stripe_session_id = $1", [event.data.object.id]);
  26. 28 break;
  27. 29 default:
  28. 30 break;
  29. 31 }
  30. 32 res.json({ received: true });
  31. 33});
  32. 35export default router;