Nouvelle réglementation de cybersécurité 2026 en vigueur au Maroc. Obtenir un audit de conformité gratuit →
← العودة إلى المدونة
أمن الويب 2026-08-25 ⏱️ 11 min

حالات التسابق في تطبيقات الويب: من ثغرات المنطق إلى الاستيلاء على الحسابات

ركزت اختبارات أمان تطبيقات الويب تقليدياً على ثغرات الحقن (Injection)، وعيوب المصادقة، والخلل في إدارة الصلاحيات والتحكم بالوصول. ومع ذلك، ومع تحول البنى البرمجية الحديثة نحو الأنظمة الموزعة، والخدمات المصغرة (Microservices)، والمعالجة غير المتزامنة عالية السرعة، برز تصنيف خفي وبالغ الخطورة من ثغرات المنطق التجاري: حالات التسابق (Race Conditions). عندما تعالج التطبيقات طلبات متزامنة متعددة دون آليات مزامنة صارمة، تنهار الفرضيات الزمنية التي وضعها المطورون، وتتحول القواعد المنطقية التي بدت محكمة على الورق إلى ثغرات أمنية كارثية.

تحدث حالة التسابق عندما يحاول مساران أو عمليتان معالجة الوصول إلى مورد أو حالة بيانات مشتركة وتعديلها في الوقت ذاته، وتعتمد النتيجة النهائية على الترتيب الزمني الدقيق وغير المضمون للتنفيذ. وفي بيئات الويب المعاصرة، تتيح هذه الثغرات للمهاجمين تجاوز الحدود المالية، ومضاعفة الأرصدة الرقمية، واستخدام أكواد الخصم الترويجية لمرات عديدة، وصولاً إلى الاستيلاء الكامل على حسابات المستخدمين (Account Takeover). بعيداً عن كونها فرضيات أكاديمية مقتصرة على أنوية أنظمة التشغيل، تمثل حالات التسابق اليوم واحدة من أكثر الثغرات ربحية وتأثيراً في عمليات التدقيق الأمني واختبارات الاختراق لمنصات التجارة والتقنية المالية.


1. الميكانيكية الجوهرية: ثغرة وقت الفحص إلى وقت الاستخدام (TOCTOU)

في القلب الرياضي لكل حالة تسابق على الويب تقريباً، تكمن ثغرة وقت الفحص إلى وقت الاستخدام (Time-of-Check to Time-of-Use - TOCTOU). يقوم المبرمج بطبيعته بكتابة المنطق التجاري في تسلسل إجرائي متتابع:

  1. الخطوة 1 (الفحص - Check): التحقق من حالة المورد الحالية (مثلاً: "هل تم استخدام كود الخصم هذا من قبل؟").
  2. الخطوة 2 (التقييم والتنفيذ): إذا نجح التحقق، يتم تنفيذ العملية المنطقية (مثلاً: خصم 50% من إجمالي سلة الشراء).
  3. الخطوة 3 (الاستخدام والتحديث - Use/Update): تعديل قاعدة البيانات لتسجيل الحالة الجديدة (مثلاً: تعيين redeemed = TRUE).

تكمن الثغرة الأمنية في الفجوة الزمنية الدقيقة الفاصلة بين الخطوة 1 والخطوة 3. في خوادم الويب متعددة المسارات (مثل Puma أو Gunicorn أو مسارات معالجة Node.js وGo)، إذا وصل طلبان من نوع HTTP بفارق أجزاء من الألف من الثانية، فسينفذ كلاهما الخطوة الأولى في وقت متزامن. ولأن أياً من الطلبين لم يصل بعد إلى الخطوة 3، فإن كلا الطلبين يقرأ نفس الحالة السابقة (redeemed = FALSE)، وينجح كلا الطلبين في تجاوز الفحص، ويحصل المهاجم على الخصم مرتين. هذه الفجوة الزمنية الدقيقة تُعرف باسم نافذة التسابق (Race Window).

الطلب الأول: [فحص: هل تم الاستخدام؟ لا] ----> [تطبيق الخصم] ----> [تحديث: تم الاستخدام]
الطلب الثاني: ----- [فحص: هل تم الاستخدام؟ لا] ----> [تطبيق الخصم] ----> [تحديث: تم الاستخدام]
النتيجة: تكرار الخصم مرتين لأن الطلب الثاني فحص الحالة قبل أن يكمل الطلب الأول كتابة التحديث.

أي تأخير ناجم عن بطء الشبكة، أو قراءة الذاكرة والتخزين، أو الاتصال بقاعدة البيانات، أو طلب واجهة خارجية (مثل تأكيد الدفع عبر Stripe أو إرسال بريد إلكتروني) يؤدي إلى توسيع نافذة التسابق من مجرد ميكروثانية إلى مئات المللي ثوانٍ، مما يجعل تنفيذ الهجوم سهلاً ومضموناً.


2. الاستغلال الكلاسيكي: تجاوز قيد "مرة واحدة لكل مستخدم"

يعد التجاوز الترويجي في منصات التجارة الإلكترونية وبرامج الولاء والمحافظ الرقمية أكثر التطبيقات العملية شيوعاً لحالات التسابق، حيث تقدم المنصة قسائم خصم أو أرصدة تسجيل تقتصر على استخدام واحد لكل حساب عميل.

سيناريو الهجوم: مضاعفة قسائم الشراء

لنفترض أن موقع تجارة إلكترونية يقدم للمستخدمين الجدد قسيمة بقيمة 100 درهم مغربي تُستخدم مرة واحدة فقط عند إتمام عملية الشراء. تم تصميم نقطة النهاية لمعالجة القسيمة كالتالي:

POST /api/v2/cart/apply-coupon HTTP/1.1
Host: shop.example.ma
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json

{
    "coupon_code": "WELCOME100"
}

في الاستخدام العادي عبر المتصفح، يرسل العميل طلباً واحداً ويحصل على الخصم. إذا حاول إرساله ثانية تظهر له رسالة خطأ HTTP 400 توضح أن القسيمة استُخدمت بالفعل.

لكن عند قيام مختبر الاختراق باستخدام أداة Burp Suite Professional:

  1. يتم اعتراض طلب تطبيق القسيمة POST /api/v2/cart/apply-coupon.
  2. يتم تكرار الطلب في 20 تبويباً داخل وحدة Repeater.
  3. يتم جمع كافة التبويبات في مجموعة مخصصة (Tab Group).
  4. يختار المختبر أمر "Send group in parallel" (إرسال المجموعة بالتوازي).

تقوم أداة Burp Suite بإطلاق الـ 20 طلباً في التوقيت نفسه عبر اتصالات متزامنة. إذا كان التطبيق يعاني من حالة تسابق، تنجح ما بين 10 إلى 15 طلباً في تجاوز فحص الصلاحية قبل تحديث السجل في قاعدة البيانات. وبذلك يتم تخفيض قيمة الفاتورة بمقدار 1,500 درهم بدلاً من 100 درهم فقط، مما يتيح الحصول على المنتجات مجاناً دون تكلفة.


3. السيطرة على الحسابات (ATO) عبر استغلال استعادة كلمة المرور

لا تتوقف مخاطر حالات التسابق عند الخسائر المالية المباشرة، بل تمتد لتضرب منظومات التحقق والهوية الرقمية في مقتل. وتعد ثغرات التسابق في آليات استعادة كلمات المرور واحدة من أخطر السبل للسيطرة الكاملة على حسابات الضحايا (Account Takeover).

سيناريو الثغرة: توليد الرموز السرية المتزامنة

عندما يطلب المستخدم استعادة كلمة المرور، يولد التطبيق رمزاً عشوائياً مشفراً، ويحفظه في قاعدة البيانات مع وقت انتهاء الصلاحية، ثم يرسل رابط الاستعادة في بريد إلكتروني:

# معالج طلب استعادة كلمة المرور المعرض للثغرة
def request_password_reset(email):
    user = db.find_user_by_email(email)
    if not user:
        return {"status": "ok"}  # منع تعداد الحسابات

    token = generate_secure_token()
    db.save_reset_token(user.id, token)  # يستبدل الرمز النشط القديم
    send_reset_email(user.email, token)
    return {"status": "ok"}

لننظر الآن إلى معالج إنهاء العملية وتعيين كلمة المرور الجديدة:

# معالج تعيين كلمة المرور الجديدة
def complete_password_reset(token, new_password):
    record = db.find_reset_record(token)
    if not record or record.is_expired():
        raise ValidationError("الرمز غير صالح أو منتهي الصلاحية")

    user = db.find_user(record.user_id)
    user.set_password(new_password)
    db.save(user)

    # إلغاء الرمز بعد حفظ البيانات
    db.invalidate_token(token)
    return {"status": "success"}

طريقة استغلال الثغرة للاستيلاء على الحساب

يستهدف المهاجم حساب الضحية ([email protected]). إذا كانت بنية النظام الخلفية تشترك في حالة الجلسة أو تعاني من تصادم أثناء تعيين الرموز للمستخدمين المتزامنين، يرسل المهاجم طلبين متزامنين بدقة متناهية:

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


4. استنزاف الأرصدة في المحافظ الرقمية وبطاقات الهدايا

في التطبيقات المصرفية، والمحافظ الإلكترونية، ومنصات تداول العملات الرقمية، تتيح حالات التسابق سحب الأموال بمبالغ تفوق الرصيد الفعلي للمستخدم. ويُعرف هذا النمط بهجوم السحب المتزامن للأرصدة.

نموذج الشفرة المصابة بالخلل

تمعن في هذا الكود البرمجي المكتوب بلغة بايثون لمعالجة سحب الأموال:

# معرض للخطر: ثغرة TOCTOU صريحة في فحص الرصيد وخصمه
def process_wallet_withdrawal(user_id: int, amount: float):
    current_balance = db.query("SELECT balance FROM wallets WHERE user_id = ?", user_id).scalar()

    # 1. وقت الفحص (TIME OF CHECK)
    if current_balance >= amount:
        # تأخير ناتج عن خدمة مكافحة الاحتيال أو الاتصال بالبنك
        fraud_score = external_fraud_service.evaluate(user_id, amount)

        if fraud_score.is_acceptable():
            # 2. وقت الاستخدام (TIME OF USE)
            new_balance = current_balance - amount
            db.execute("UPDATE wallets SET balance = ? WHERE user_id = ?", new_balance, user_id)
            disburse_funds_to_bank(user_id, amount)
            return True

    return False

إذا كان لدى المستخدم في رصيده 500 درهم وقام بطلب سحب واحد بقيمة 500 درهم، تتم المعاملة بنجاح ويصبح رصيده المتبقي 0 درهم.

أما إذا أرسل المهاجم عشرة طلبات سحب متطابقة في اللحظة الزمنية ذاتها، فستنفذ جميع مسارات المعالجة استعلام SELECT balance قبل أن يتمكن أي مسار من تنفيذ أمر UPDATE wallets. وبذلك تجد كافة المسارات العشرة أن الشرط 500 >= 500 محقق وصحيح! تُطلق جميع المسارات دالة تحويل الأموال disburse_funds_to_bank()، ويتم تحويل 5,000 درهم إلى الحساب البنكي الخارجي للمهاجم، بينما يُكتب الرصيد في قاعدة البيانات أخيراً ليصبح 0 درهم. استطاع المهاجم بذلك توليد 4,500 درهم إضافية من العدم.


5. هجوم الحزمة الواحدة عبر بروتوكول HTTP/2: القضاء التام على اضطراب الشبكة

لسنوات طويلة، واجه المهاجمون صعوبة بالغة في استغلال حالات التسابق الدقيقة عبر شبكة الإنترنت العامة بسبب اضطراب توقيت الشبكة (Network Jitter)؛ إذ تؤدي تقلبات مسارات التوجيه والتأخير في مصافحة TLS إلى وصول الطلبات إلى الخادم بفارق أجزاء من الثانية، مما يؤدي إلى ضياع نافذة التسابق ذات الميكروثانية الواحدة.

في عام 2023، أحدث الباحث الأمني جيمس كيتل (مدير الأبحاث في شركة PortSwigger) ثورة في هذا المجال من خلال الكشف عن هجوم الحزمة الواحدة لبروتوكول HTTP/2 (HTTP/2 Single-Packet Attack).

كيف يعمل هجوم الحزمة الواحدة؟

يتميز بروتوكول HTTP/2 بميزة التضمين المتعدد (Multiplexing)، التي تسمح بإرسال عدة طلبات واستجابات HTTP منفصلة عبر اتصال TCP واحد ودائم. ومن خلال استغلال هذه الميزة، يقوم المهاجم بما يلي:

  1. فتح اتصال TCP واحد وإكمال مصافحة تشفير TLS مع الخادم المستهدف.
  2. تجهيز من 20 إلى 50 طلباً كاملاً من طلبات الـ HTTP.
  3. إرسال 99% من محتوى كل طلب عبر الاتصال، مع تعمد حجب البايت الأخير (أو إطار نهاية الدفق) من كل طلب.
  4. يقوم الخادم بتعليق كافة الطلبات غير المكتملة في الذاكرة بانتظار الإطارات الختامية.
  5. يقوم المهاجم بدمج كافة البايتات الأخيرة المحجوبة في حزمة TCP واحدة فقط وإرسالها عبر الشبكة دفعة واحدة.

تدفق TCP المتعدد: [الطلب 1: الترويسات + 99% من المحتوى] [الطلب 2: الترويسات + 99% من المحتوى] ...
--- يقوم الخادم بحفظ الطلبات في الذاكرة بانتظار البايت الختامي ---
حزمة TCP المنفردة: [البايت الأخير للطلب 1 + البايت الأخير للطلب 2 + ... + الطلب 30]
النتيجة: يكتمل وصول ومعالجة جميع الطلبات الـ 30 في النانوثانية الفيزيائية ذاتها داخل نواة الخادم.

نظراً لأن نواة نظام التشغيل على الخادم تعالج حزمة الـ TCP الواردة ضمن مقاطعة عتادية واحدة، يتم تحرير كافة الطلبات الـ 30 في رتل المعالجة في اللحظة الفيزيائية ذاتها تماماً. يزول اضطراب الشبكة بالكامل، وتتحول نوافذ التسابق الدقيقة التي كانت مستحيلة الاستغلال إلى ثغرات مؤكدة النجاح بنسبة 100%.

التطبيق العملي عبر أداة Turbo Intruder

يختبر خبراء الأمن السيبراني هذه الثغرات باستخدام إضافة Turbo Intruder في Burp Suite، بالاعتماد على سكريبت race-single-packet-attack.py:

# هجوم الحزمة الواحدة لحالات التسابق باستخدام Turbo Intruder
def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           engine=Engine.BURP2  # فرض استخدام بروتوكول HTTP/2
                           )

    # صف 30 طلباً متطابقاً في بوابة مزامنة واحدة
    for i in range(30):
        engine.queue(target.req, gate='race_gate')

    # فتح البوابة وإطلاق كافة الطلبات متزامنة في حزمة واحدة
    engine.openGate('race_gate')

def handleResponse(req, interesting):
    table.add(req)

6. الاستراتيجيات الدفاعية: بناء أنظمة برمجية محصنة ضد التزامن

يتطلب التصدي الجذري لحالات التسابق إلغاء الفجوة الزمنية بين التحقق من الحالة وتحديثها. إن الاعتماد على الشروط البرمجية التقليدية على مستوى لغة البرمجة غير كافٍ إطلاقاً، بل يجب فرض العزل الذري على مستوى قواعد البيانات ونظم القفل الموزع.

6.1 العمليات الذرية في قواعد البيانات (Atomic Operations)

أبسط الحلول وأكثرها فعالية لحماية الأرصدة والعدادات هو استخدام استعلامات SQL الذرية. بدلاً من قراءة الرصيد ثم حسابه في لغة البرمجة ثم حفظه، اطلب من قاعدة البيانات تنفيذ الفحص والخصم كعملية ذرية واحدة لا تتجزأ:

-- آمن: فحص وخصم ذري في تعليمة برمجية واحدة
UPDATE wallets
SET balance = balance - 500.00
WHERE user_id = 42 AND balance >= 500.00;

يقوم محرك قاعدة البيانات بتقييم شرط WHERE balance >= 500.00 تحت قفل الصف. فإذا استقبلت قاعدة البيانات عشرة طلبات متزامنة، ينجح الطلب الأول في الخصم ويعيد عدد الصفوف المتأثرة 1. أما الطلبات التسعة الأخرى فتفشل في مطابقة الشرط وتعيد 0. يقوم التطبيق بفحص الصفوف المتأثرة؛ فإذا كانت 0، يرفض العملية فوراً.

6.2 القفل المتشائم (Pessimistic Locking: SELECT FOR UPDATE)

عندما تتطلب المعاملة الحسابية قراءة جداول متعددة وإجراء عمليات تحقق معقدة قبل التعديل، يجب استخدام القفل المتشائم ضمن معاملة ACID متكاملة:

# آمن: قفل متشائم باستخدام SQLAlchemy في بايثون
from sqlalchemy.orm import Session

def secure_transfer(db: Session, sender_id: int, recipient_id: int, amount: float):
    with db.begin():
        # SELECT ... FOR UPDATE يفرض قفلاً حصرياً على صف المرسل
        sender_wallet = db.query(Wallet).filter(Wallet.id == sender_id).with_for_update().one()

        if sender_wallet.balance < amount:
            raise InsufficientFundsException("الرصيد غير كافٍ")

        sender_wallet.balance -= amount

        recipient_wallet = db.query(Wallet).filter(Wallet.id == recipient_id).with_for_update().one()
        recipient_wallet.balance += amount
        # يتم تحرير القفل تلقائياً عند إتمام المعاملة (commit)

6.3 الأقفال الموزعة عبر Redis (خوارزمية Redlock)

في البيئات الموزعة القائمة على الخدمات المصغرة (Microservices) حيث تتوزع البيانات عبر عدة خوادم وخدمات دفع خارجية، لا يمكن لأقفال قواعد البيانات التقليدية حماية دورة المعاملة بالكامل. هنا يتعين تطبيق قفل موزع عبر Redis:

import redis
from redlock import Redlock

dlm = Redlock([{"host": "redis-cluster.internal", "port": 6379, "db": 0}])

def redeem_voucher_distributed(user_id: int, voucher_code: str):
    lock_key = f"lock:voucher:{user_id}:{voucher_code}"
    # حجز القفل لمدة 3 ثوانٍ لمنع حالات التوقف التام (Deadlocks)
    my_lock = dlm.lock(lock_key, 3000)

    if not my_lock:
        raise ConcurrencyConflictException("هناك معاملة جارية حالياً على هذه القسيمة")

    try:
        # منطقة حرجة: ينفذ طلب واحد فقط للمستخدم في كل لحظة
        execute_redemption_logic(user_id, voucher_code)
    finally:
        dlm.unlock(my_lock)

6.4 مفاتيح حتمية المعاملات (Idempotency Keys)

في واجهات الدفع الإلكتروني والتحويلات المالية، يجب إلزام المستخدمين بإرسال ترويسة Idempotency-Key فريدة لكل عملية (UUID). يسجل الخادم المفتاح فور استلامه في ذاكرة سريعة، وأي طلب متزامن يحمل نفس المفتاح أثناء قيد المعالجة يتم رفضه برمز الخطأ HTTP 409 Conflict، مما يمنع تنفيذ العمليات المتكررة قطعياً.


7. منهجية الاختبار: صيد ثغرات التزامن في الأنظمة الحية

يتطلب التدقيق الاستباقي لكشف حالات التسابق استخدام أدوات متخصصة قادرة على إلغاء فوارق توقيت الشبكة:

  • المحرك المتوازي في Burp Suite Repeater: تجميع التبويبات واختبار الإرسال المتوازي للتحقق الفوري من قيود الخصومات والمكافآت.
  • أداة Turbo Intruder: استغلال بوابات التزامن (openGate) عبر قنوات بروتوكول HTTP/2 لكشف نوافذ التسابق بالغة الصغر في بوابات المصادقة والدفع.
  • أداة Race Condition Hunter (RCH): ملحقات آلية تقوم بمسح التطبيقات واكتشاف المسارات الحساسة لتطبيق اختبارات الفحص التزامني عليها تلقائياً.

خاتمة: تحصين الأصول الرقمية ضد أخطاء التزامن البرمجي

تثبت حالات التسابق أن الشفرة البرمجية قد تكون خالية تماماً من ثغرات الحقن التقليدية أو تسريب الذاكرة، ولكنها تظل معرضة للاختراق الشامل بسبب خلل في الفرضيات الزمنية. ومع توسع المؤسسات المغربية ومنصات التكنولوجيا المالية الإقليمية نحو البيئات السحابية الموزعة عالية السرعة، تتسع رقعة الاستغلال لهذه الثغرات بشكل كبير.

إن حماية تطبيقات الويب الحديثة تتطلب الانتقال الجذري نحو البرمجة الواعية بالتزامن: استعلامات ذرية، أقفال موزعة، مفاتيح المعاملات الحتمية، وإجراء تدقيقات أمنية دورية تعتمد على تقنيات هجمات الحزمة الواحدة HTTP/2.

افحص تطبيقاتك ضد ثغرات التزامن والمنطق التجاري

هل معاملاتك المالية أو بوابات المصادقة في موقعك محصنة ضد هجمات حالات التسابق؟ تقدم شركة Cayvora Security اختبارات اختراق متقدمة لتطبيقات الويب باستخدام تقنيات HTTP/2 single-packet لاكتشاف الثغرات المنطقية الخفية ومعالجتها قبل وقوع أي خسائر مالية.

✉️ طلب تدقيق أمني لتطبيقات الويب

Articles Connexes