أمان Webhook: توقيعات HMAC، والعجز، وحماية إعادة التشغيل
تعمل ثلاثة أنماط من التعليمات البرمجية على إيقاف الحمولات المخادعة وعمليات التسليم المكررة وطلبات خطاف الويب المعاد تشغيلها. أمثلة Node.js مع HMAC-SHA256 والتحقق من الطابع الزمني.
يقبل معالج خطاف الويب الخاص بك طلب POST، ويوزع JSON، ويقوم بتشغيل منطق الأعمال. أن يعمل حتى شخص ما يرسل حمولة مزورة إلى نقطة النهاية الخاصة بك. أو حتى يقوم المزود بإعادة محاولة التسليم ويقوم المعالج الخاص بك بتحصيل رسوم أ العميل مرتين. أو حتى يسجل المهاجم طلبًا مشروعًا ويعيد تشغيله بعد ست ساعات.
تعمل ثلاث وسائل حماية على حل هذه المشكلة: التحقق من توقيع HMAC، ومفاتيح الفاعلية، ورفض إعادة التشغيل المستند إلى الطابع الزمني. يغطي هذا البرنامج التعليمي كل واحدة منها باستخدام كود Node.js العامل.
كيف يعمل توقيع webhook
يقوم كل مزود خدمة ربط الويب الرئيسي (Stripe وGitHub وShopify وTwilio) بتوقيع الحمولات الصادرة باستخدام HMAC-SHA256. ال يجمع الموفر نص الطلب الأولي مع سر مشترك، ويحسب التجزئة، ويرسل تلك التجزئة في الرأس. يقوم الخادم الخاص بك بإعادة حساب التجزئة بنفس السر ومقارنتها. إذا تطابقت التجزئات، تكون الحمولة أصلية.
تبدو صيغة التوقيع كما يلي:
HMAC-SHA256(secret, timestamp + "." + raw_body) = signature
الشريط يرسل التوقيع Stripe-Signature. يرسلها GitHub X-Hub-Signature-256.
الخوارزمية هي نفسها؛ يختلف اسم الرأس وتنسيقه فقط.
تحقق من توقيع خطاف الويب Stripe في Node.js
شريط Stripe-Signature يحتوي الرأس على طابع زمني (t=) وإصدار واحد أو أكثر
التوقيعات (v1=). إليك كيفية التحقق من ذلك بدون Stripe SDK:
const crypto = require('crypto');
function verifyStripeSignature(payload, header, secret) {
const parts = header.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).slice(2);
const signature = parts.find(p => p.startsWith('v1=')).slice(3);
const signedPayload = timestamp + '.' + payload;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
if (expected !== signature) {
throw new Error('Invalid signature');
}
// Replay protection: reject if older than 5 minutes
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
throw new Error('Timestamp too old');
}
return JSON.parse(payload);
}
التفاصيل الأساسية: يقوم الشريط بتسلسل الطابع الزمني والنص الخام بفاصل زمني. ال v1
يستخدم التوقيع HMAC-SHA256. يوفر التحقق من الطابع الزمني في النهاية حماية إعادة التشغيل (موضحة بالتفصيل أدناه).
تحقق من توقيع GitHub webhook في Node.js
تنسيق GitHub أبسط. ال X-Hub-Signature-256 يحتوي الرأس sha256= يتبع
بواسطة HMAC السداسي للجسم الخام:
const crypto = require('crypto');
function verifyGitHubSignature(payload, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const trusted = Buffer.from(expected, 'utf8');
const untrusted = Buffer.from(signatureHeader, 'utf8');
if (trusted.length !== untrusted.length) {
throw new Error('Invalid signature');
}
if (!crypto.timingSafeEqual(trusted, untrusted)) {
throw new Error('Invalid signature');
}
return JSON.parse(payload);
}
يلاحظ crypto.timingSafeEqual. معيار === مقارنة تسرب معلومات التوقيت.
يمكن للمهاجم قياس أوقات الاستجابة لتخمين بايت التوقيع الصحيح بالبايت. مقارنة زمنية ثابتة
يزيل هذا المتجه.
حساب توقيعات HMAC باستخدام واجهة برمجة التطبيقات الخاصة بـ botoi
إذا كنت بحاجة إلى حساب توقيعات HMAC-SHA256 أو التحقق منها من برنامج نصي أو خط أنابيب CI أو وظيفة بدون خادم
بدون استيراد crypto، استخدم /v1/hash/hmac نقطة النهاية:
curl -X POST https://api.botoi.com/v1/hash/hmac \\
-H "Content-Type: application/json" \\
-d '{
"text": "1712345678.{\\"event\\":\\"payment_intent.succeeded\\",\\"amount\\":4999}",
"key": "whsec_your_stripe_secret",
"algorithm": "sha256"
}'
إجابة:
{
"success": true,
"data": {
"hmac": "a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0",
"algorithm": "sha256"
}
}
يؤدي هذا إلى إرجاع HMAC المشفر بالست عشري. قارنه برأس التوقيع من موفر الويب هوك.
نقطة النهاية تدعم كليهما sha256 و sha512 خوارزميات.
قم ببناء برنامج وسيط سريع للتحقق من HMAC
قم بلف التحقق من التوقيع في برنامج وسيط قابل لإعادة الاستخدام بحيث يحصل كل مسار خطاف على الويب على الحماية دون تكرار التعليمات البرمجية:
const crypto = require('crypto');
function webhookAuth(secret) {
return (req, res, next) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
if (!signature || !timestamp) {
return res.status(401).json({ error: 'Missing signature headers' });
}
// Step 1: Replay protection
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
return res.status(401).json({ error: 'Request too old' });
}
// Step 2: HMAC verification
const payload = JSON.stringify(req.body);
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + payload)
.digest('hex');
const trusted = Buffer.from(expected, 'utf8');
const untrusted = Buffer.from(signature, 'utf8');
if (trusted.length !== untrusted.length ||
!crypto.timingSafeEqual(trusted, untrusted)) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
};
}
// Mount on your webhook route
app.post('/webhooks/stripe', webhookAuth(process.env.STRIPE_WEBHOOK_SECRET), handler);
ترفض هذه البرامج الوسيطة الطلبات ذات الرؤوس المفقودة أو الطوابع الزمنية التي لا معنى لها أو التوقيعات غير الصالحة قبل يعمل المعالج. تتطابق نافذة الـ 300 ثانية مع التسامح الافتراضي لـ Stripe.
العجز: تخطي عمليات التسليم المكررة
يقوم موفرو Webhook بإعادة محاولة عمليات التسليم الفاشلة. إذا أعاد خادمك 500 بعد معالجة الحدث ولكن قبل ذلك بإرسال الرد 200، يرسل الموفر نفس الحدث مرة أخرى. بدون العجز الجنسي، يقوم المعالج الخاص بك بتشغيل الآثار الجانبية مرتين: الرسوم المزدوجة، ورسائل البريد الإلكتروني المكررة، وتحديثات المخزون المتكررة.
الحل: قم بتخزين كل معرف حدث تمت معالجته وتخطي الأحداث التي شاهدتها من قبل.
العجز في الذاكرة (التنمية)
const processedEvents = new Map(); // Use Redis in production
async function handleWebhook(req, res) {
const eventId = req.body.id;
// Check if already processed
if (processedEvents.has(eventId)) {
return res.status(200).json({ status: 'duplicate', eventId });
}
// Mark as processing before side effects
processedEvents.set(eventId, Date.now());
try {
await processEvent(req.body);
return res.status(200).json({ status: 'processed', eventId });
} catch (err) {
// Remove on failure so retries work
processedEvents.delete(eventId);
return res.status(500).json({ error: 'Processing failed' });
}
}
ال Map يعمل من أجل التنمية المحلية. في الإنتاج، استخدم مخزنًا مشتركًا لجميع مثيلات الخادم
رؤية نفس المجموعة من الأحداث التي تمت معالجتها.
العجز المدعوم من Redis (الإنتاج)
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function handleWebhook(req, res) {
const eventId = req.body.id;
const lockKey = \`webhook:processed:\${eventId}\
SET NX ذرية: عملية واحدة فقط هي التي تفوز بالقفل. TTL لمدة 72 ساعة (259200 ثواني)
تقليم الإدخالات القديمة تلقائيًا. في حالة فشل المعالجة، يقوم المعالج بحذف المفتاح حتى تنجح عملية إعادة المحاولة التالية.
إعادة الحماية: رفض الطلبات التي لا معنى لها
يؤكد التحقق من HMAC أن الحمولة جاءت من الموفر. لكن طلبًا موقعًا صالحًا تم التقاطه بواسطة أ يظل مهاجم الشبكة صالحًا إلى الأبد ما لم تقم بإضافة التحقق من الوقت. إعادة الحماية ترفض الحمولات مع الطوابع الزمنية الأقدم من العتبة.
function rejectStaleRequests(req, res, next) {
const timestamp = parseInt(req.headers['x-webhook-timestamp'], 10);
if (isNaN(timestamp)) {
return res.status(401).json({ error: 'Missing timestamp' });
}
const now = Math.floor(Date.now() / 1000);
const age = now - timestamp;
// Reject anything older than 5 minutes or from the future
if (age > 300 || age < -30) {
return res.status(401).json({ error: 'Timestamp out of range' });
}
next();
}
يتعامل التسامح المستقبلي لمدة 30 ثانية مع الانجراف البسيط على مدار الساعة بين الخادم الخاص بك والمزود. أي شيء يتم رفض الطلب الأقدم من 5 دقائق أو أكثر من 30 ثانية في المستقبل.
الجمع بين الثلاثة في وسيطة واحدة
إليك البرنامج الوسيط الكامل الذي يقوم بتشغيل جميع عمليات التحقق الثلاثة بالتسلسل: الطابع الزمني، وHMAC، ثم الخلل.
const crypto = require('crypto');
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
function secureWebhook(secret) {
return async (req, res, next) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const eventId = req.body?.id;
// 1. Reject missing headers
if (!signature || !timestamp || !eventId) {
return res.status(401).json({ error: 'Missing required headers or event ID' });
}
// 2. Replay protection: reject stale requests
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300 || age < -30) {
return res.status(401).json({ error: 'Timestamp out of range' });
}
// 3. HMAC verification
const payload = JSON.stringify(req.body);
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + payload)
.digest('hex');
const trusted = Buffer.from(expected, 'utf8');
const untrusted = Buffer.from(signature, 'utf8');
if (trusted.length !== untrusted.length ||
!crypto.timingSafeEqual(trusted, untrusted)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 4. Idempotency: skip duplicates
const lockKey = \`webhook:processed:\${eventId}\
الترتيب مهم. الطابع الزمني هو أرخص فحص (بدون إدخال/إخراج)، لذا فهو يعمل أولاً. التحقق من HMAC هو مرتبط بوحدة المعالجة المركزية ولكنه لا يزال سريعًا. يتطلب العجز استدعاء Redis، لذلك يتم تشغيله أخيرًا. هذا التسلسل يرفض أكبر عدد من الطلبات بأقل تكلفة.
اختبر معالج webhook الخاص بك باستخدام صندوق الوارد الخاص بـ webhook الخاص بـ botoi
استخدم الأزرار /v1/webhook/inbox لإنشاء عنوان URL يمكن التخلص منه، قم بحساب توقيع HMAC باستخدامه
/v1/hash/hmac، وإرسال حمولة اختبار موقعة. لا توجد أنفاق ولا خادم محلي.
# 1. Create a webhook inbox
INBOX=\$(curl -s -X POST https://api.botoi.com/v1/webhook/inbox/create)
INBOX_ID=\$(echo \$INBOX | jq -r '.data.inbox_id')
INBOX_URL=\$(echo \$INBOX | jq -r '.data.url')
echo "Inbox URL: \$INBOX_URL"
# 2. Compute an HMAC signature for your test payload
PAYLOAD='{"id":"evt_001","event":"order.created","amount":2999}'
TIMESTAMP=\$(date +%s)
SIGNED_PAYLOAD="\$TIMESTAMP.\$PAYLOAD"
HMAC=\$(curl -s -X POST https://api.botoi.com/v1/hash/hmac \\
-H "Content-Type: application/json" \\
-d "{
\\"text\\": \\"\$SIGNED_PAYLOAD\\",
\\"key\\": \\"test_secret_key\\",
\\"algorithm\\": \\"sha256\\"
}" | jq -r '.data.hmac')
echo "HMAC: \$HMAC"
# 3. Send the signed payload to your inbox
curl -s -X POST "\$INBOX_URL" \\
-H "Content-Type: application/json" \\
-H "X-Webhook-Signature: \$HMAC" \\
-H "X-Webhook-Timestamp: \$TIMESTAMP" \\
-d "\$PAYLOAD"
# 4. Retrieve and inspect the payload
curl -s -X POST "https://api.botoi.com/v1/webhook/inbox/\$INBOX_ID/list" | jq '.data.payloads'
تقوم نقطة نهاية القائمة بإرجاع كافة الحمولات المستلمة:
[
{
"id": "p_abc123",
"received_at": "2026-04-05T14:30:00Z",
"body": {
"id": "evt_001",
"event": "order.created",
"amount": 2999
}
}
]
يتيح لك سير العمل هذا اختبار تدفق التوقيع الكامل من سطر الأوامر. قم بتبديل عنوان URL للبريد الوارد الخاص بك نقطة نهاية الإنتاج لتشغيل اختبارات التكامل في CI.
كيف يقوم Stripe وGitHub وShopify بالتوقيع على خطافات الويب
| مزود | رأس | خوارزمية | تنسيق الحمولة الموقعة | تم تضمين الطابع الزمني |
|---|---|---|---|---|
| شريط | Stripe-Signature |
هماك-SHA256 | t=UNIX.v1=HEX |
نعم |
| جيثب | X-Hub-Signature-256 |
هماك-SHA256 | sha256=HEX |
لا |
| شوبيفي | X-Shopify-Hmac-Sha256 |
هماك-SHA256 | ترميز Base64 | لا |
| تويليو | X-Twilio-Signature |
HMAC-SHA1 | URL + المعلمات المصنفة | لا |
Stripe هو الوحيد الذي يجمع طابعًا زمنيًا مع التوقيع، مما يمنحك حماية إعادة التشغيل خارج الصندوق. بالنسبة إلى GitHub وShopify، أضف رأس الطابع الزمني الخاص بك أو تحقق من وقت إنشاء الحدث من نص الحمولة.
قائمة مرجعية قبل الذهاب إلى الإنتاج
- قم بتخزين سر خطاف الويب في متغير البيئة، وليس في التعليمات البرمجية المصدر.
- قم بتحليل نص الطلب الأولي، وليس الإصدار الذي تم تحليله بواسطة JSON. يوقع HMAC البايتات الدقيقة على السلك.
- يستخدم
crypto.timingSafeEqualلمقارنة التوقيع لا تستخدم أبدا===. - قم بإرجاع 200 قبل تشغيل المعالجة البطيئة. طابور العمل والاعتراف بالتسليم.
- تسجيل الطلبات المرفوضة (توقيع غير صالح، طابع زمني قديم، مكرر) لتصحيح الأخطاء والتنبيه.
- قم بتعيين TTL على متجر العجز الجنسي الخاص بك. تغطي 72 ساعة معظم نوافذ إعادة محاولة الموفر.
- قم بالاختبار باستخدام صندوق بريد وارد يمكن التخلص منه وتوقيعات HMAC المحسوبة قبل الاتصال بمزود مباشر.
FAQ
- لماذا نستخدم HMAC-SHA256 بدلاً من السر المشترك في معلمة استعلام؟
- تنتقل معلمة الاستعلام بنص عادي عبر الوكلاء وسجلات الوصول. يقوم HMAC-SHA256 بتوقيع نص الطلب بالكامل باستخدام السر، لذلك لا يزال المهاجم الذي يعترض عنوان URL غير قادر على تزوير توقيع صالح.
- ما المدة التي يجب أن أحتفظ فيها بمعرفات الأحداث التي تمت معالجتها بسبب العجز الجنسي؟
- احتفظ بها لمدة 24 إلى 72 ساعة على الأقل. يقوم معظم موفري خدمة الرد التلقائي على الويب بإعادة المحاولة ضمن تلك النافذة. بعد 72 ساعة، يمكنك إزالة المعرفات القديمة من متجرك بأمان.
- ما هو التسامح مع الطابع الزمني الذي يجب أن أستخدمه لإعادة الحماية؟
- خمس دقائق (300 ثانية) هي المعيار. يستخدم الشريط 300 ثانية. قد تؤدي النوافذ الأقصر إلى رفض عمليات التسليم المشروعة التي تتأخر بسبب ازدحام الشبكة.
- هل يمكنني استخدام نقطة نهاية botoi hash/hmac للتحقق من خطافات الويب الواردة؟
- نعم. انشر نص الحمولة وسرك إلى /v1/hash/hmac باستخدام الخوارزمية sha256. قارن HMAC الذي تم إرجاعه برأس التوقيع من موفر webhook.
- هل أحتاج إلى وسائل الحماية الثلاثة أم يمكنني اختيار واحدة؟
- التحقق من HMAC هو الحد الأدنى. أضف العجز إذا أدى معالج webhook الخاص بك إلى حدوث آثار جانبية مثل الرسوم أو رسائل البريد الإلكتروني. قم بإضافة حماية إعادة التشغيل إذا كان نظامك يتعامل مع المعاملات المالية أو الأحداث الحساسة للأمان.
ابدأ البناء مع botoi
أكثر من 150 نقطة نهاية API للبحث ومعالجة النصوص وتوليد الصور وأدوات المطورين. باقة مجانية، بدون بطاقة ائتمان.