يعنى إيه Race Condition؟

الـ Race Condition معناها إن فى سباق بيحصل بين عمليتين (أو Threads) فى نفس الوقت على (resource) مشترك، زى متغير (variable) أو ملف أو قاعدة بيانات،
والنتيجة بتعتمد على مين فيهم يخلص الأول أو مين يشتغل قبل التانى.

بمعنى تانى، لو العمليتين بيحاولوا يقروا أو يكتبوا على نفس الحاجة فى نفس اللحظة، ومفيش تنظيم بينهم، يحصل لخبطة فى النتائج.


💡 مثال بسيط:

تخيل إن فيه اتنين بيحاولوا يسحبوا فلوس من نفس الحساب البنكى فى نفس اللحظة:

  • الرصيد فى الأول = 1000 جنيه
  • الشخص الأول يسحب 500
  • الشخص التانى برضه سحب 500

لو النظام ماعملش تنظيم (synchronization)، ممكن يحصل ان:

  1. الشخص الأول يقرأ الرصيد = 1000
  2. الشخص التانى يقرأ الرصيد = 1000 (قبل ما الأول يخصم الفلوس)
  3. الأول يخصم 500 → الرصيد المفروض يبقى 500
  4. التانى يخصم 500 → بس هو كان لسه شايف الرصيد 1000، فالناتج يبقى 500 تانى
    ✅ المفروض الرصيد يبقى 0
    ❌ لكن هيبقى 500 بسبب الـ Race Condition

طب ليه الثغرة دى بتحصل؟

لأن فى أكتر من Thread أو Process بيشتغلوا مع بعض على نفس الحاجة، :

  • ومفيش (Lock) يمنع التداخل
  • أو تنفيذ ذكى يضمن إن العملية تتحسب كلها مرة واحدة (atomic operation)

الــ Attacker يقدر يعمل من خلالها اكتر من حاجة زى

  • ممكن تعمل Data corruption
  • أو Unauthorized actions حاجات تحصل مش المفروض تحصل
  • أو System crash
  • او انه يستغل اللحظة اللى مفيش فيها قفل ويعدّى خطوة تحقق أو صلاحية او يشترى منتج بسعر اقل او ياخد خصم اكتر من مرة.


1. الـ Features أو الـ Endpoints اللى ممكن نلاقى فيها الثغرة:

  • CouponApply: لو الثغرة في تطبيق كوبونات أو خصومات، زي لما الكوبون يتطبق أكتر من مرة (زي المثال اللي قولناه قبل كده).
  • PaymentProcessing: لو الثغرة في عمليات الدفع، زي لما الـ App يسمح بسحب فلوس مرتين من الرصيد بسبب Race.
  • SignUp: لو الثغرة في تسجيل حسابات، زي لما يتسجل حسابين بنفس الـ Username (زي المثال بتاع الـ Account Creation).
  • Checkout: لو الثغرة في عملية الـ Checkout في مواقع التسوق، زي لما الـ Order يتكرر أو يتعدل بطريقة غلط.
  • FileUpload: لو الثغرة في رفع ملفات، زي لما ملفين يترفعوا في نفس الوقت ويحصل Overwrite.
  • Booking: لو الثغرة في نظام الحجز (زي حجز تذكرة طيران أو فندق)، لما مقعد يتحجز مرتين.
  • ResetPassword: لو الثغرة في عملية إعادة تعيين كلمة السر، زي لما Requestين يغيروا الباسورد مع بعض.
  • InventoryUpdate: لو الثغرة في تحديث المخزون، زي لما منتج يتباع أكتر من الكمية المتاحة.

الــ Mitigation (حماية)

إيه اللي بيخلي الـ Race Condition يحصل أصلًا؟ زي ما قولنا قبل كده، الـ Race Condition بيحصل لما فيه عمليات (Requests) متعددة بتحاول تتفاعل مع نفس الـ Resource (زي Database، File، أو Memory) في نفس الوقت، ومفيش حماية كافية عشان تمنع التداخل. عشان كده، الـ Mitigation بيركز على إنك تخلي العمليات دي آمنة ومنظمة بحيث مفيش تداخل يحصل. هنقسم الـ Mitigation لخطوات عملية، وهنتكلم عن الحلول من منظور الـ Code، الـ Database، والـ Infrastructure.

1. استخدام Locks

الـ Locks هي أول حاجة تفكر فيها لما تيجي تحمي من الـ Race Condition. الفكرة إنك تقفل الـ Resource لحد ما العملية اللي بتستخدمه تخلّص، عشان ماحدش تاني يقدر يتدخل في نفس الوقت.

  • في الـ Code:

    • لو الـ App مبرمجة بـ Java، استخدم synchronized blocks أو ReentrantLock.
      مثال:
      synchronized(account) {
          if (account.balance >= amount) {
              account.balance -= amount;
          }
      }
      
      ده بيضمن إن مفيش Thread تاني يدخل يعدل في الـ balance لحد ما الأولاني يخلّص.
    • لو بـ Python، استخدم threading.Lock:
      from threading import Lock
      lock = Lock()
      
      def transfer_money(account, amount):
          with lock:
              if account['balance'] >= amount:
                  account['balance'] -= amount
      
      الـ with lock هنا بيمنع أي Thread تاني من الدخول لحد ما الـ Operation تخلّص.
  • في الـ File System: لو الـ App بتكتب في Files، استخدم File Locks (زي flock في Linux أو LockFile في Windows) عشان تمنع الكتابة المتزامنة.


2. استخدام Database Transactions

لو الـ Race Condition بيحصل في الـ Database (وهو شائع جدًا في الـ Web Apps)، الحل الأقوى هو استخدام Transactions مع Locking Mechanisms. الـ Transactions بتضمن إن العمليات بتحصل كلها مرة واحدة (Atomic) أو متتحدثش خالص.

  • كيف تعمل Transaction:
    • في SQL، استخدم BEGIN TRANSACTION و COMMIT:
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 123 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 123;
COMMIT;

الـ FOR UPDATE هنا بيقفل الـ Row في الـ Database، عشان مفيش Query تاني يقدر يعدل فيه لحد ما الـ Transaction تخلّص.

  • الــ Optimistic Locking:
    • لو الـ App عندها حركة كبيرة ومش عايزين نستخدم Locks ثقيلة، نستخدم Optimistic Locking. الفكرة إنك تضيف version أو timestamp لكل Record، وتشيك إنه لسه زي ما هو قبل التحديث. مثال:
UPDATE accounts SET balance = balance - 100, version = version + 1
WHERE user_id = 123 AND version = 5;

لو الـ version اتغير من Request تاني، الـ Update هيفشل.


3. الــ Atomic Operations

لو مش عايز تستخدم Locks أو Transactions، فيه عمليات Atomic (غير قابلة للتقسيم) بتضمن إن الـ Operation تتم كلها مرة واحدة من غير تداخل. دي شائعة في الـ Databases زي Redis أو MongoDB.

  • في Redis:
    • استخدم أوامر زي INCR أو DECR بدل ما تقرأ وتكتب يدوي:
INCR user:123:balance

ده بيزود الـ Value بطريقة Atomic، يعني مفيش مجال لحدوث Race.

  • في MongoDB:
    • استخدم findAndModify أو $inc:
db.accounts.findOneAndUpdate(
    { user_id: "123" },
    { $inc: { balance: -100 } }
 );

دي بتعدل الـ Value في خطوة واحدة.

  • وده بيتعمل عشان لو الـ App بتعتمد على Read ثم Write، هيبقى فيه فرصة للـ Race. لكن الـ Atomic Operations بتحل المشكلة دي.

4. الــ Queue Systems للـ High Traffic

لو الـ App بتستقبل طلبات كتير جدًا (زي موقع تسوق زي Noon في الـ Black Friday)، الحل الأفضل هو استخدام Queue Systems زي RabbitMQ أو Kafka. الفكرة إنك بدل ما تسيب الـ Requests تضرب الـ Server مباشرة، تحطها في طابور (Queue)، والـ Server يتعامل معاهم واحد واحد.

  • بتتم كالآتى

    • الـ User يبعت Request (مثلًا Apply Coupon).
    • الـ Request يتحط في Queue.
    • الـ Server ياخد الـ Request من الـ Queue، ينفذه، ويحدث الـ Database بطريقة آمنة (مع Lock أو Transaction).
  • الميزة هنا انه بيمنع الـ Concurrent Requests من التداخل، لأن الـ Queue بيضمن إن كل عملية تتم لوحدها.


5. الـ Rate Limiting مع مراقبة الـ Requests

في بعض الحالات، الـ Race Condition بيحصل بسبب إن الـ App بتستقبل Requests كتير أوي في وقت واحد. هنا ممكن تستخدم Rate Limiting عشان تحد من عدد الـ Requests من نفس الـ User أو الـ IP في ثانية واحدة.

  • وده بيتم عن طريق
    • استخدام Middleware زي Nginx أو Cloudflare عشان تحد عدد الـ Requests.
    • أو في الـ Code، حط Check على عدد الـ Requests:
from redis import Redis
    redis_client = Redis()

def rate_limit(user_id):
    key = f"ratelimit:{user_id}"
    count = redis_client.incr(key)
    redis_client.expire(key, 1)  # 1 second window
if count > 5:  # Max 5 requests per second
    return False
    return True

6. تقليل الـ Time Window للـ TOCTOU

الـ Race Condition بيحصل كتير بسبب الـ Time Of Check To Time Of Use (TOCTOU)، يعني الوقت بين الـ Check (التحقق) والـ Use (الاستخدام). لو قللت الوقت ده، هتقلل فرصة الـ Race.

  • ازاي؟

    • بدل ما تعمل Check ثم Update في خطوتين، ادمجهم في خطوة واحدة (زي الـ Atomic Operations).
    • أو استخدم In-Memory Databases زي Redis عشان السرعة العالية بتقلل الـ Window.
  • مثال: بدل:

  SELECT balance FROM accounts WHERE user_id = 123;
  -- (Time gap)
  UPDATE accounts SET balance = balance - 100 WHERE user_id = 123;

اعمل:

  UPDATE accounts SET balance = balance - 100 WHERE user_id = 123 AND balance >= 100;

ده بيخلي الـ Check والـ Update في Query واحدة.


Recourses

تقدر تقرى الـ Reports دى علشان تفهم اكتر فين ممكن تلاقى الثغرة وازاى تستغلها