توقيعات ويب هوك تحقق من المحتوى ووقف الطلبات المزيفة
تعرف كيف تعمل علامات ويب هوك HMAC-SHA256، وكيفية تطبيق التحقق بوقت ثابت في Python وNode.js وPHP، والطريقة الشائعة التي تؤدي إلى فشل سري في البيئة الإنتاجية.
يمكن لأي شخص إرسال طلب POST إلى نقطة الاتصال الخاصة بك. دون التحقق من التوقيع، لا يمكن لخادمك أن يعلم ما إذا كان الطلب قد جاء من ستريب أو جيثوب أو من البنية التحتية الخاصة بك، أو من مهاجم يعيد إرسال حدث قانوني.
يُحلل هذا التحقق من التوقيع. إنه الطريقة التي تسمح بها مُعالجات الدفع، وبيئات التحكم في الإصدارات، ونظامي التجارة الإلكترونية بتأكيد صحة البيانات قبل التصرف عليها. يشرح هذا المقال كيفية عمل توقيعات HMAC-SHA256، ونمط التحقق خطوة بخطوة، والطريقة الخفيفة التي تؤدي إلى فشل التحقق في البيئة الحقيقية.
لماذا تشكل الاتصالات غير المُصادقة خطرًا أمنيًا؟
نقطة الاتصال الخاصة بالـ webhook هي مجرد معالج HTTP مُعرض للإنترنت. بدون التحقق، يتم معالجة أي طلب يصل إليها. تبرز نوعان من الهجمات:
- الإحتيال — يُصمم مهاجم طلبًا مزيفًا يشبه حدثًا قانونيًا (مثلاً: دفع ناجح أو تجديد اشتراك)، ويُحفّز التصرف على جانبك دون حدث حقيقي.
- هجمات التكرار — يتم سجّل طلب قانوني أثناء نقله ثم إعادة إرساله لاحقًا. إذا كانت نقطة الاتصال مُعادلة ولكن غير محمية، فإن نفس الحدث يُحفّز عدة مرات.
يُمنع كلا الهجمات باستخدام سرّ مشترك مع توقيع كريبتوجرافي. يُوقع المرسل على المحتوى، ويُتحقق من التوقيع قبل أي معالجة.
كيف تعمل توقيعات HMAC-SHA256؟
يأخذ HMAC (رمز التحقق المبني على التجزئة) مدخلين: سرّ مفتاح السرّ واختر محتوى الرسالة. يمرّهما عبر دالة التجزئة — SHA-256 في معظم تطبيقات الـ webhook — ويُنتج توقيعًا من الطول الثابت.
الخاصية الأساسية: يُنتج نفس السرّ + المحتوى دائمًا توقيعًا متماثلًا، وعند تغيير أي بيت واحد في المحتوى، يُنتج مخرجًا مختلفًا تمامًا. لا يمكن لأي شخص بدون مفتاح السرّ إنتاج توقيع صحيح، حتى لو كان يمكنه رؤية المحتوى بالكامل.
في الممارسة، يُظهر التدفق كالتالي:
- تُسجل نقطة الاتصال الخاصة بك مع الخدمة (مثلاً ستريب). تُعطي الخدمة لك سرّ التوقيع.
- عند إرسال الحدث من قبل الخدمة، تُحسب
HMAC-SHA256(secret, payload)وتنضم النتيجة إلى رأس الطلب. - تُستلم طلب من جانبك، وتُحسب نفس HMAC باستخدام السرّ المخزّن والجسم الأصلي للطلب، ثم تُقارن التوقيعات.
- إذا كانت متطابقة، فإن الطلب موثوق. وإذا لم تكن متطابقة، يتم رفضه.
نمط التحقق خطوة بخطوة
إليك تطبيقًا بالبيتوني يُماثل ما تتوقعه كل خدمة رئيسية:
import hmac
import hashlib
def verify_signature(payload: bytes, secret: str, received_sig: str) -> bool:
expected = hmac.new(
key=secret.encode("utf-8"),
msg=payload,
digestmod=hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received_sig)
إليك شيئين مهمين هنا بخلاف مكالمة HMAC نفسها. أولاً: payload يكون bytes، وليس سلسلة مُفككة — يجب أن تمرّ محتوى الطلب الأصلي كما وصل من الشبكة. ثانيًا: يستخدم المقارنة hmac.compare_digest بدلاً من ==. هذا مقصود.
لماذا لا يمكن تجاهل مقارنة زمنية ثابتة؟
في معظم اللغات، تُختصر مقارنة السلاسل: تُرجع false عندما تجد تباينًا في الحرف. يمكن للمهاجم الذي يمكنه قياس وقت الاستجابة أن يستفيد من ذلك — إرسال آلاف الطلبات مع توقيعات مختلفة واستخدام الفروق الزمنية لاستكشاف القيمة الصحيحة حرفًا بحرف. هذا هو هجوم على الزمن.
hmac.compare_digest في بايثون، hash_equals في بايثون، crypto.timingSafeEqual في نود.js جميعها تستغرق نفس المدة بغض النظر عن مكان التباين بين السلاسل. استخدمها كل مرة تُقارن فيها توقيع — بدون استثناء.
تنسيقات الرأس في البيئات الحقيقية: ستريب، جيثوب، شوببي
تختلف كل خدمة في اسم الرأس والشكل المُستخدم للتوقيع. إليك ما يجب تحليله للثلاثة تكاملات الشائعة.
Stripe
تُرسل ستريب رأسًا Stripe-Signature بشكل يحتوي على هذا التنسيق:
Stripe-Signature: t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a05bd539e6d5b9d2a2d2fbe
ال t الحقل هو وقت التوقيت Unix عندما تم إرسال الحدث. القيمة هي التوقيع HMAC-SHA256 لـ v1 عند تطبيقه يدويًا: timestamp + "." + raw_bodyاستخرج الرأس لاستخراج
- احسب
tوv1. - قارن مع
HMAC-SHA256(secret, t + "." + raw_body). - باستخدام مقارنة زمنية ثابتة.
v1أرفض إذا كان - أكبر من 300 ثانية (5 دقائق) عن الوقت الحالي.
tيستخدم جيثوب الرأس
جيثب
أزل X-Hub-Signature-256 الرأس:
X-Hub-Signature-256: sha256=6ffbb59b2300aae63f272406069a9788598b770f698a48021f99b32f8de06bb3
السَّيْر، ثم قارن الباقي مع توقيعك المُحسب للجسم الأصلي. لا يحتوي جيثوب على توقيت في محتوى التوقيع، لذا يجب تطبيق تكرار الحدث بشكل منفصل إذا كان التأمين من التكرار مهمًا لك. sha256= يستخدم شوببي
Shopify
مع تشفير المُستخلص بالكود المُستخدم بدلاً من التشفير بالهكس: X-Shopify-Hmac-Sha256 أعد تشفير التوقيع المُحسب والقيمة المذكورة في الرأس من الكود المُستخدم إلى أحرف مُباشرة، ثم قارنها باستخدام دالة زمنية ثابتة. مقارنة السلاسل المُستخلصة مباشرة يمكن أن تؤدي إلى أخطاء خفيفة في التطبيع.
X-Shopify-Hmac-Sha256: b6LPiZidmXnJQf0Ff/p7MZQPIBPN9TqAqLMgAfn2YLQ=
الخطأ الأكثر شيوعًا: المحتوى الأصلي مقابل المحتوى المُحلّل
هنا تحدث أغلب حالات فشل التحقق من توقيع الـ webhook في البيئة الحقيقية. عندما تُحلّل الإطار التلقائي للجسم المُستلم إلى قاموس أو كائن قبل تشغيل المُعالج، يختفي السلاسل الأصلية. تم حساب التوقيع على هذه السلاسل الأصلية — وليس على أي شيء يُعيد ترميزه المُحلّل عند إعادة ترميز الهيكل.
حتى لو كان المحتوى متماثلًا من حيث المعنى،
(بدون فراغ بعد النقطة) تُنتج قيم توقيع مختلفة. يمكن أن تؤدي تباينات ترتيب الحقول، التطبيع الـ Unicode، ودقة الأعداد العشرية إلى تدمير التحقق دون أي خطأ واضح. {"amount": 100} و {"amount":100} الحل:
أوقف السلاسل الأصلية قبل أي تحليل ومرّر هذه السلاسل إلى دالة التحقق. في إكسبريس جي، قم بتسجيل المسار باستخدام مُعالج بدلاً من express.raw() (السلاسل). في لارافيل، استخدم express.json():
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body; // Buffer — not a parsed object
const sig = req.headers['stripe-signature'];
// Verify rawBody against sig before JSON.parse()
});
في داينو، استخدم request.body . يبقى النمط متماسكًا: احصل على السلاسل الأصلية مباشرة من الطلب، ولا تُعيد ترميز أي تمثيل مُحلّل. $request->getContent()الحماية من التكرار باستخدام التوقيت
يُثبت التوقيع فقط أن المحتوى لم يُعدل أثناء النقل. لا يمنع مهاجم من سجّل طلب قانوني ثم إعادة إرساله بعد ساعات — لأن التوقيع سيتحقق بشكل صحيح لأن لا شيء تغير.
تُعالج ستريب هذا الأمر من خلال إدراج توقيت Unix في المحتوى المُوقع، وتوصي بفترة مقبولة بـ 5 دقائق. بعد استخراج
من الرأس t أرفض الطلبات التي تقع خارج هذه الفترة: Stripe-Signature للمؤسسات مثل جيثوب وشوببي التي لا تُدرج توقيتًا في نمط التوقيع، قم بتطبيق حماية من التكرار من خلال تخزين هوية الحدث (التي تُوجد في المحتوى) ورفض أي هوية تم معالجتها مسبقًا. يُستخدم مخزن قصير المدة أو مجموعة ريديس مع فترة تجديد تطابق فترة المعالجة.
import time
def is_within_tolerance(timestamp: int, tolerance_seconds: int = 300) -> bool:
return abs(time.time() - timestamp) <= tolerance_seconds
# In your handler:
if not is_within_tolerance(stripe_timestamp):
return HttpResponse(status=403) # Reject stale request
التحقق من التوقيع دون كتابة أي كود
عند تدقيق تكامل الـ webhook — أو مراجعة محتوى تم استلامه مسبقًا — يُمكنك استخدام
لإدخال محتوى، سرّ، وتوقيع مُستلم لفحص التحقق فورًا، دون الحاجة لتشغيل بيئة محلية. مدقق توقيع Webhook لإنشاء توقيعات HMAC لاختبار نقطة الاتصال مع محتوى مُصمم مسبقًا، استخدم
يُنتج نتائج بالهكس والكود المُستخدم لدالة التجزئة SHA-256، SHA-512، وعدد من الخوارزميات الأخرى — مفيدة لبناء حالات اختبار تغطي التنسيقات المستخدمة من قبل كل خدمة. مولد هماكيُعد توقيع HMAC-SHA256 مع سرّ مشترك المعيار المُستخدم في ستريب، جيثوب، شوببي، ومعظم الخدمات الأخرى.
الرجوع السريع
- استخدم دائمًا مقارنة زمنية ثابتة (
- ) — وليس
hmac.compare_digest,hash_equals,crypto.timingSafeEqualمرّر السلاسل الأصلية للطلب إلى دالة HMAC، ولا تمرّ بتمثيل مُعاد ترميزه.==. - تحقق من التوقيت عند الخدمات التي تحتوي عليه؛ فترة التسامح في ستريب هي 5 دقائق.
- أعد تكرار الحدث من خلال هوية الحدث للخدمات التي لا تحتوي على حماية من التكرار بناءً على التوقيت.
- يختلف اسم الرأس والشكل (الهكس مقابل الكود المُستخدم) حسب الخدمة — يجب دائمًا التحقق من وثائق التكامل.
- اسم الرأس وطريقة التشفير (السادس مقابل Base64) يختلف بين الخدمات — تحقق دائمًا من وثائق التكامل.
قد يعجبك أيضاً
تثبيت ملحقاتنا
أضف أدوات IO إلى متصفحك المفضل للوصول الفوري والبحث بشكل أسرع
恵 وصلت لوحة النتائج!
لوحة النتائج هي طريقة ممتعة لتتبع ألعابك، يتم تخزين جميع البيانات في متصفحك. المزيد من الميزات قريبا!
