إشعاراتنا الفورية كانت تُرسل إلى أي شخص: كيف أنقذني ‘توقيع الـ Webhook’ من كارثة محققة؟

يا جماعة الخير، السلام عليكم ورحمة الله.

خلوني أحكيلكم هالشغلة اللي صارت معي قبل كم سنة، شغلة علّمتني درس ما بنساه طول عمري في عالم البرمجة. كنا وقتها شغالين على نظام إدارة متجر إلكتروني، وكان جزء أساسي من النظام هو التكامل مع بوابة دفع إلكترونية. الفكرة بسيطة: لما الزبون يشتري ويدفع، بوابة الدفع بتبعتلنا “إشعار فوري” (Webhook) على رابط معين (Endpoint) في نظامنا، عشان نحدّث حالة الطلب ونبدأ عملية الشحن.

الأمور كانت ماشية زي الحلاوة في مرحلة التطوير. الإشعارات بتوصل، الطلبات بتتحدث، والكل مبسوط. طلعنا بالنظام للبيئة الحقيقية (Production)، وفي أول يومين، كل شي كان تمام. في اليوم الثالث، وأنا قاعد بشرب فنجان القهوة الصباحي وبتصفح لوحة التحكم، لاحظت إشي غريب… طلبات جديدة قاعدة بتظهر، بس بمعلومات دفع غريبة، وأحياناً بدون معلومات دفع أصلاً! ولّعت الدنيا عندي، وصرت أحكي لحالي: “يا زلمة شو هاد؟ من وين بتيجي هاي الطلبات؟”.

بعد شوية حفر وتحليل للسجلات (Logs)، اكتشفت الكارثة. الرابط تبع الـ Webhook كان مكشوفاً، وأي شخص عنده شوية خبرة تقنية بيقدر يبعت طلب POST بسيط على هاد الرابط و”يخترع” طلب شراء جديد في نظامنا. كان ممكن مخرب يغرقنا بآلاف الطلبات الوهمية، أو أسوأ من هيك، كان ممكن منافس يستخدم هاي الثغرة عشان يعطل نظامنا بالكامل. شعرت للحظة إن كل تعبنا راح يروح عالفاضي. وهنا بدأت رحلتي الحقيقية مع مفهوم “توقيع الويب هوك”، المفهوم اللي أنقذ الموقف.

ما هي الـ Webhooks أصلاً؟ (وليه بنحبها هالقد؟)

قبل ما نغوص في الحل، خلينا نرجع خطوة للوراء ونفهم شو هي الـ Webhooks. تخيل معي سيناريوهين:

  1. طريقة “السؤال المستمر” (Polling): تطبيقك كل دقيقة بروح يسأل تطبيق تاني (زي بوابة الدفع): “هل في إشي جديد؟ هل في دفعة جديدة؟ هل صار إشي؟”. هاي الطريقة مرهقة، بتستهلك موارد، وبكون فيها تأخير دايماً. زي اللي كل شوي بفتح الباب يتأكد إذا في حدا إجا أو لأ.
  2. طريقة “جرس الباب” (Webhooks): تطبيقك بعطي للتطبيق التاني عنوانه (URL)، وبقله: “لما يصير عندك إشي جديد ومهم، رنلي على هاد الجرس”. ولما الحدث يصير فعلاً (زي عملية دفع ناجحة)، التطبيق التاني هو اللي بيبادر وببعتلك كل المعلومات فوراً. هاي هي الـ Webhook.

الـ Webhooks ممتازة لأنها فورية وفعّالة. بتستخدمها خدمات كثيرة زي Stripe للمدفوعات، و GitHub لتنبيهات الأكواد، و Slack لرسائل الفرق، وغيرها كثير. هي اللي بتخلي التطبيقات الحديثة تتكلم مع بعضها بسلاسة.

الكارثة الصامتة: عندما يصبح “جرس الباب” نقطة ضعف

المشكلة اللي واجهتها هي الوجه المظلم للـ Webhooks. “جرس الباب” تبعي (الـ Endpoint URL) كان عاماً. أي حدا بيعرف العنوان بيقدر ييجي ويرن، ويدّعي إنه هو الشخص الصح.

بالمصطلحات التقنية، أي شخص على الإنترنت يمكنه إرسال طلب HTTP POST إلى عنوان الـ Webhook الخاص بك. إذا كان تطبيقك يثق في كل طلب يصل إليه دون تحقق، فأنت تفتح الباب على مصراعيه لمجموعة من المخاطر:

  • الانتحال والتزييف (Spoofing): يمكن للمهاجم إرسال بيانات مزيفة. تخيل أن نظامك يستقبل إشعارات دفع وهمية ويقوم بشحن بضائع حقيقية بناءً عليها. خسارة مالية مباشرة.
  • هجمات حجب الخدمة (Denial of Service): يمكن للمهاجم إغراق الخادم الخاص بك بآلاف الطلبات الوهمية في الثانية، مما يؤدي إلى استهلاك كل موارد الخادم (CPU, Memory) وتعطيل الخدمة عن المستخدمين الحقيقيين.
  • تلف البيانات (Data Corruption): إذا كانت هذه الطلبات المزيفة تؤدي إلى كتابة بيانات في قاعدة البيانات الخاصة بك، فقد ينتهي بك الأمر بقاعدة بيانات مليئة بالمعلومات الخاطئة والفوضوية التي يصعب تنظيفها.

باختصار، الـ Webhook بدون حماية أمنية هو بمثابة باب بيت مفتوح على الشارع وبدون أي أقفال.

الحل السحري: “توقيع الـ Webhook” أو الـ Webhook Signature

هنا يأتي دور البطل في قصتنا: التوقيع الرقمي. الفكرة عبقرية في بساطتها. بدل ما نعتمد فقط على أن الطلب وصل، سنتحقق من “هوية” المرسل. هذا لا يعني أن المرسل يوقع بقلم، بل يستخدم عملية تشفير تُعرف بـ HMAC (Hash-based Message Authentication Code).

كيف يعمل التوقيع بالضبط؟ (خلّينا نبسّطها)

العملية بتتم على عدة خطوات بين الطرفين: المرسل (مثلاً Stripe) والمستقبل (تطبيقنا).

  1. المفتاح السري المشترك (The Shared Secret): قبل أي شي، أنت كمطور بتقوم بإنشاء “مفتاح سري” (Secret Key) في لوحة تحكم الخدمة المرسلة (مثلاً Stripe). هذا المفتاح هو عبارة عن سلسلة نصية طويلة وعشوائية، بتعرفها أنت والخدمة فقط. أهم نقطة: هذا المفتاح لا يتم إرساله أبداً مع الطلب عبر الإنترنت.
  2. من جهة المرسل (Stripe):
    • قبل إرسال الـ Webhook، يأخذ المرسل محتوى الطلب كاملاً (الـ Payload).
    • يستخدم “المفتاح السري” مع خوارزمية تشفير (مثل SHA-256) لإنشاء “توقيع” فريد لهذا المحتوى تحديداً. هذا التوقيع هو الـ HMAC.
    • يقوم المرسل بإرسال الطلب الأصلي (Payload) وفي نفس الوقت يضيف هذا التوقيع في أحد ترويسات الطلب (Request Headers)، مثلاً في ترويسة اسمها Stripe-Signature.
  3. من جهة المستقبل (تطبيقنا):
    • عندما نستقبل الطلب، نقوم بعزل جزأين: المحتوى (Payload) والتوقيع الموجود في الترويسة.
    • نأخذ المحتوى الخام (Raw Body) كما وصلنا بالضبط.
    • نستخدم نفس “المفتاح السري” اللي خزّناه بشكل آمن عندنا (مثلاً في متغيرات البيئة).
    • نقوم بتطبيق نفس خوارزمية التشفير (SHA-256) على المحتوى لإنشاء توقيع خاص بنا.
    • لحظة الحقيقة: نقارن التوقيع الذي قمنا بإنشائه مع التوقيع الذي وصل إلينا في الترويسة.
      • ✅ إذا تطابق التوقيعان: “أصلي ومضمون!”. هذا يعني أن الطلب جاء من المصدر الحقيقي (Stripe)، وأن محتواه لم يتم التلاعب به في الطريق. نقبل الطلب ونقوم بمعالجته.
      • ❌ إذا لم يتطابق التوقيعان: هذا طلب مزيف أو تم التلاعب به. نرفضه فوراً (نرجع استجابة 401 Unauthorized أو 403 Forbidden) ونتجاهله تماماً.

بهذه الطريقة، حتى لو عرف المهاجم عنوان الـ Webhook الخاص بنا، وحتى لو حاول إرسال طلب بنفس شكل الطلبات الحقيقية، لن يتمكن أبداً من إنشاء التوقيع الصحيح لأنه لا يملك “المفتاح السري”.

يلا نكتب كود: تطبيق التحقق من التوقيع عملياً

الكلام النظري جميل، لكن خلينا نشوف كيف ممكن نطبق هذا الكلام في كود حقيقي. سأستخدم مثالاً شائعاً باستخدام Node.js وإطار العمل Express.

مثال باستخدام Node.js و Express

أولاً، يجب أن نتأكد من أن Express لا يقوم بتحليل محتوى الطلب القادم كـ JSON تلقائياً، لأن عملية التوقيع تتطلب المحتوى الخام (Raw Body) كما هو. أي تغيير، حتى لو مسافة بيضاء، سيفشل عملية التحقق.


// server.js
const express = require('express');
const crypto = require('crypto');

const app = express();

// هذا هو المفتاح السري الذي تحصل عليه من مزود الخدمة
// يجب تخزينه في متغيرات البيئة (Environment Variables) وليس في الكود مباشرة
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;

// Middleware للتحقق من التوقيع
const verifyWebhookSignature = (req, res, next) => {
  // 1. احصل على التوقيع من ترويسة الطلب
  const signature = req.headers['stripe-signature'];

  if (!signature) {
    console.error("Signature missing!");
    return res.status(400).send('Webhook Error: Signature missing.');
  }

  try {
    // 2. قم بإنشاء التوقيع الخاص بك باستخدام المحتوى الخام والمفتاح السري
    const expectedSignature = crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(req.rawBody) // نستخدم المحتوى الخام هنا
      .digest('hex');

    // Stripe يضيف معلومات إضافية للتوقيع (timestamp, version)
    // لذا نحتاج إلى استخلاص التوقيع الفعلي للمقارنة
    // (هذه الخطوة تعتمد على مزود الخدمة، اقرأ التوثيق الخاص بهم)
    const sigDetails = signature.split(',').reduce((acc, curr) => {
        const [key, value] = curr.split('=');
        acc[key] = value;
        return acc;
    }, {});
    
    const stripeSignature = sigDetails.v1;

    // 3. قارن التوقيعين بطريقة آمنة (لمنع هجمات التوقيت)
    const signaturesMatch = crypto.timingSafeEqual(
      Buffer.from(stripeSignature, 'hex'),
      Buffer.from(expectedSignature, 'hex')
    );

    if (signaturesMatch) {
      console.log("Webhook signature verified successfully! 👍");
      next(); // التوقيع صحيح، اسمح للطلب بالمرور
    } else {
      console.warn("Invalid signature. Request rejected.");
      return res.status(400).send('Webhook Error: Invalid signature.');
    }
  } catch (error) {
    console.error("Error verifying signature:", error);
    return res.status(500).send('Webhook Error: Verification failed.');
  }
};

// نقطة النهاية (Endpoint) الخاصة بالويب هوك
// لاحظ أننا نستخدم express.raw للحصول على المحتوى الخام
app.post(
  '/stripe-webhook',
  express.raw({ type: 'application/json', verify: (req, res, buf) => { req.rawBody = buf; } }),
  verifyWebhookSignature,
  (req, res) => {
    // إذا وصل الطلب إلى هنا، فهذا يعني أنه موثوق 100%
    const event = JSON.parse(req.rawBody.toString());

    // قم بمعالجة الحدث هنا...
    switch (event.type) {
      case 'payment_intent.succeeded':
        console.log('Payment was successful for:', event.data.object.id);
        // ... قم بتحديث قاعدة البيانات، إرسال إيميل، إلخ
        break;
      case 'customer.subscription.created':
        console.log('New subscription created!');
        // ...
        break;
      // ... عالج باقي أنواع الأحداث
    }

    res.status(200).json({ received: true });
  }
);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server is running on port ${PORT}`));

في هذا المثال، قمنا بإنشاء middleware اسمه verifyWebhookSignature. هذا الـ middleware يعترض أي طلب قادم إلى الرابط /stripe-webhook ويقوم بعملية التحقق كاملة قبل أن يصل إلى منطق العمل الأساسي. إذا كان التوقيع خاطئاً، يتم إيقاف الطلب فوراً.

نصائح من “الختيار”: خلاصة خبرة سنين

على مدار السنين، تعلمت بعض الدروس المهمة عند التعامل مع الـ Webhooks. اسمحولي أشارككم إياها:

  • لا تشارك السرّ أبداً: المفتاح السري (Secret) هو بمثابة كلمة المرور لنظام إشعاراتك. عامله بنفس الأهمية. لا تضعه أبداً في الكود المصدري بشكل مباشر، ولا تشاركه في مستودعات Git العامة.
  • استخدم متغيرات البيئة (Environment Variables): هي المكان الصحيح والآمن لتخزين المفاتيح السرية. كل بيئة عمل (تطوير، اختبار، إنتاج) يجب أن يكون لها مفتاحها الخاص.
  • لكل خدمة سرّها الخاص: إذا كنت تتعامل مع أكثر من خدمة ترسل لك Webhooks (مثلاً Stripe و GitHub)، استخدم مفتاحاً سرياً مختلفاً لكل خدمة. هذا يقلل من الضرر في حال تم تسريب أحد المفاتيح.
  • ابحث عن مكتبات جاهزة: معظم الخدمات الكبيرة توفر مكتبات (SDKs) بلغات برمجة مختلفة. هذه المكتبات غالباً ما تحتوي على دوال جاهزة للتحقق من توقيع الـ Webhook. استخدامها يوفر عليك الوقت ويحميك من الأخطاء الشائعة. (مثلاً، مكتبة Stripe لـ Node.js لديها stripe.webhooks.constructEvent التي تقوم بكل هذا العمل الشاق).
  • تحقق من الطابع الزمني (Timestamp): بعض الخدمات تضيف طابعاً زمنياً (Timestamp) إلى التوقيع. من الجيد التحقق من هذا الطابع ورفض الطلبات التي مر عليها وقت طويل (مثلاً أكثر من 5 دقائق). هذا يمنع نوعاً من الهجمات يسمى “هجمات إعادة الإرسال” (Replay Attacks) حيث يقوم المهاجم بتسجيل طلب حقيقي وإعادة إرساله لاحقاً.
  • سجّل المحاولات الفاشلة: عندما تفشل عملية التحقق من التوقيع، قم بتسجيل هذه المحاولة (Log it). إذا رأيت عدداً كبيراً من المحاولات الفاشلة من نفس عنوان الـ IP، فهذه علامة واضحة على أن هناك من يحاول اختراق نظامك، وقد ترغب في حظر هذا الـ IP.

الخلاصة: لا تترك بابك مفتوحاً! 🔐

في النهاية، الـ Webhooks هي أداة قوية جداً ومفيدة في بناء الأنظمة المترابطة والحديثة. لكن القوة تأتي مع مسؤولية. تجاهل تأمين الـ Webhook الخاص بك هو خطأ فادح قد يكلفك الكثير، كما كاد أن يكلفني.

عملية التحقق من التوقيع قد تبدو خطوة إضافية ومعقدة في البداية، لكنها في الحقيقة بسيطة جداً بمجرد فهمها، وهي الحاجز الأساسي بين نظام آمن ومستقر، وبين نظام فوضوي وعرضة للكوارث. لا تتهاون بها أبداً.

أتمنى أن تكون قصتي وتجربتي قد أفادتكم. ديروا بالكم على كودكم، ودائماً فكروا في الأمن أولاً.

يعطيكم العافية، وبالتوفيق في مشاريعكم.

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

​معمارية البرمجيات

خدماتنا كانت متشابكة كخيوط العنكبوت: كيف أنقذتني ‘المعمارية القائمة على الأحداث’ من كابوس الاعتمادية؟

أنا أبو عمر، وأروي لكم قصتي مع كابوس الأنظمة المتشابكة وكيف كانت "المعمارية القائمة على الأحداث" (Event-Driven Architecture) طوق النجاة الذي حرر خدماتنا. هذه المقالة...

27 مارس، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

واجهاتي كانت بلا روح: كيف أنقذتني ‘التفاعلات الدقيقة’ من تجربة مستخدم مملة؟

أنا أبو عمر، وفي هذه المقالة سأشارككم قصتي مع واجهات المستخدم "الميتة" وكيف أن التفاصيل الصغيرة، أو ما نسميه "التفاعلات الدقيقة" (Microinteractions)، كانت هي الروح...

27 مارس، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

مقابلاتي التقنية كانت صندوقاً أسود: كيف أنقذني ‘التفكير بصوت عالٍ’ من رفض الشركات رغم صحة الكود؟

في بداية مسيرتي، كنت أحل أصعب المسائل البرمجية في المقابلات التقنية بصمت، وأقدم كوداً صحيحاً 100%، لكن الرفض كان يأتيني تباعاً. هذه المقالة هي قصتي...

27 مارس، 2026 قراءة المزيد
التوسع والأداء العالي والأحمال

طلبات المستخدمين كانت تغرق خوادمنا: كيف أنقذني ‘تحديد معدل الطلبات’ (Rate Limiting) من استنزاف الموارد؟

أشارككم قصة حقيقية من تجربتي كمطور، عندما كاد نجاح أحد تطبيقاتي أن يتسبب في انهياره. سأشرح لكم بالتفصيل كيف كانت تقنية "تحديد معدل الطلبات" (Rate...

27 مارس، 2026 قراءة المزيد
البودكاست