پرش به محتوای اصلی
آسیب‌پذیری‌های سایت

Race Condition چیست؟ بررسی آسیب‌پذیری شرایط رقابتی در وب‌سایت‌ها

Race Condition یا شرایط رقابتی زمانی رخ می‌دهد که چند عملیات هم‌زمان روی یک State مشترک اجرا شوند و نتیجه برنامه به ترتیب زمانی اجرای آن‌ها وابسته باشد. این آسیب‌پذیری می‌تواند باعث استفاده چندباره از کوپن، ثبت سفارش بیش از موجودی، مشکلات مالی، دورزدن محدودیت‌ها یا ایجاد State ناسازگار شود. استفاده از Atomic Operation، Transaction مناسب، Locking، Unique Constraint، Idempotency و کنترل دقیق Business Logic از مهم‌ترین روش‌های جلوگیری از Race Condition است.

تیم امنیت رخنه‌کاو ۳۸ دقیقه مطالعه انتشار:

Race Condition یا «شرایط رقابتی» نوعی ضعف نرم‌افزاری است که زمانی ایجاد می‌شود که دو یا چند درخواست، Thread، Process یا عملیات هم‌زمان روی یک State یا منبع مشترک کار کنند و نتیجه برنامه به ترتیب زمانی اجرای آن‌ها وابسته شود. در وب‌سایت‌ها این آسیب‌پذیری می‌تواند باعث استفاده چندباره از یک کوپن، ثبت سفارش بیش از موجودی، خرج‌کردن اعتبار بیش از حد، اجرای چندباره عملیات یک‌بارمصرف، عبور از محدودیت‌ها یا ایجاد وضعیت ناسازگار در حساب کاربر شود.

نکته مهم این است که Race Condition الزاماً به معنی «سریع بودن دو درخواست» نیست. مشکل واقعی زمانی رخ می‌دهد که برنامه عملیاتی را که باید به شکل Atomic و غیرقابل‌تقسیم انجام شود به چند مرحله مستقل تقسیم کند؛ برای مثال ابتدا بررسی کند موجودی کافی است و چند لحظه بعد موجودی را کاهش دهد. اگر درخواست دیگری در فاصله میان این دو مرحله همان State را تغییر دهد، تصمیم اولیه دیگر معتبر نیست.

Race Condition در دسته ضعف‌های مرتبط با Concurrency یا اجرای هم‌زمان قرار می‌گیرد و در CWE با شناسه CWE-362 شناخته می‌شود. MITRE این ضعف را شرایطی توصیف می‌کند که یک بخش از برنامه برای مدت کوتاهی به دسترسی انحصاری به منبع مشترک نیاز دارد، اما مسیر اجرایی دیگری می‌تواند در همان بازه آن منبع را تغییر دهد.

این آسیب‌پذیری در وب‌سایت‌های فروشگاهی، سیستم‌های پرداخت، APIها، کیف پول‌ها، برنامه‌های SaaS، سامانه‌های رزرو، پنل‌های کاربری، سیستم‌های امتیاز و پاداش و حتی قابلیت‌های امنیتی مانند بازیابی حساب اهمیت ویژه‌ای دارد؛ زیرا بسیاری از این سیستم‌ها براساس State مشترکی کار می‌کنند که ممکن است هم‌زمان از چند Worker، Server یا Request تغییر کند. Race Condition چگونه ایجاد می‌شود؟

Race Condition چگونه ایجاد می‌شود؟

برای درک Race Condition باید تفاوت میان «یک عملیات از دید کاربر» و «چند مرحله از دید Backend» را در نظر بگیریم.

فرض کنید کاربر می‌خواهد ۲۰ واحد از اعتبار حساب خود خرج کند. از دید رابط کاربری این عملیات شاید فقط یک کلیک باشد، اما Backend ممکن است چنین مراحلی داشته باشد:

1. موجودی حساب را بخوان.
2. بررسی کن موجودی حداقل 20 باشد.
3. عملیات موردنظر را انجام بده.
4. بیست واحد از موجودی کم کن.
5. تغییرات را ذخیره کن.

اگر موجودی کاربر ۳۰ باشد، در حالت عادی همه‌چیز درست کار می‌کند.

مشکل زمانی ایجاد می‌شود که دو درخواست تقریباً هم‌زمان وارد مرحله اول شوند:

Request A:
Balance = 30
30 >= 20 → Allowed

Request B:
Balance = 30
30 >= 20 → Allowed

هر دو درخواست یک State قدیمی را دیده‌اند و هر دو نتیجه گرفته‌اند که عملیات مجاز است.

اگر برنامه کنترل هم‌زمانی مناسبی نداشته باشد، ممکن است هر دو عملیات اجرا شوند؛ در حالی که منطق تجاری اجازه آن را نمی‌داده است.

این الگو یکی از مهم‌ترین ریشه‌های Race Condition در برنامه‌های وب است. Race Window چیست؟

Race Window چیست؟

Race Window به بازه زمانی گفته می‌شود که در آن درخواست یا Process دیگری می‌تواند وارد فرآیند شود و State مشترک را تغییر دهد.

این بازه ممکن است فقط چند میلی‌ثانیه باشد.

برای مثال:

Check balance
      ↓
      ↓   ← Race Window
      ↓
Update balance

اگر یک عملیات دیگر در این فاصله State را تغییر دهد، نتیجه مرحله Check دیگر الزاماً معتبر نیست.

PortSwigger نیز Race Condition در برنامه‌های وب را به اجرای هم‌زمان درخواست‌ها روی داده مشترک مرتبط می‌داند و فاصله زمانی‌ای را که Collision در آن امکان‌پذیر است Race Window می‌نامد.

هرچه عملیات بین Check و Update بیشتر باشد، معمولاً سطح ریسک نیز بالاتر می‌رود.

برای مثال اگر سیستم بعد از بررسی موجودی مجبور باشد:

به یک API خارجی متصل شود،

قیمت را محاسبه کند،

سفارش ایجاد کند،

Log بنویسد،

و سپس Balance را تغییر دهد،

Race Window بالقوه بزرگ‌تر می‌شود.

Race Condition چه تفاوتی با یک باگ معمولی دارد؟

Race Condition از باگ‌های عادی متفاوت است، زیرا ممکن است رفتار برنامه در بیشتر مواقع کاملاً صحیح باشد.

فرض کنید یک Endpoint هزاران بار به شکل Sequential یا پشت‌سرهم آزمایش شود و هیچ خطایی دیده نشود.

اما وقتی چند عملیات در یک بازه زمانی کوچک روی یک Resource مشترک اجرا شوند، مشکل ظاهر شود.

به همین دلیل Race Condition ممکن است:

در تست دستی دیده نشود،

به‌صورت تصادفی رخ دهد،

وابسته به Load سرور باشد،

روی محیط Production دیده شود اما روی Development رخ ندهد،

با افزایش تعداد Workerها آشکار شود،

و بازتولید آن دشوار باشد.

همین ویژگی باعث می‌شود برخی Race Conditionها مدت زیادی در سیستم باقی بمانند.

مفهوم Shared State در Race Condition

تقریباً تمام Race Conditionهای مهم یک عنصر مشترک دارند: Shared State.

Shared State یعنی اطلاعاتی که چند Request، Thread، Server یا Process می‌توانند آن را مشاهده یا تغییر دهند.

نمونه‌های رایج عبارت‌اند از:

موجودی حساب،

تعداد موجودی کالا،

وضعیت سفارش،

تعداد دفعات استفاده از کوپن،

توکن بازیابی رمز عبور،

Quota یک API،

تعداد صندلی‌های باقی‌مانده،

امتیاز کاربر،

Role یا Permission،

وضعیت پرداخت،

Session،

رکورد Database،

فایل مشترک،

Cache،

Lock،

و State ذخیره‌شده در Redis.

اگر چند مسیر اجرایی بتوانند بدون هماهنگی صحیح یک State مشترک را تغییر دهند، احتمال Race Condition ایجاد می‌شود.

Check-Then-Act چیست؟

یکی از مهم‌ترین الگوهای ناامن، Check-Then-Act است.

در این الگو ابتدا یک شرط بررسی می‌شود و سپس بر اساس نتیجه آن عملیاتی انجام می‌شود.

برای مثال:

if coupon.used == false:
    apply_discount()
    coupon.used = true

این کد از نظر منطقی ساده به نظر می‌رسد.

اما اگر دو اجرای هم‌زمان هر دو مقدار used = false را قبل از تغییر آن بخوانند، هر دو ممکن است تخفیف را اعمال کنند.

مشکل این نیست که شرط اشتباه نوشته شده است.

مشکل این است که «بررسی» و «تغییر State» دو عملیات جدا هستند.

روش امن‌تر آن است که این تصمیم در سطحی اجرا شود که عملیات به شکل Atomic انجام شود و سیستم اجازه ندهد State بین Check و Update توسط مسیر دیگری تغییر کند. TOCTOU چیست؟

TOCTOU چیست؟

Time-of-Check to Time-of-Use یا TOCTOU یکی از مهم‌ترین زیرگروه‌های Race Condition است و در CWE-367 نیز به شکل اختصاصی طبقه‌بندی شده است.

در TOCTOU سیستم ابتدا وضعیت یک Resource را بررسی می‌کند و کمی بعد بر اساس همان نتیجه از Resource استفاده می‌کند.

به شکل ساده:

Check Resource
      ↓
State changes
      ↓
Use Resource

تصور کنید برنامه ابتدا بررسی کند:

«آیا کاربر هنوز اجازه دسترسی دارد؟»

و پس از انجام چند عملیات دیگر داده را ارسال کند.

اگر Permission در فاصله میان Check و Use تغییر کند اما سیستم دوباره وضعیت را بررسی نکند یا فرآیند Atomic نباشد، تصمیم امنیتی ممکن است بر اساس State منقضی‌شده گرفته شود.

TOCTOU فقط مربوط به Database نیست.

این مشکل ممکن است در:

فایل‌ها،

Permissionها،

توکن‌ها،

Cache،

Session،

Resourceهای سیستم‌عامل،

و Serviceهای خارجی

نیز ایجاد شود.

Limit Overrun Race Condition چیست؟

یکی از رایج‌ترین شکل‌های Race Condition در وب زمانی رخ می‌دهد که برنامه محدودیتی را بررسی می‌کند اما این بررسی و ثبت مصرف به شکل Atomic انجام نمی‌شود.

فرض کنید کاربر فقط یک بار اجازه استفاده از یک پیشنهاد را دارد.

سیستم ممکن است چنین منطق مفهومی داشته باشد:

Check:
usage_count < 1 ?

Yes → Apply benefit

Then:
usage_count = usage_count + 1

اگر چند اجرای هم‌زمان قبل از Update مقدار صفر را ببینند، ممکن است بیش از یک عملیات تأیید شود.

این نوع ضعف را می‌توان در قابلیت‌هایی مانند:

کوپن یک‌بارمصرف،

پاداش ثبت‌نام،

محدودیت خرید،

دعوت دوستان،

API Quota،

برداشت مالی،

محدودیت تعداد رأی،

محدودیت دریافت هدیه،

و محدودیت استفاده از Promotion

مشاهده کرد.

PortSwigger این دسته را در قالب Limit Overrun Race Conditions بررسی می‌کند و آن را یکی از نمونه‌های مهم Race Conditionهای مبتنی بر Business Logic می‌داند.

Race Condition در فروشگاه‌های اینترنتی

فروشگاه‌های اینترنتی یکی از محیط‌هایی هستند که Concurrency اهمیت بسیار زیادی دارد.

موجودی کالا

فرض کنید تنها یک عدد از یک محصول باقی مانده باشد.

دو مشتری تقریباً هم‌زمان سفارش ثبت می‌کنند.

اگر Backend چنین الگویی داشته باشد:

stock = get_stock()

if stock > 0:
    create_order()
    stock = stock - 1

هر دو Request ممکن است مقدار stock = 1 را ببینند.

اگر Database یا Application محدودیت مناسبی نداشته باشد، دو سفارش برای یک موجودی ایجاد می‌شود.

نتیجه می‌تواند شامل:

Overselling،

لغو سفارش،

نارضایتی مشتری،

اختلال انبار،

یا ناسازگاری سیستم مالی

باشد.

کوپن تخفیف

کوپن‌هایی که باید فقط یک بار استفاده شوند نیز مستعد Race Condition هستند.

برنامه نباید صرفاً ابتدا وضعیت کوپن را بخواند و بعداً آن را Used کند.

شرط «استفاده‌نشده بودن» و عملیات «ثبت استفاده» باید در یک مدل هماهنگ و Atomic کنترل شوند.

ظرفیت محدود

Race Condition در رزرو هتل، بلیت، کلاس آموزشی، وقت ملاقات یا رویداد نیز می‌تواند باعث رزرو بیش از ظرفیت شود.

اگر ظرفیت ۱ باشد، دو Transaction نباید بتوانند مستقل از یکدیگر نتیجه بگیرند که ظرفیت موجود است. Race Condition در کیف پول و موجودی حساب

Race Condition در کیف پول و موجودی حساب

سیستم‌های مالی از حساس‌ترین محیط‌ها برای Race Condition هستند.

یک الگوی ناامن ممکن است چنین باشد:

balance = read_balance(user)

if balance >= amount:
    process_transaction()

write_balance(balance - amount)

این مدل خطرناک است، زیرا State خوانده‌شده ممکن است هنگام Write دیگر معتبر نباشد.

فرض کنید:

Balance = 100

دو Operation هر کدام قصد مصرف ۷۰ واحد دارند.

هر دو ممکن است مقدار ۱۰۰ را بخوانند.

هر دو شرط 100 >= 70 را صحیح در نظر بگیرند.

در این شرایط Business Rule نقض می‌شود.

در سیستم‌های واقعی، طراحی Ledger، Transaction، Constraint و مدل Consistency اهمیت بسیار بیشتری از یک if ساده دارد.

برای عملیات مالی بهتر است تصمیم نهایی تا حد امکان در لایه‌ای اعمال شود که بتواند Consistency را تضمین کند.

Race Condition در سیستم‌های پرداخت

پرداخت فقط به موجودی محدود نمی‌شود.

سناریوهای دیگری نیز وجود دارند:

پردازش چندباره Callback،

ثبت چندباره یک تراکنش،

ایجاد چند سفارش برای یک Payment،

اجرای مجدد Refund،

دریافت چند Webhook مشابه،

و Retry ناامن پس از Timeout.

یک خطای بسیار رایج این است که توسعه‌دهنده تصور کند هر Event خارجی فقط یک بار دریافت می‌شود.

در سیستم‌های توزیع‌شده، Retry کاملاً طبیعی است.

بنابراین عملیات مالی مهم باید Idempotent طراحی شوند.

یعنی اجرای مجدد همان درخواست منطقی نباید نتیجه مالی جدیدی ایجاد کند.

Idempotency چیست و چه ارتباطی با Race Condition دارد؟

Idempotency یعنی اجرای چندباره یک Operation مشخص، نتیجه نهایی متفاوتی از یک اجرای صحیح ایجاد نکند.

برای مثال اگر عملیات ایجاد پرداخت دارای یک Idempotency Key معتبر باشد:

Operation ID: ABC-123

Backend می‌تواند تشخیص دهد که این Operation قبلاً انجام شده است.

اگر همان درخواست دوباره دریافت شود، نباید تراکنش جدیدی ایجاد شود.

اما نکته مهم این است که خود ثبت Idempotency Key نیز باید Race-safe باشد.

الگوی زیر کافی نیست:

if key_not_exists:
    create_payment()
    save_key()

زیرا دو Request هم‌زمان ممکن است هر دو نتیجه بگیرند که Key وجود ندارد.

بهتر است Database با Unique Constraint یا مکانیزم Atomic دیگری تضمین کند که فقط یک رکورد برای آن Key ایجاد می‌شود.

Idempotency یک کنترل مهم برای:

پرداخت،

سفارش،

Refund،

ایجاد Resource،

Webhook،

Retry،

و Message Processing

محسوب می‌شود.

Race Condition در بازیابی رمز عبور

فرآیند Password Reset نیز ممکن است دارای State یک‌بارمصرف باشد.

برای مثال یک Reset Token باید پس از مصرف نامعتبر شود.

اگر منطق سیستم چنین باشد:

Check token valid
Change password
Invalidate token

Race Window میان مرحله اول و سوم می‌تواند مشکل‌ساز شود.

مدل امن‌تر باید مصرف Token را به‌گونه‌ای مدیریت کند که فقط یک مسیر بتواند آن را از حالت Valid به Used منتقل کند.

همچنین باید توجه داشت Race Condition ممکن است فقط در یک Endpoint رخ ندهد.

گاهی چند Endpoint مختلف روی یک State امنیتی مشترک اثر می‌گذارند.

Multi-Endpoint Race Condition چیست؟

در ساده‌ترین Race Condition، چند درخواست مشابه به یک Endpoint ارسال می‌شوند.

اما گاهی Race بین دو یا چند Endpoint متفاوت اتفاق می‌افتد.

برای مثال:

Endpoint اول:

/change-email

Endpoint دوم:

/verify-email

اگر State میان این مراحل به‌درستی کنترل نشود، ترتیب غیرمنتظره عملیات ممکن است Business Logic را بشکند.

مثال دیگر:

/apply-coupon

و:

/checkout

ممکن است هر Endpoint به‌تنهایی امن به نظر برسد، اما زمانی که هر دو روی Cart یا Order مشترک کار می‌کنند، وضعیت غیرمنتظره‌ای ایجاد شود.

این موضوع نشان می‌دهد امنیت Endpointها نباید کاملاً جدا از یکدیگر بررسی شود.

گاهی آسیب‌پذیری در State Machine کل برنامه قرار دارد.

Hidden Multi-Step Sequence چیست؟

بسیاری از عملیات وب از دید کاربر ساده هستند اما در Backend چند مرحله دارند.

برای مثال ایجاد حساب ممکن است شامل:

ایجاد User،

اختصاص Role،

ساخت Profile،

ثبت Verification State،

ارسال ایمیل،

ساخت Subscription،

و فعال‌سازی Featureها

باشد.

اگر Object پیش از کامل‌شدن تمام مراحل برای Requestهای دیگر قابل دسترسی شود، ممکن است Resource در یک وضعیت ناقص یا Partial مشاهده شود.

این نوع مشکل در معماری‌های پیچیده، Microserviceها و Event-driven Systemها اهمیت بیشتری دارد.

یک اصل مناسب این است که Stateهای میانی صریح و کنترل‌شده باشند.

به‌جای اینکه Object نیمه‌ساخته به‌طور ضمنی «فعال» تلقی شود، می‌توان State Machine مشخصی داشت:

CREATED
↓
PENDING_VERIFICATION
↓
ACTIVE

و Permissionها براساس State نهایی اعمال شوند.

Race Condition و Partial Construction

گاهی Resource قبل از تکمیل کامل تنظیمات امنیتی ایجاد می‌شود.

فرض کنید ایجاد حساب شامل دو مرحله باشد:

1. Create user
2. Apply permissions

اگر بین این دو مرحله حساب برای درخواست دیگری قابل استفاده باشد، برای مدت کوتاهی ممکن است Policy موردنظر روی آن اعمال نشده باشد.

این بازه کوتاه نیز یک Race Window است.

راهکارهای دفاعی شامل:

ایجاد Resource در وضعیت Disabled،

استفاده از Transaction،

عدم انتشار Object تا پایان Initialization،

و اجرای تغییر State نهایی به‌صورت Atomic

هستند.

Race Condition در Permission و Access Control

Race Condition فقط مسئله مالی نیست.

ممکن است روی Authorization نیز اثر بگذارد.

فرض کنید برنامه ابتدا Role کاربر را بررسی می‌کند.

سپس چند عملیات انجام می‌دهد.

و در نهایت Resource حساس را تغییر می‌دهد.

اگر Role یا Permission بین این مراحل تغییر کند، سیستم باید مشخص کند تصمیم Authorization چه مدت معتبر است.

در سیستم‌های بسیار حساس، Authorization ممکن است نیاز باشد نزدیک به محل استفاده Resource بررسی شود.

به‌ویژه در Architectureهایی که Permission از Service دیگری دریافت می‌شود، Cache شدن تصمیم‌های دسترسی باید با دقت انجام شود.

Race Condition در API Rate Limit و Quota

فرض کنید API به هر حساب روزانه ۱۰۰۰ درخواست اجازه می‌دهد.

یک مدل ناامن ممکن است:

مقدار Counter را بخواند،

بررسی کند کمتر از Limit است،

Request را اجرا کند،

سپس Counter را افزایش دهد.

اگر Counter Update اتمیک نباشد، چند Request هم‌زمان ممکن است یک مقدار قدیمی را مشاهده کنند.

راهکار معمول این است که Counter در سیستم مناسب مانند Database یا Data Store با عملیات Atomic افزایش یابد.

اگر Limit امنیتی یا مالی است، فقط Cache غیرقابل‌اعتماد نباید منبع نهایی حقیقت باشد.

چرا Transaction به‌تنهایی همیشه Race Condition را حل نمی‌کند؟

یکی از سوءبرداشت‌های رایج این است که:

«کد داخل Database Transaction است، پس Race Condition نداریم.»

این نتیجه الزاماً صحیح نیست.

Transaction یک مفهوم بسیار مهم است، اما رفتار آن به Isolation Level و نوع Queryها بستگی دارد.

برای مثال در PostgreSQL، در سطح Read Committed که سطح پیش‌فرض است، دو SELECT متوالی در یک Transaction ممکن است در شرایط خاص Stateهای متفاوتی ببینند، زیرا Transactionهای دیگر می‌توانند میان آن‌ها Commit شوند. مستندات PostgreSQL همچنین برای سناریوهای نیازمند کنترل Concurrent Update ابزارهایی مانند Row-level Lock و Isolation بالاتر را توضیح می‌دهد.

بنابراین صرفاً نوشتن:

BEGIN
SELECT ...
UPDATE ...
COMMIT

به معنی حل خودکار تمام Race Conditionها نیست.

توسعه‌دهنده باید مشخص کند چه Guaranteeای از Database نیاز دارد. Atomic Operation چیست؟

Atomic Operation چیست؟

Atomicity یعنی یک عملیات از دید سایر مسیرهای اجرایی به‌صورت یک واحد غیرقابل‌تقسیم رفتار کند.

برای مثال به جای:

balance = SELECT balance

if balance >= 20:
    balance = balance - 20
    UPDATE balance

در بسیاری از Databaseها می‌توان منطق را به شکلی طراحی کرد که شرط و تغییر در یک Statement انجام شوند:

UPDATE account
SET balance = balance - 20
WHERE id = ?
  AND balance >= 20

سپس Application بررسی کند آیا واقعاً Row به‌روزرسانی شده است یا خیر.

مزیت این روش آن است که Decision و Update به یک عملیات Database نزدیک‌تر می‌شوند.

البته طراحی دقیق به Database، Business Rule و معماری پروژه بستگی دارد.

Pessimistic Locking چیست؟

در Pessimistic Locking برنامه فرض می‌کند Conflict محتمل است و Resource را برای مدت لازم Lock می‌کند.

برای مثال در Database می‌توان Row موردنظر را برای Update Lock کرد.

در PostgreSQL، SELECT ... FOR UPDATE Row انتخاب‌شده را در برابر برخی تغییرات هم‌زمان Lock می‌کند و Lock تا پایان Transaction حفظ می‌شود.

مفهوم کلی:

BEGIN

Lock account row

Read balance

Validate operation

Update balance

COMMIT

تا زمانی که Lock در اختیار Transaction اول باشد، Transaction متعارض باید منتظر بماند یا طبق Policy مشخص رفتار کند.

مزایای Pessimistic Locking

پیاده‌سازی منطق Consistency در برخی سناریوها ساده‌تر می‌شود.

برای Resourceهای حساس مناسب است.

از Lost Update و برخی Race Conditionها جلوگیری می‌کند.

معایب

Lock Contention ایجاد می‌کند.

Latency را افزایش می‌دهد.

در صورت طراحی بد احتمال Deadlock وجود دارد.

Transactionهای طولانی Performance را کاهش می‌دهند.

به همین دلیل Critical Section باید تا حد امکان کوچک باشد.

Optimistic Locking چیست؟

در Optimistic Locking فرض می‌شود Conflict نسبتاً کم رخ می‌دهد.

به جای Lock کردن Resource از ابتدا، یک Version یا Revision نگهداری می‌شود.

مثلاً:

id: 10
balance: 100
version: 5

Application مقدار Version را می‌خواند.

هنگام Update می‌گوید:

UPDATE account
SET balance = 80,
    version = 6
WHERE id = 10
  AND version = 5

اگر Transaction دیگری Version را تغییر داده باشد، Update انجام نمی‌شود.

Application سپس می‌تواند عملیات را Reject یا طبق طراحی Retry کند.

این مدل در سیستم‌هایی که Conflict کمتر است مفید است.

اما Retry نیز باید با دقت طراحی شود؛ زیرا Retry کردن Blind یک عملیات غیرIdempotent ممکن است خود باعث مشکل شود.

Unique Constraint یکی از قوی‌ترین کنترل‌های ساده است

گاهی Business Rule را می‌توان مستقیماً در Database مدل کرد.

فرض کنید یک کاربر فقط یک بار اجازه دریافت یک Bonus خاص را دارد.

به جای اینکه Application ابتدا Query کند:

SELECT ...

و سپس تصمیم بگیرد رکورد جدید ایجاد کند، می‌توان Database را نیز وادار کرد که تکرار را نپذیرد.

برای مثال یک Unique Constraint روی ترکیب:

user_id + bonus_id

قرار گیرد.

Database در این صورت اجازه نمی‌دهد دو رکورد با همان ترکیب ثبت شوند.

PostgreSQL نیز Unique Constraint را مکانیزمی برای تضمین یکتا بودن مقدار یک Column یا ترکیبی از Columnها تعریف می‌کند.

Constraintها اهمیت زیادی دارند، زیرا حتی اگر چند Application Worker هم‌زمان تصمیم مشابه بگیرند، Data Layer همچنان Invariant را حفظ می‌کند.

Invariant چیست؟

Invariant یک قانون است که همیشه باید صحیح باقی بماند.

برای مثال:

موجودی حساب هرگز نباید منفی شود.

یا:

هر Payment ID فقط یک Order نهایی ایجاد می‌کند.

یا:

هر کاربر فقط یک بار این پاداش را دریافت می‌کند.

یا:

تعداد رزرو تأییدشده نباید از ظرفیت بیشتر شود.

یکی از بهترین رویکردهای Secure by Design این است که ابتدا Invariantهای سیستم مشخص شوند.

سپس بررسی شود چه لایه‌ای می‌تواند آن‌ها را به شکل قابل‌اعتماد enforce کند.

ممکن است این لایه شامل:

Database Constraint،

Atomic Update،

Transaction،

Lock،

State Machine،

یا ترکیبی از آن‌ها

باشد.

Queue و Serialization

راه دیگر کاهش Race Condition این است که عملیات مربوط به یک Resource به‌صورت Sequential پردازش شوند.

برای مثال تغییرات مالی یک Account ممکن است وارد Queue شوند و Worker آن‌ها را به‌ترتیب پردازش کند.

این مدل می‌تواند Collision را کاهش دهد.

اما Queue به‌تنهایی تمام مشکلات را حل نمی‌کند.

سیستم باید همچنان موارد زیر را در نظر بگیرد:

Message Duplicate،

Retry،

Worker Crash،

Out-of-order Delivery،

Idempotency،

و Transaction Boundary.

در سیستم‌های توزیع‌شده فرض «هر Message دقیقاً یک بار اجرا می‌شود» معمولاً فرض مناسبی برای طراحی Business Logic حساس نیست.

Distributed Lock چیست؟

در معماری تک‌سرور، Lock در حافظه Process شاید در برخی شرایط کافی به نظر برسد.

اما وقتی چند Application Server داریم:

Server A
Server B
Server C

Lock محلی Server A برای Server B قابل مشاهده نیست.

در این شرایط برخی تیم‌ها از Distributed Lock استفاده می‌کنند.

اما Distributed Lock نیز باید با احتیاط طراحی شود.

مواردی مانند:

Timeout،

Lease Expiration،

Network Partition،

Process Crash،

Clock Assumption،

و Ownership Lock

می‌توانند پیچیدگی ایجاد کنند.

بنابراین Distributed Lock نباید اولین انتخاب بدون بررسی باشد.

در بسیاری از Business Ruleها می‌توان Constraint و Atomic Operation را در Database اصلی پیاده‌سازی کرد و معماری ساده‌تری داشت.

Race Condition در Microserviceها

Microservice Architecture مشکل Race Condition را پیچیده‌تر می‌کند.

فرض کنید عملیات سفارش شامل Serviceهای زیر باشد:

Order Service

Inventory Service

Payment Service

Coupon Service

Notification Service

هر Service State مستقل خود را دارد.

دیگر یک Database Transaction ساده نمی‌تواند تمام فرآیند را Atomic کند.

در چنین شرایطی معمولاً باید مفاهیمی مانند:

State Machine،

Idempotency،

Saga،

Compensating Action،

Event Deduplication،

Optimistic Concurrency،

و Consistency Model

به‌صورت آگاهانه طراحی شوند.

نکته امنیتی مهم این است که Business Rule نباید بین Serviceها گم شود.

اگر Inventory Service تنها لایه‌ای است که می‌تواند موجودی را تغییر دهد، باید خودش Invariant مربوط به Stock را enforce کند.

نباید فقط به Order Service اعتماد کند که قبلاً موجودی را بررسی کرده است.

Race Condition در Cache

Cache نیز می‌تواند منبع Shared State باشد.

مشکل زمانی ایجاد می‌شود که برنامه یک مقدار را از Cache بخواند و بعد بر اساس آن تصمیم امنیتی بگیرد، در حالی که Source of Truth تغییر کرده است.

مثلاً:

permission = cache.get(user_permission)

اگر Permission کاربر لغو شود اما Cache هنوز مقدار قدیمی داشته باشد، برای مدتی Access Control قدیمی اعمال می‌شود.

این مسئله همیشه Race Condition کلاسیک نیست، اما در طراحی هم‌زمانی و Consistency نزدیک به همان خانواده مشکلات قرار می‌گیرد.

برای Stateهای امنیتی باید موارد زیر مشخص شوند:

TTL چقدر است؟

چه زمانی Cache Invalidate می‌شود؟

منبع نهایی حقیقت کجاست؟

آیا Stale Data قابل قبول است؟

اگر Cache در دسترس نباشد سیستم Fail Open است یا Fail Closed؟

آیا Rate Limiting جلوی Race Condition را می‌گیرد؟

خیر.

Rate Limiting می‌تواند Abuse را دشوارتر کند، اما راهکار اصلی Race Condition نیست.

حتی دو Request می‌توانند برای ایجاد Collision کافی باشند.

بنابراین محدودکردن کاربر به:

10 requests / second

باعث Atomic شدن Business Logic نمی‌شود.

Rate Limiting باید یک لایه دفاعی اضافی باشد، نه جایگزین:

Transaction،

Lock،

Constraint،

Idempotency،

و Atomic Update.

آیا WAF می‌تواند Race Condition را متوقف کند؟

Web Application Firewall یا WAF برای بسیاری از کنترل‌های Edge مفید است.

برای مثال:

Bot Detection،

Rate Limiting،

IP Reputation،

Request Filtering،

و برخی الگوهای مخرب.

اما WAF معمولاً از Business Invariantهای داخلی اطلاع ندارد.

WAF نمی‌داند:

«این Coupon فقط یک بار قابل استفاده است.»

یا:

«این حساب فقط ۵۰ واحد اعتبار دارد.»

یا:

«برای این صندلی فقط یک رزرو باید تأیید شود.»

بنابراین Race Condition باید در Application و Data Layer اصلاح شود.

آیا Mutex داخل Application کافی است؟

گاهی توسعه‌دهنده دور یک بخش از کد Mutex قرار می‌دهد.

این روش ممکن است در یک Process خاص صحیح کار کند.

اما اگر Application دارای چند Process یا چند Server باشد، هر Process Mutex خودش را دارد.

برای مثال:

Worker 1 → Lock A
Worker 2 → Lock B

از دید هر Worker Lock فعال است، اما هیچ هماهنگی مشترکی وجود ندارد.

به همین دلیل هنگام انتخاب Lock باید Scope آن مشخص باشد.

آیا Lock:

Thread-local است؟

Process-local است؟

Host-local است؟

Database-wide است؟

Distributed است؟

محدوده Lock باید با محدوده Shared State هماهنگ باشد.

Session Locking و محدودیت آن

برخی Frameworkها Sessionهای یک کاربر را Serialize می‌کنند.

این موضوع ممکن است بعضی Raceها را به‌طور تصادفی کاهش دهد.

اما تکیه کردن به Session Lock به‌عنوان کنترل اصلی خطرناک است.

زیرا Race ممکن است میان:

دو کاربر مختلف،

دو Session،

یک Worker و یک Background Job،

Webhook و User Request،

یا چند Service

رخ دهد.

کنترل اصلی باید روی Resource مشترک و Invariant قرار گیرد.

امنیت Race Condition در WordPress و WooCommerce

در WordPress و WooCommerce نیز Race Condition می‌تواند در Pluginها، Integrationهای سفارشی و قابلیت‌های Business Logic ایجاد شود.

نمونه‌های بالقوه شامل:

سیستم کیف پول سفارشی،

اعتبار حساب،

کوپن اختصاصی،

سیستم امتیاز،

ثبت موجودی خارجی،

رزرو،

محدودیت دانلود،

عضویت،

Referral،

پرداخت سفارشی،

و API Integration

هستند.

اگر افزونه‌ای چنین الگویی داشته باشد:

read option/meta
validate
calculate
write option/meta

باید بررسی شود آیا تغییر State در برابر اجرای هم‌زمان محافظت شده است یا خیر.

صرف استفاده از WordPress Nonce نیز Race Condition را حل نمی‌کند.

Nonce بیشتر برای کنترل Intent و برخی سناریوهای CSRF کاربرد دارد و مکانیزم Synchronization برای Shared State نیست.

پیامدهای آسیب‌پذیری Race Condition

شدت Race Condition کاملاً به Business Logic بستگی دارد.

MITRE برای CWE-362 پیامدهایی در حوزه Confidentiality، Integrity، Availability و Access Control مطرح می‌کند؛ بنابراین Race Condition فقط یک مشکل Performance محسوب نمی‌شود.

خسارت مالی

ممکن است شامل:

خرید بیش از اعتبار،

Refund چندباره،

دریافت چندباره Bonus،

استفاده تکراری از تخفیف،

یا ایجاد Transaction ناسازگار

باشد.

نقض Integrity

اطلاعات ممکن است وارد State غیرممکن شوند.

مثلاً:

available_stock = -3

یا:

order = CANCELLED
payment = CAPTURED
shipment = SENT

دورزدن Business Rule

Ruleهایی مانند:

«فقط یک بار»

«حداکثر سه مرتبه»

«فقط یک Resource»

«حداکثر ظرفیت ۱۰۰ نفر»

ممکن است شکسته شوند.

آسیب به Availability

Race Condition می‌تواند باعث:

Deadlock،

Resource Exhaustion،

Crash،

یا ایجاد Lock Contention شدید

شود.

مشکلات امنیتی

اگر Race روی Permission، Session، Token یا Object Creation اثر بگذارد، ممکن است Access Control نیز تحت تأثیر قرار گیرد.

چگونه Race Condition را در Code Review پیدا کنیم؟

در Code Review باید به دنبال الگوهایی بود که روی Shared State تصمیم‌گیری می‌کنند.

یک سؤال بسیار مفید این است:

«اگر این خط کد هم‌زمان توسط دو Worker اجرا شود چه اتفاقی می‌افتد؟»

الگوی Read-Check-Write

یکی از مهم‌ترین الگوها:

read
check
modify
write

است.

اگر این چهار مرحله Atomic نیستند، باید Concurrency بررسی شود.

استفاده از شمارنده

کدهایی مانند:

counter = counter + 1

ممکن است از دید زبان ساده باشند، اما همیشه Atomic نیستند. MITRE نیز درباره فرض اتمیک بودن عملیات ظاهراً ساده روی Shared Variable هشدار می‌دهد.

کنترل «وجود نداشتن»

الگوی زیر مهم است:

if not exists:
    insert

اگر Unique Constraint وجود نداشته باشد، چند Thread ممکن است هم‌زمان به نتیجه «وجود ندارد» برسند.

Stateهای یک‌بارمصرف

هر چیزی که باید «فقط یک بار» انجام شود، ارزش بررسی ویژه دارد.

مانند:

Token

Coupon

Bonus

Vote

Payment

Webhook

Invitation

Download

Verification Code

روش دفاعی تست Race Condition

تست Race Condition باید فقط روی سامانه‌ای انجام شود که مالک آن هستید یا مجوز صریح تست آن را دارید.

در محیط توسعه و Staging، هدف این است که بررسی شود Invariantها در شرایط Concurrency همچنان برقرار می‌مانند.

ابتدا Business Invariant را تعریف کنید

مثلاً:

Balance must never be negative.

یا:

Coupon redemption count <= 1 per user.

سپس تست هم‌زمانی بنویسید

به جای تمرکز روی HTTP Response، وضعیت نهایی Database نیز بررسی شود.

ممکن است همه Requestها 200 برگردانند ولی State نهایی اشتباه باشد.

تست را تکرار کنید

Race Condition ماهیت زمان‌بندی دارد.

یک Test ممکن است بار اول Pass شود و بار بعد Fail شود.

بنابراین Concurrency Test باید قابل تکرار باشد.

در CI از تست‌های کنترل‌شده استفاده کنید

برای Business Logicهای حساس می‌توان Automated Testهایی داشت که چند Execution کنترل‌شده را روی Resource مشترک اجرا کنند و سپس Invariant را بررسی کنند.

Logging برای شناسایی Race Condition

Race Condition اغلب از روی یک Log منفرد قابل تشخیص نیست.

بهتر است Eventها دارای Correlation ID باشند.

برای عملیات حساس می‌توان ثبت کرد:

Request ID

User ID

Resource ID

Operation ID

Idempotency Key

State Before

State After

Timestamp

Transaction Result

Version

Worker ID

البته اطلاعات حساس نباید بی‌دلیل در Log ذخیره شوند.

دنبال الگوهای غیرممکن بگردید

مثلاً:

یک Coupon با Limit یک، دو بار Redeem شده است.

یک Payment ID چند Order دارد.

Stock منفی شده است.

دو Refund موفق برای Transaction واحد وجود دارد.

یک Token یک‌بارمصرف چند Successful Event دارد.

این موارد می‌توانند Signal خوبی برای بررسی Race Condition باشند.

Monitoring و Alerting

بهتر است فقط Errorهای Application مانیتور نشوند.

Invariant Violation نیز باید Alert ایجاد کند.

برای مثال:

balance < 0

یا:

confirmed_bookings > capacity

یا:

payment_count(order_id) > 1

اگر Business Invariant قابل Query باشد، می‌توان Monitoring مستقلی برای آن ایجاد کرد.

این رویکرد باعث می‌شود حتی اگر Bug از تست‌ها عبور کرد، سریع‌تر شناسایی شود. معماری پیشنهادی برای جلوگیری از Race Condition

معماری پیشنهادی برای جلوگیری از Race Condition

یک معماری مناسب معمولاً چند لایه دارد.

لایه API

در این لایه می‌توان:

Authentication،

Authorization،

Input Validation،

Request ID،

Idempotency Key،

Rate Limit

را اعمال کرد.

لایه Business Logic

Business Rule باید به شکل واضح تعریف شود.

نباید فقط Front-end مسئول جلوگیری از عملیات تکراری باشد.

لایه Transaction

عملیاتی که باید با هم موفق یا Rollback شوند داخل Transaction مناسب قرار گیرند.

لایه Concurrency Control

براساس سناریو می‌توان از:

Atomic Update،

Pessimistic Lock،

Optimistic Lock،

Serializable Transaction،

Queue

یا سایر مکانیزم‌ها استفاده کرد.

لایه Data Integrity

Database باید تا حد امکان قوانین پایه را enforce کند.

مانند:

Unique Constraint

Foreign Key

Check Constraint

Not Null

لایه Observability

Logging، Monitoring و Alerting باید Invariantها را پوشش دهند.

انتخاب بین Lock، Atomic Update و Constraint

هیچ راهکار واحدی برای تمام Race Conditionها وجود ندارد.

Atomic Update

مناسب زمانی است که Business Rule را می‌توان در یک Statement بیان کرد.

Unique Constraint

برای جلوگیری از ایجاد داده تکراری بسیار قدرتمند است.

Pessimistic Lock

برای Resource حساس و Conflict محتمل مناسب است.

Optimistic Lock

برای سیستم‌هایی با Read زیاد و Conflict نسبتاً کم مناسب است.

Serializable Transaction

می‌تواند Guarantee قوی‌تری ارائه کند، اما هزینه و احتمال Retry را نیز باید در نظر گرفت.

Queue

برای Serialize کردن Workflowهای مشخص مفید است.

معماری صحیح ممکن است ترکیبی از این روش‌ها باشد.

اشتباهات رایج در جلوگیری از Race Condition

استفاده از Sleep

گاهی توسعه‌دهنده برای «حل» Race Condition بین عملیات Delay اضافه می‌کند.

این کار مشکل را حل نمی‌کند.

فقط Timing را تغییر می‌دهد.

Race Window همچنان وجود دارد.

بررسی مجدد فقط در Application

اگر چند Worker دارید، Check در Memory محلی کافی نیست.

Source of Truth باید در تصمیم Concurrency نقش داشته باشد.

اعتماد به Front-end

غیرفعال کردن Button پس از یک Click کنترل امنیتی نیست.

Request همچنان ممکن است چند بار به Backend برسد.

تکیه بر Rate Limit

دو Request نیز ممکن است برای Race کافی باشند.

استفاده از Cache به‌عنوان Lock بدون طراحی دقیق

Cache Operation باید واقعاً Atomic و semantics آن مشخص باشد.

Transaction بدون شناخت Isolation

صرف وجود BEGIN/COMMIT تضمین نمی‌کند Race رفع شده باشد.

Lock بیش از حد بزرگ

Lock کردن کل Table یا کل سیستم می‌تواند Performance را تخریب کند.

Critical Section باید حداقل لازم باشد.

Transaction بسیار طولانی

انجام HTTP Call خارجی درون Transaction طولانی ممکن است Lock را برای مدت زیادی نگه دارد.

Retry بدون Idempotency

Retry یک عملیات مالی بدون شناسه یکتا می‌تواند باعث Duplicate شود.

بررسی کردن و سپس Insert کردن

اگر Rule واقعاً Unique است، بهتر است Database نیز Unique Constraint داشته باشد.

Fail Open و Fail Closed در Concurrency

برای عملیات حساس باید مشخص باشد اگر Lock Service یا Data Store پاسخ ندهد چه اتفاقی می‌افتد.

Fail Open یعنی سیستم اجازه عملیات را بدهد.

Fail Closed یعنی عملیات رد یا متوقف شود.

برای قابلیت‌های کم‌ریسک شاید Availability مهم‌تر باشد.

اما در عملیات مالی یا امنیتی ممکن است Fail Closed منطقی‌تر باشد.

این تصمیم باید آگاهانه و مستند باشد.

نباید به‌صورت تصادفی از Exception Handling ناشی شود.

Deadlock چیست و چه ارتباطی با Locking دارد؟

Locking برای جلوگیری از Race Condition مفید است، اما طراحی نادرست Lock می‌تواند Deadlock ایجاد کند.

فرض کنید:

Transaction A:

Lock User
Lock Order

و Transaction B:

Lock Order
Lock User

ممکن است هر کدام منتظر Resource دیگری بمانند.

برای کاهش Deadlock:

ترتیب Lock گرفتن ثابت باشد،

Critical Section کوچک بماند،

Transaction کوتاه باشد،

و Application برای خطاهای Deadlock یا Serialization Strategy مشخص داشته باشد.

Concurrency Control همیشه Trade-off دارد.

Performance و امنیت Race Condition

برخی تیم‌ها از Lock اجتناب می‌کنند چون نگران Performance هستند.

این نگرانی معتبر است، اما حذف Synchronization بدون جایگزین امن خطرناک است.

هدف این نیست که تمام Application Serial شود.

بلکه باید کوچک‌ترین بخش ضروری که Invariant را محافظت می‌کند Synchronize شود.

MITRE نیز پیشنهاد می‌کند Critical Code تا حد لازم محدود شود تا هزینه Synchronization کاهش پیدا کند.

یک معماری خوب بین:

Correctness،

Security،

Latency،

Throughput،

و Availability

تعادل ایجاد می‌کند.

Secure by Design برای جلوگیری از Race Condition

بهترین زمان مقابله با Race Condition هنگام طراحی Business Logic است.

برای هر عملیات حساس این سؤال‌ها را مطرح کنید:

چه State مشترکی تغییر می‌کند؟

چه کسانی می‌توانند هم‌زمان آن را تغییر دهند؟

آیا Update اتمیک است؟

Business Invariant چیست؟

آیا Database آن را enforce می‌کند؟

اگر دو Request هم‌زمان برسند چه می‌شود؟

اگر Request Retry شود چه می‌شود؟

اگر Worker وسط عملیات Crash کند چه می‌شود؟

اگر Message دوباره Delivery شود چه می‌شود؟

اگر External Service Timeout شود چه می‌شود؟

اگر Transaction Rollback شود چه می‌شود؟

اگر Lock منقضی شود چه می‌شود؟

این سؤال‌ها کمک می‌کنند Race Condition قبل از Production شناسایی شود.

چک‌لیست جلوگیری از Race Condition در وب‌سایت

پیش از انتشار قابلیت‌های حساس بررسی کنید:

  • Shared Stateهای مهم شناسایی شده‌اند.
  • Business Invariantها مستند شده‌اند.
  • الگوهای Check-Then-Act بررسی شده‌اند.
  • عملیات مالی Idempotent هستند.
  • Idempotency Key در Data Layer یکتا است.
  • عملیات حساس از Atomic Update استفاده می‌کنند.
  • در صورت نیاز Row-level Lock وجود دارد.
  • Isolation Level آگاهانه انتخاب شده است.
  • Unique Constraintهای لازم تعریف شده‌اند.
  • Retry Strategy مشخص است.
  • Retry عملیات غیرIdempotent بدون کنترل انجام نمی‌شود.
  • Transactionها کوتاه نگه داشته شده‌اند.
  • External HTTP Request داخل Lock طولانی قرار نگرفته است.
  • Scope هر Lock مشخص است.
  • معماری چندسروری در طراحی Lock لحاظ شده است.
  • Queue Consumerها Duplicate Message را مدیریت می‌کنند.
  • Webhookها Idempotent هستند.
  • Business Stateهای میانی مشخص هستند.
  • Object نیمه‌ساخته قابل استفاده نیست.
  • Permissionهای امنیتی نزدیک به محل استفاده enforce می‌شوند.
  • Cache منبع نهایی تصمیم‌های حساس نیست مگر طراحی مناسب داشته باشد.
  • Concurrency Test برای قابلیت‌های مهم وجود دارد.
  • Monitoring برای Invariant Violation فعال است.
  • Eventهای حساس Correlation ID دارند.
  • عملیات مشکوک Audit می‌شوند.
  • Front-end به‌عنوان کنترل امنیتی اصلی در نظر گرفته نشده است.

تفاوت Race Condition با CSRF

Race Condition و CSRF دو آسیب‌پذیری متفاوت هستند.

CSRF زمانی رخ می‌دهد که مرورگر یک کاربر احراز هویت‌شده به اجرای Request ناخواسته وادار شود.

Race Condition به Timing و Concurrency روی Shared State مرتبط است.

یک Application می‌تواند در برابر CSRF کاملاً ایمن باشد ولی Race Condition داشته باشد.

همچنین CSRF Token یا SameSite Cookie باعث Atomic شدن Database Operation نمی‌شوند.

تفاوت Race Condition با IDOR

IDOR یا Broken Object Level Authorization به کنترل دسترسی روی Object مربوط است.

Race Condition مربوط به Synchronization و State مشترک است.

برای مثال:

اگر User A بتواند Order متعلق به User B را مشاهده کند، مشکل Access Control داریم.

اگر دو Request مجاز User A بتوانند محدودیت یک‌بارمصرف را هم‌زمان دور بزنند، مشکل Race Condition داریم.

گاهی این ضعف‌ها می‌توانند با یکدیگر ترکیب شوند، اما ریشه آن‌ها متفاوت است.

تفاوت Race Condition با Replay Attack

Replay Attack یعنی یک Request یا پیام معتبر دوباره استفاده شود.

Race Condition الزاماً نیازمند Replay نیست و چند Operation مستقل نیز می‌توانند Collision ایجاد کنند.

با این حال Idempotency می‌تواند در هر دو حوزه بسیار مفید باشد.

آیا HTTPS جلوی Race Condition را می‌گیرد؟

خیر.

HTTPS محرمانگی و Integrity ارتباط Client و Server را بهبود می‌دهد.

اما Race Condition داخل Application، Database یا Serviceها ایجاد می‌شود.

TLS نمی‌تواند Business Logic را Atomic کند.

آیا Race Condition فقط در سیستم‌های چند Thread رخ می‌دهد؟

خیر.

ممکن است Application Language ظاهراً Single-threaded باشد اما چند Process، Worker، Container یا Server مختلف Requestها را پردازش کنند.

حتی در یک Event Loop نیز عملیات Async می‌توانند بین Read و Write فاصله ایجاد کنند.

مسئله اصلی وجود اجرای Concurrent روی Shared State است، نه صرفاً Thread.

آیا افزایش سرعت سرور Race Condition را حل می‌کند؟

خیر.

ممکن است Race Window کوچک‌تر شود، اما ضعف معماری همچنان وجود دارد.

حتی تغییر Performance گاهی باعث آشکارتر یا پنهان‌تر شدن Race می‌شود.

راهکار واقعی Synchronization و حفظ Invariant است.

آیا Race Condition همیشه قابل سوءاستفاده است؟

خیر.

برخی Race Conditionها فقط باعث خطای تصادفی می‌شوند.

برخی نیازمند Timing بسیار خاص هستند.

برخی فقط در Load بالا دیده می‌شوند.

اما اگر Race روی Business Logic حساس قرار داشته باشد، نباید به دشوار بودن Trigger شدن آن به‌عنوان کنترل امنیتی تکیه کرد.

امنیت باید بر Correctness معماری استوار باشد.

سؤالات متداول درباره Race Condition

Race Condition چیست؟

Race Condition یا شرایط رقابتی زمانی رخ می‌دهد که نتیجه اجرای برنامه به ترتیب زمانی چند عملیات هم‌زمان روی یک Resource یا State مشترک وابسته باشد. اگر Synchronization کافی وجود نداشته باشد، Requestها ممکن است یک State قدیمی را مشاهده کنند و Business Logic را وارد وضعیت غیرمنتظره کنند.

آسیب‌پذیری Race Condition در وب‌سایت چگونه رخ می‌دهد؟

معمولاً Backend ابتدا وضعیتی مانند موجودی، تعداد استفاده، Permission یا ظرفیت را بررسی می‌کند و سپس در مرحله‌ای جدا آن را تغییر می‌دهد. اگر Request دیگری بین Check و Update همان State را تغییر دهد، تصمیم قبلی ممکن است دیگر معتبر نباشد.

Race Window چیست؟

Race Window فاصله زمانی‌ای است که در آن یک Operation دیگر می‌تواند با عملیات اصلی تداخل داشته باشد. این بازه ممکن است بین خواندن و نوشتن Database یا بین چند مرحله یک Workflow قرار داشته باشد.

TOCTOU چیست؟

TOCTOU مخفف Time-of-Check to Time-of-Use است. در این حالت وضعیت یک Resource بررسی می‌شود، اما قبل از استفاده نهایی State آن تغییر می‌کند و برنامه همچنان براساس نتیجه قدیمی تصمیم می‌گیرد.

آیا Race Condition فقط در سیستم‌های مالی مهم است؟

خیر. این ضعف می‌تواند موجودی، کوپن، رزرو، Permission، بازیابی حساب، Token، API Quota، فایل، Session، Cache، سیستم رأی‌گیری، عضویت و بسیاری از Workflowهای دیگر را تحت تأثیر قرار دهد.

بهترین راه جلوگیری از Race Condition چیست؟

یک پاسخ واحد وجود ندارد. بسته به سناریو می‌توان از Atomic Operation، Database Transaction صحیح، Row Lock، Optimistic Locking، Unique Constraint، Idempotency، Queue و State Machine استفاده کرد. نکته اصلی enforce کردن Business Invariant در لایه قابل‌اعتماد است.

آیا Database Transaction به‌تنهایی کافی است؟

خیر. رفتار Transaction به Isolation Level و Queryهای استفاده‌شده بستگی دارد. بعضی سناریوها به Lock، Atomic Update، Constraint یا Serializable Isolation نیاز دارند.

آیا SELECT FOR UPDATE جلوی Race Condition را می‌گیرد؟

در سناریوهای مناسب می‌تواند Row موردنظر را برای Update Lock کند و از تغییر هم‌زمان متعارض جلوگیری کند، اما باید داخل Transaction صحیح و با Critical Section کوتاه استفاده شود. استفاده نادرست می‌تواند باعث Lock Contention یا Deadlock شود.

Optimistic Locking چیست؟

Optimistic Locking معمولاً از Version Number استفاده می‌کند. Update تنها زمانی انجام می‌شود که Version رکورد از زمان خواندن تغییر نکرده باشد. اگر Version عوض شده باشد، سیستم Conflict را تشخیص می‌دهد.

Unique Constraint چگونه کمک می‌کند؟

Unique Constraint اجازه نمی‌دهد دو رکورد با کلید تجاری یکسان ایجاد شوند. برای Business Ruleهایی مانند «یک پاداش برای هر کاربر» یا «یک Operation برای هر Idempotency Key» این کنترل بسیار مؤثر است.

Idempotency Key چیست؟

Idempotency Key شناسه‌ای است که یک Operation منطقی را مشخص می‌کند. اگر همان Operation دوباره دریافت شود، Backend می‌تواند از اجرای مجدد اثر تجاری آن جلوگیری کند.

آیا Rate Limiting برای جلوگیری از Race Condition کافی است؟

خیر. Race ممکن است فقط با دو عملیات هم‌زمان ایجاد شود. Rate Limit می‌تواند لایه کمکی باشد اما Business Logic باید خودش Race-safe طراحی شود.

آیا WAF می‌تواند Race Condition را برطرف کند؟

خیر. WAF Business State داخلی مانند Balance، Stock یا Coupon Usage را نمی‌شناسد. اصلاح اصلی باید در Application، Transaction و Data Layer انجام شود.

آیا Disable کردن دکمه در Front-end کافی است؟

خیر. Client قابل اعتماد نیست و Request ممکن است به دلایل مختلف دوباره ارسال شود. Backend باید عملیات را مستقل از رفتار رابط کاربری ایمن کند.

چگونه Race Condition را پیدا کنیم؟

Code Review باید روی Shared State و الگوهای Read-Check-Write تمرکز کند. در محیط مجاز توسعه یا Staging نیز باید Concurrency Test نوشته شود و Invariant نهایی Database بررسی شود.

آیا Race Condition روی WordPress هم ممکن است؟

بله. افزونه‌ها یا کدهای سفارشی WordPress که روی موجودی، کیف پول، کوپن، عضویت، امتیاز، رزرو یا Metaهای مشترک کار می‌کنند ممکن است در صورت نبود Concurrency Control دچار Race Condition شوند.

جمع‌بندی

Race Condition یکی از آن دسته آسیب‌پذیری‌هایی است که ممکن است در نگاه اول بسیار ساده به نظر برسد اما در سیستم‌های واقعی پیامدهای گسترده‌ای ایجاد کند. این ضعف زمانی رخ می‌دهد که چند مسیر اجرایی به State مشترک دسترسی داشته باشند و نتیجه صحیح برنامه به ترتیب زمانی اجرای آن‌ها وابسته شود.

مهم‌ترین الگوی خطرناک، Check-Then-Act است: برنامه ابتدا یک شرط را بررسی می‌کند و بعداً State را تغییر می‌دهد. اگر عملیات دیگری در Race Window میان این مراحل وارد شود، هر دو Request ممکن است براساس اطلاعات قدیمی تصمیم‌گیری کنند.

آسیب‌پذیری Race Condition در وب‌سایت‌ها می‌تواند روی موجودی فروشگاه، کیف پول، پرداخت، کوپن، رزرو، API Quota، بازیابی رمز عبور، Permission، Webhook و بسیاری از Workflowهای Business Logic اثر بگذارد.

راهکار اصولی، تلاش برای «کندکردن» یا «محدودکردن» Requestها نیست. Business Rule باید در لایه‌ای enforce شود که Concurrency را درک می‌کند.

Atomic Update، Transaction صحیح، Row-level Lock، Optimistic Locking، Unique Constraint، Idempotency Key، Queue و State Machine از مهم‌ترین ابزارهای دفاعی هستند. انتخاب میان آن‌ها باید براساس نوع Shared State، میزان Conflict، Performance و Invariantهای سیستم انجام شود.

همچنین نباید فرض کرد داشتن Transaction، WAF، HTTPS، CSRF Protection یا Rate Limiting به‌تنهایی Race Condition را برطرف می‌کند. این کنترل‌ها اهداف متفاوتی دارند.

تیم‌های توسعه باید Invariantهای مهم سیستم را از مرحله طراحی مشخص کنند و از خود بپرسند: «اگر همین Operation دقیقاً در همین لحظه توسط چند Worker اجرا شود، آیا قانون تجاری همچنان برقرار می‌ماند؟»

اگر پاسخ قطعی نیست، آن بخش نیازمند بررسی Concurrency است.

امنیت Race Condition در نهایت به یک اصل بنیادی برمی‌گردد: عملیاتی که باید از دید Business Logic یک واحد غیرقابل‌تقسیم باشد، نباید در معماری واقعی به مجموعه‌ای از مراحل مستقل و قابل‌تداخل تبدیل شود.

مطالب مرتبط