أمان الدفع في أودو: رابط نجاح مزوّر لا يجعل الطلب مدفوعًا
كيف يستطيع رابط رجوع مزوّر أو ويب هوك معاد إرساله أن يجعل طلب أودو مدفوعًا، وكيف يمنع ذلك قراءة حالة الدفعة من واجهة تابي وتمارا نفسها بنداء موثّق.
في أي عملية دفع تعتمد على إعادة التوجيه، النقطة الأضعف ليست البوابة ولا الشبكة. النقطة الأضعف هي لحظة رجوع العميل. متجرك يرسل العميل إلى تابي أو تمارا، يصير شيء ما عند المزوّد، ثم يأتي متصفح لا تتحكم فيه ويطلب رابطًا على نطاقك أنت. ومهما كان هذا الطلب يقول، الوحدة عندك لازم تقرر: هل الطلب مدفوع أو لا؟
إذا كانت الوحدة تصدّق ما يقوله الرابط، فأي شخص يعرف يقرأ رابطًا يقدر يشتري من متجرك مجانًا. هذا المقال يشرح كيف يبدو هذا الهجوم فعليًا على متجر أودو، ثم كيف تتعامل معه Tabby & Tamara for Odoo. مكتوب للمطوّر الذي سيوقّع على قرار الشراء، أو الذي يريد مراجعة الوحدة المركّبة عنده اليوم.
الطلب الوحيد الذي لا تتحكم فيه
إطار الدفع في أودو يستقبل من مزوّد إعادة التوجيه ثلاثة أحداث، وهي ليست متساوية في الثقة أبدًا.
- رابط الرجوع. متصفح العميل يرجع إلى مسار على نطاقك، غالبًا طلب GET ومعه باراميترات. هذا الطلب تحت سيطرة العميل بالكامل: يقدر يعدّله، ويحفظه، ويعيد إرساله بعد ساعات، ويرسله لغيره.
- الويب هوك. المزوّد يرسل إشعارًا إلى نقطة نهاية عامة. وكونها عامة يعني أن أي شخص على الإنترنت يقدر يرسل لها هو أيضًا.
- النداء الصادر من خادمك. خادمك يسأل المزوّد مباشرة، عبر اتصال خاص به وبمفتاحك السري. العميل ليس طرفًا في هذه المحادثة إطلاقًا.
الحدث الثالث وحده هو مصدر الحقيقة. الأول والثاني مجرد تنبيه بأن شيئًا قد تغيّر. الربط الآمن يتعامل معهما بهذا المنطق، والربط غير الآمن يتعامل مع رابط الرجوع كأنه كشف حساب بنكي.
ولا تنسَ أن رابط الرجوع يُفتح في حالات بريئة تمامًا. عميل تراجع عن الخطة في صفحة تابي يقدر يضغط زر الرجوع في المتصفح، أو يفتح الرابط من سجل التصفح بعد يومين. أي منطق يفترض أن «وصل إلى رابط الرجوع» تعني «دفع» سينكسر مع عميل صادق قبل أن يصل إليه عميل غير صادق.
كيف يبدو رابط رجوع مزوّر
افترض أن الوحدة تسجّل مسارًا باسم /payment/tabby/return. الرابط الحقيقي قد يكون بهذا الشكل:
https://shop.example.com/payment/tabby/return?payment_id=9d7f1a2c&status=authorized
والمعالجة الساذجة تكون غالبًا بهذا الشكل:
@http.route('/payment/tabby/return', type='http', auth='public', csrf=False)
def tabby_return(self, **data):
tx = request.env['payment.transaction'].sudo().search(
[('reference', '=', data.get('order'))], limit=1)
if data.get('status') == 'success':
tx._set_done() # يثق بما جاء في الرابط
return request.redirect('/payment/status')
لاحظ أن هذا الكود لا يكلّم تابي إطلاقًا. حالة عملية مالية حقيقية تتحدد بنص كتبه العميل. ولهذا الهجوم ليس ذكيًا، الهجوم مجرد كتابة:
https://shop.example.com/payment/tabby/return?order=SO0042&status=success&amount=999
اطلب طلبًا عاديًا، خذ رقم المرجع من شاشة التأكيد، اترك صفحة الدفع، ثم افتح هذا الرابط. المعاملة تنتقل إلى حالة مدفوع، وأمر البيع يتأكد، والفاتورة تُرحّل، ورسالة التأكيد تُرسل. وفي متجر فيه تسليم تلقائي، المستودع يستلم أمر تجهيز لبضاعة لم يدفع ثمنها أحد.
والنسخ الأسوأ من هذا السيناريو معروفة: تغيير قيمة amount لمعرفة هل الوحدة تسجّل الرقم من الرابط بدل الرقم من البوابة، أو تجربة المراجع بالتسلسل SO0043 وSO0044 لإقفال طلبات آخرين، أو تنفيذ دفعة صغيرة حقيقية ثم إعادة إرسال إشعار نجاحها على معاملة أكبر.
توقيع الرابط يساعد لكنه ليس الحل
بعض البوابات توقّع باراميترات إعادة التوجيه، والتحقق من هذا التوقيع أفضل من عدمه بكثير، وهو يوقف الرابط المكتوب باليد أعلاه.
لكنه لا يخبرك بالحالة الحالية. التوقيع يثبت أن المزوّد أصدر هذه البيانات في لحظة ما، ولا يثبت أن الدفعة مصرّح بها الآن، ولا أنها لم تُلغَ بعدها بدقيقتين، ولا أنها حُصّلت فعلًا، ولا أن هذه الرسالة لم تُستهلك سابقًا على معاملة أخرى. التوقيع يجيب على سؤال «من قال هذا»، والسؤال الذي تحتاجه هو «ما الصحيح الآن».
ولهذا النمط الوحيد المتين هو أن تتوقف عن تحليل ما يُقال لك، وتذهب تسأل بنفسك.
كيف تتحقق Tabby & Tamara for Odoo من الدفعة
رابط الرجوع لا يعطي إلا المرجع
القيمة الوحيدة المأخوذة من المتصفح العائد هي المعرّف اللازم للعثور على المعاملة. لا حالة، ولا مبلغ، ولا عملة، ولا أي قرار للمزوّد يُقرأ من الباراميترات.
وهذه النقطة أهم مما تبدو. معناها أنه لا يوجد في ذلك الرابط شيء يستطيع المهاجم ضبطه ليستفيد منه. إضافة &status=success&amount=999 لا تغيّر شيئًا لأنه لا يوجد أصلًا مسار في الكود يقرأ هذه المفاتيح. الباراميترات المزوّرة ببساطة ليست جزءًا من القرار.
الحالة تُقرأ من واجهة المزوّد نفسه
بعد تحديد المعاملة، ترسل الوحدة نداءً من الخادم إلى الخادم وتقرأ كائن الدفعة من المصدر:
GET /api/v2/payments/{id}
Authorization: Bearer <مفتاحك السري>
الرد هو السجل المعتمد: الحالة الحالية، والمبلغ، والعملة كما هي عند المزوّد. وأودو يحرّك المعاملة بناءً على هذا الرد ولا شيء غيره. الدفعة المصرّح بها تُحصّل ثم تُغلق كمدفوعة، والدفعة المغلقة أو المرفوضة أو المنتهية الصلاحية تُلغى أو تُسجّل كخطأ، والدفعة التي ما زالت جارية تبقى معلّقة حتى يحسمها إشعار أو استعلام لاحق.
الخلاصة التي تستحق الحفظ: أفضل نتيجة ممكنة لرابط رجوع مزوّر هي أنه يشغّل استعلامًا يرجع بالحقيقة. لا توجد بيانات يستطيع المهاجم تلفيقها تجعل واجهة المزوّد تقول «مصرّح بها» والمزوّد لم يصرّح بشيء.
الويب هوك يمر من نفس الباب
الويب هوك في النهاية مجرد طلب HTTP قادم من الإنترنت. نقطة النهاية لازم تكون متاحة للعموم حتى يستخدمها المزوّد، وهذا يعني أنها متاحة لغيره أيضًا. وكتابة جسم JSON يدّعي نجاح دفعة ليست أصعب من تعديل رابط.
لذلك يُعامَل الويب هوك كمُشغّل لا كدليل. يُستخدم محتواه لمعرفة أي معاملة يتحدث عنها، ثم يتولى نفس النداء الموثّق GET /api/v2/payments/{id} تحديد ما يحدث فعلًا. الإشعار الملفّق أو المعاد إرساله يمر على نفس خطوة التحقق التي يمر عليها الإشعار الحقيقي، والإشعار الحقيقي لا يكسب شيئًا لكونه حقيقيًا.
المعاملة المنتهية لا يُعاد معالجتها
القطعة الأخيرة هي عدم تكرار المعالجة. المعاملة التي وصلت إلى حالة مدفوعة أو ملغاة أو خطأ لا تُعالج مرة ثانية، والإشعارات اللاحقة عليها تُستقبل وتُتجاهل.
وهذه ليست تفصيلة نظرية، فهي تغطي حالات يومية جدًا:
- المزوّدون يعيدون إرسال الويب هوك عند فشل التسليم، فوصول نسخة مكررة سلوك طبيعي لا استثنائي.
- رجوع العميل والويب هوك يصلان كثيرًا في نفس الثانية ويشيران لنفس السجل. بدون حارس الحالة النهائية قد تُسجَّل الدفعة مرتين على فاتورة واحدة.
- إشعار تحصيل قديم يُعاد إرساله بعد أن تكون قد استرجعت المبلغ للعميل، فيقلب معاملة مسترجعة إلى مدفوعة.
- إشعار إلغاء يصل متأخرًا، فيلغي دفعة سبق تحصيلها وتسويتها محاسبيًا.
وباجتماع هذا مع القراءة من الواجهة، يُغلق باب هجمات إعادة الإرسال كاملًا. أي إشعار مسجّل يمكن إرساله ألف مرة، وفي كل مرة إما يصطدم بمعاملة منتهية فلا يفعل شيئًا، أو يشغّل استعلامًا جديدًا يرجع بالحالة الحقيقية.
نفس السؤال عند الكاشير
في نقاط البيع السؤال نفسه لكن بشكل مختلف: ليس رابطًا مزوّرًا بل شاشة جوال. عميل يرفع جواله وفيه صفحة نجاح، والكاشير في طابور مزدحم ولا يملك طريقة يفرّق فيها بين موافقة الآن ولقطة شاشة من الأسبوع الماضي.
الحل بنفس المنطق. الكاشير يختار المزوّد، تفتح نافذة برمز QR، العميل يمسح ويوافق من جواله، والخادم يتابع البوابة ويحصّل عند التصريح. وزر الترحيل يبقى مقفلًا حتى ينجح التحصيل فعليًا، بدون تجاوز يدوي. الشاشة التي يثق بها الكاشير تُحرّكها استجابة البوابة، لا ما يعرضه العميل. والمسار كاملًا عند الكاشير، وما يجب تدريبه عليه، في تابي وتمارا في نقاط البيع في أودو.
مراجعة في عشرين دقيقة تقدر تسويها على أي وحدة
لا داعي لتصديق كلام أي بائع، ولا كلامنا نحن. في وضع الاختبار، وبطلب تجريبي، نفّذ التالي:
- أكمل دفعة اختبارية حقيقية، والتقط رابط الرجوع كاملًا من تبويب الشبكة في المتصفح.
- ابدأ طلبًا ثانيًا، ألغِه من صفحة المزوّد، ثم الصق رابط الرجوع الأول بعد تغيير المرجع إلى مرجع الطلب الثاني. يجب ألا يصبح الطلب مدفوعًا.
- أضف
&status=success&amount=1إلى رابط رجوع حقيقي وأعد التحميل. يجب ألا يتغير شيء، وألا يتغير المبلغ المسجّل. - أعد إرسال إشعار ناجح مرة ثانية. النتيجة الصحيحة دفعة واحدة وفاتورة واحدة، لا اثنتان.
- أرسل بنفسك طلب POST بجسم JSON مكتوب يدويًا إلى نقطة الويب هوك، بمرجع صحيح وحالة نجاح. يجب ألا يتغير شيء.
- افتح سجل الـ API. لكل حدث من الأحداث السابقة يجب أن ترى نداءً صادرًا إلى المزوّد. وإذا انقلب طلب إلى مدفوع بدون أي نداء صادر مسجّل، فالوحدة صدّقت المدخلات. وفي دليل حل مشكلة اختفاء تابي من صفحة الدفع فقرة عن قراءة هذا السجل حسب رمز الاستجابة.
- اقرأ كود المتحكم وابحث عن باراميترات الطلب وهي تصل إلى
_set_doneأو_set_authorizedأو ما يقابلها في إصدارك. دوال تغيير الحالة يجب أن تتغذى من رد الواجهة بعد تحليله، لا منkwargs.
النقطة السابعة مسألة ترخيص بقدر ما هي مسألة تقنية. Tabby & Tamara for Odoo تصدر برخصة LGPL-3 وبمصدر مقروء، فهذه المراجعة شيء تقدر تنفّذه فعليًا. أما الوحدة المغلقة، أو المرخّصة لنطاق واحد مع منع مشاركة المصدر، فلا يمكن قبولها إلا بالثقة المجردة.
ماذا تقول صفحات المنافسين عن هذا الموضوع
هذه ملاحظة منصفة لا اتهام. في صفحات وحدات تابي وتمارا التي فُحصت في متجر تطبيقات أودو، لا توجد صفحة واحدة توضّح كيف تتحقق من حالة الدفعة أبعد من ذكر «تسجيل ويب هوك». وأغلبها مزوّد واحد لكل وحدة بسعر يقارب 270 دولارًا، فمن يريد المزوّدين يشتري مرتين. وواحدة تغطي الاثنين لكن للمتجر الإلكتروني فقط، وأخرى تغطي الاثنين لكن لنقاط البيع فقط وتشترط نسخة Enterprise، وأرخص خيار مرخّص لنطاق واحد ويمنع مشاركة المصدر. والمقارنة الكاملة بين هذه الإضافات، بأسعارها وما تغطيه كل واحدة، في الفرق بين إضافات تابي وتمارا في متجر أودو.
وهذا كلام عن الأوصاف المنشورة، لا عن الكود خلفها. قد تكون تلك الوحدات تتحقق من جهة الخادم ولم تذكر ذلك. لكنك مطالب بالشراء بناءً على الوصف، فاسأل البائع ثلاثة أسئلة قبل الدفع:
- هل يؤثر أي جزء من رابط الرجوع على حالة المعاملة؟
- هل تُقرأ الحالة النهائية من واجهة المزوّد بنداء موثّق منفصل؟
- ماذا يحدث حين يصل إشعار جديد لمعاملة وصلت حالة نهائية؟
أي بائع فكّر في الموضوع سيجيب في فقرة واحدة.
ما الذي لا يحميك منه التحقق من جهة الخادم
الوضوح في حدود المسؤولية جزء من المصداقية.
هو لا يفتح لك حساب تاجر. تحتاج حسابك الخاص لدى تابي وحسابك لدى تمارا بمستنداتهما وإجراءات الموافقة عندهما. الوحدة توصّل مفاتيحك فقط، وهي ربط مستقل من كرنلتكس بدون أي ارتباط أو اعتماد من الشركتين.
وهو لا يحمل عنك مخاطر الائتمان. التزام العميل بسداد أقساطه شأن بينه وبين المزوّد حسب اتفاقيتك، وعدد الدفعات ونسب العمولة تحددها تلك الشركات لا أي وحدة في أودو.
وهو لا يغني عن انضباطك أنت. احفظ المفاتيح السرية خارج مستودع الكود، وشغّل مسارات الرجوع والويب هوك على HTTPS فقط، وأبقِ الاسترداد خلف مجموعة الصلاحيات المخصصة التي توفّرها الوحدة بدل إتاحته لكل موظف في الإدارة الخلفية. وسجل الـ API يخفي الأسرار، وهذا يجعل قراءته آمنة، لكنه يبقى بيانات تشغيلية يستحسن تقييد الوصول إليها.
الخلاصة
أي ربط للدفع الآجل هو في جوهره نظام يسلّم بضاعة بناءً على رسالة. والسؤال الوحيد المهم: أي رسالة يصدّقها؟ إذا صدّق متصفح العميل فأنت بنيت متجرًا مجانيًا. وإذا صدّق ردًا موثّقًا من المزوّد، ورفض إعادة معالجة ما انتهى، تحوّل رابط الرجوع المزوّر إلى مجرد صفحة تُفتح بلا أثر.
Tabby & Tamara for Odoo وحدة واحدة بشراء واحد بـ 200 دولار، تغطي المزوّدين معًا في المتجر الإلكتروني وفواتير البوابة وعروض الأسعار ونقاط البيع، على أودو من 14.0 إلى 19.0، على النسخة المجتمعية والمؤسسية، وبرخصة LGPL-3 حتى يقرأ مطوّرك كل سطر في مسار التحقق قبل الإطلاق.
أسئلة متكررة
هل يمكن لرابط رجوع مزوّر أن يجعل طلب أودو مدفوعًا؟
لا، ليس مع ربط يتحقق من جهة الخادم. في Tabby & Tamara for Odoo لا يؤخذ من رابط الرجوع إلا المرجع، أما الحالة فتُقرأ بنداء موثّق إلى واجهة المزوّد عبر GET /api/v2/payments/{id}. إضافة باراميترات مثل status=success أو amount لا تؤثر على شيء لأن الكود لا يقرأها أصلًا. لكن الوحدة التي تصدّق الرابط تستطيع فعلًا تحويل الطلب إلى مدفوع، ولهذا يستحق السؤال أن يُطرح قبل الشراء.
كيف يتم التحقق من الويب هوك، وهل يكفي التوقيع؟
الويب هوك يُعامل كمُشغّل لا كدليل. يُستخدم لتحديد المعاملة المقصودة فقط، ثم تُقرأ الحالة الحقيقية من واجهة المزوّد بنفس النداء الموثّق. التوقيع مفيد ويمنع الرسائل المكتوبة يدويًا، لكنه يثبت من أرسل الرسالة ولا يثبت الحالة الحالية: رسالة موقّعة قديمة قد تكون لدفعة أُلغيت أو استُرجعت بعدها. القراءة من المصدر هي ما يحسم.
ماذا يحدث لو وصل نفس الإشعار مرتين؟
لا شيء في المرة الثانية. المعاملة التي وصلت حالة نهائية، مدفوعة أو ملغاة أو خطأ، لا يُعاد معالجتها، والإشعار اللاحق يُستقبل ويُتجاهل. هذا يغطي إعادة المحاولة الطبيعية من المزوّد، ووصول رجوع العميل والويب هوك في نفس الثانية، وإعادة إرسال إشعار قديم بعد الاسترداد. النتيجة دفعة واحدة على الفاتورة، لا دفعتان.
كيف أتأكد بنفسي أن الوحدة تتحقق فعلًا من جهة الخادم؟
افتح سجل الـ API بعد كل حدث دفع. يجب أن ترى نداءً صادرًا إلى المزوّد مقابل كل رجوع أو ويب هوك. إذا انقلب طلب إلى مدفوع بدون أي نداء صادر، فالوحدة صدّقت المدخلات. وبما أن الوحدة برخصة LGPL-3 ومصدرها مقروء، تقدر أيضًا تفتح كود المتحكم وتتأكد أن دوال مثل _set_done تتغذى من رد الواجهة بعد تحليله لا من باراميترات الطلب.
وفي نقاط البيع، ما الذي يمنع الكاشير من إقفال بيع غير مدفوع؟
زر الترحيل يبقى مقفلًا حتى ينجح التحصيل فعليًا عند المزوّد، بدون تجاوز يدوي. الكاشير يختار المزوّد، وتفتح نافذة برمز QR، والعميل يوافق من جواله، والخادم يتابع البوابة ويحصّل عند التصريح. لقطة شاشة لموافقة قديمة لا تفتح الزر، لأن الشاشة تُحرّكها استجابة البوابة لا جوال العميل.