یک چهارم وباپهای پرداخت، جعل رویداد Stripe رو قبول میکنن
خلاصهٔ کاملتر
یه تیم امنیتی اخیراً حدود ۶۰۰۰ وباپ رو با یه ماژول تست bypass پرداخت اسکن کرده. منطق تست سادهست: یه رویداد جعلی checkout.session.completed بدون هدر Stripe-Signature به مسیرهای رایج وبهوک ارسال میشه، و نگاه میکنن کدوم سرورها جواب 200 میدن. نتیجه تکاندهنده بود: ۱۵۴۲ اپ — تقریباً یک چهارم — رویداد جعلی رو پذیرفتن. هیچ احراز هویتی، هیچ بررسی امضایی، هیچ محافظتی در برابر replay نداشتن.
پیلودی که ارسال شده خیلی سادهست؛ دقیقاً همون چیزیه که مستندات Stripe توصیف میکنه، فقط بدون امضای رمزنگاریشده:
{
"id": "evt_secprobe_test",
"object": "event",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_secprobe_test",
"payment_status": "paid",
"amount_total": 100,
"customer": "cus_secprobe",
"currency": "usd"
}
},
"livemode": false
}یه رویداد واقعی Stripe با هدری مثل t=1234567890,v1=hexdigest... میاد که با یه secret محاسبه میشه. اگه سرور این هدر رو چک نکنه، هیچ راهی نداره بفهمه این رویداد از Stripe اومده یا از یه مهاجم با curl.
چرا این خطرناکه؟ چون وبهوک Stripe دقیقاً همون جاییه که اپ میفهمه کاربر پرداخت کرده. در یه SaaS معمولی، بعد از پرداخت، Stripe یه checkout.session.completed میفرسته، سرور اطلاعات کاربر رو از داخل رویداد میخونه و اکانتش رو از plan: free به plan: pro ارتقا میده. اگه این مرحله امضا رو چک نکنه، مهاجم میتونه مستقیماً این مرحله رو trigger کنه: یه اکانت رایگان میسازه، ایمیلش رو توی رویداد جعلی میذاره، POST میزنه، و بدون پرداخت pro میشه.
چرا این مشکل اینقدر رایجه؟ مستندات Stripe خوبه و کد نمونهشون شامل چک امضاست، ولی الگوی رایجی بین توسعهدهندهها وجود داره: اول یه handler ساده مینویسن که فقط رویداد رو لاگ میکنه، بعد منطق ارتقای اکانت رو اضافه میکنن، چک امضا رو میذارن رو TODO، deploy میکنن و چون همه چیز کار میکنه، اون TODO هیچوقت انجام نمیشه. همین الگو در کدهایی که با ابزارهای هوش مصنوعی تولید میشن هم دیده میشه — مدل route handler رو میسازه ولی چک امضا یه ایده جداگانهست که توسعهدهنده باید خودش بهش فکر کنه.
از نظر توزیع، این ۱۵۴۲ مورد روی پلتفرمهای مختلفی پخش شدن: دامنههای اختصاصی (~۷۲۰ مورد)، Render (198)، Vercel (142)، Replit (121)، Railway (87)، و بقیه. خطرناکترین دسته همون اپهای SaaS روی دامنه اختصاصی هستن چون کسبوکارهای واقعی با حسابهای Stripe واقعی دارن.
رفع این مشکل در Node.js با استفاده از SDK رسمی Stripe اینطوریه:
app.post('/api/webhook/stripe', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
res.json({received: true});
});سه نکته مهم در این کد: اول هدر stripe-signature رو بخونید. دوم، body خام رو پاس بدید نه JSON پارسشده — این جاییه که بیشترین اشتباه رخ میده؛ middleware پیشفرض express.json() بدنه خام رو میخوره و امضا دیگه هیچوقت match نمیشه، پس باید express.raw({type: 'application/json'}) رو فقط روی روت وبهوک ثبت کنید. سوم، webhook secret رو از داشبورد Stripe بگیرید و توی STRIPE_WEBHOOK_SECRET ذخیره کنید. در FastAPI هم باید از await request.body() استفاده کنید، نه request.json(). Paddle و LemonSqueezy هم همین الگو رو دارن.
نکات کلیدی:
- ۱۵۴۲ از ۶۰۰۰ وباپ اسکنشده (۲۵٪) رویدادهای جعلی Stripe رو بدون چک امضا قبول میکنن
- مهاجم میتونه بدون هیچ پرداختی، اکانتش رو به پلن پولی ارتقا بده
- ریشه مشکل معمولاً یه TODO فراموششده در مرحله توسعهست
- کدهای تولیدشده توسط AI هم اغلب این چک رو ندارن
- راهحل: استفاده از
stripe.webhooks.constructEvent()با body خام و secret مربوطه - در Express باید
express.raw()رو بهجایexpress.json()روی روت وبهوک ثبت کنید - همین اصل برای Paddle، LemonSqueezy و سایر پردازشگرهای پرداخت هم صدق میکنه




