VULN · آسیب‌پذیری‌ها منطق همروند بالا

Race Condition — شرایط رقابتی

Race Condition / TOCTOU

شرایط رقابتی وقتی به آسیب‌پذیری تبدیل می‌شود که برنامه بین «بررسی» و «اقدام» یک پنجره‌ی زمانی باز بگذارد و مهاجم چند درخواست را هم‌زمان داخل آن پنجره برساند — کاری که با حمله‌ی تک‌بسته‌ای روی HTTP/2 دیگر به شبکه‌ی سریع نیاز ندارد.

تیم فنی پی‌هانتر

شرایط رقابتی چیست؟

Race Condition یا «شرایط رقابتی» وقتی رخ می‌دهد که نتیجه‌ی نهایی یک عملیات به ترتیب و زمان‌بندی اجرای چند مسیر هم‌زمان وابسته باشد. در برنامه‌های وب شکل غالب آن الگوی TOCTOU است: Time-of-Check to Time-of-Use — یعنی فاصله‌ی میان لحظه‌ای که برنامه شرطی را بررسی می‌کند و لحظه‌ای که بر اساس آن اقدام می‌کند.

یک نمونه‌ی ساده که تقریباً در هر پایگاه کدی دیده می‌شود:

coupon = db.get(code)            // 1. check
if (coupon.used) return error
applyDiscount(order, coupon)     // 2. use
db.markUsed(code)                // 3. record

اگر دو درخواست هم‌زمان به گام ۱ برسند، هر دو مقدار used = false را می‌بینند و هر دو تخفیف را اعمال می‌کنند. هیچ‌کدام از این درخواست‌ها به‌تنهایی نامعتبر نیست؛ به همین دلیل هیچ فیلتر ورودی و هیچ WAFی این حمله را نمی‌بیند. PortSwigger این وضعیت را «عبور برنامه از یک زیرحالت موقت» توصیف می‌کند.

نکته‌ی مهم برای تستر: شرایط رقابتی غیرقطعی است. یک بار تلاش ناموفق، شاهدی بر نبود آسیب‌پذیری نیست.

دسته‌بندی شرایط رقابتی در وب

  • عبور از سقف (Limit Overrun) — کلاسیک‌ترین حالت: کد تخفیف یک‌بارمصرف چند بار اعمال می‌شود، کارت هدیه چند بار خرج می‌شود، برداشت بیش از موجودی انجام می‌شود، رأی یا دعوت‌نامه‌ی محدود چند بار مصرف می‌شود.
  • چند-اندپوینتی (Multi-endpoint) — درخواست‌های موازی به اندپوینت‌های متفاوتی که روی وضعیت مشترکی کار می‌کنند. مثال متعارف: تغییر محتوای سبد خرید در فاصله‌ی میان اعتبارسنجی پرداخت و ثبت نهایی سفارش.
  • تک-اندپوینتی (Single-endpoint) — درخواست‌های موازی به یک اندپوینت با مقادیر متفاوت. نمونه‌ای که PortSwigger توضیح می‌دهد: دو درخواست هم‌زمان بازنشانی رمز از یک نشست با دو نام کاربری متفاوت، که می‌تواند به ناهماهنگی توکن و کاربر و در نتیجه توکن معتبر برای حساب دلخواه بینجامد.
  • دور زدن محدودیت نرخ و شمارنده‌ها — شمارنده‌ی تلاش ورود، شمارنده‌ی تلاش کد یک‌بارمصرف، و سهمیه‌های API؛ همه در برابر افزایش غیراتمی شمارنده آسیب‌پذیرند.
  • ساخت جزئی (Partial Construction) — استفاده از شیئی که هنوز در میانه‌ی مقداردهی اولیه است، مثلاً کاربری که ثبت شده اما هنوز مرحله‌ی تأیید را کامل نکرده.
  • حساس به زمان — دو درخواست در یک ثانیه، توکنی یکسان تولید می‌کنند چون منبع «تصادفی» در واقع بر پایه‌ی زمان است.

حمله‌ی تک‌بسته‌ای: چرا «غیرعملی بودن از راه اینترنت» دیگر درست نیست

باور غلط رایج

«شرایط رقابتی فقط در شبکه‌ی محلی قابل بهره‌برداری است و از راه اینترنت عملی نیست.» این ادعا با حمله‌ی تک‌بسته‌ای (Single-Packet Attack) منسوخ شده است. پژوهش «Smashing the State Machine» از تیم PortSwigger نشان داد می‌توان چند درخواست کامل HTTP را در یک بسته‌ی TCP جای داد تا هم‌زمان به سرور برسند و اثر جیتر شبکه کاملاً حذف شود. در آن پژوهش ۳۰ درخواست از ملبورن به دوبلین در پنجره‌ی زیر یک میلی‌ثانیه اجرا شد.

جزئیات فنی که باید دقیق گفته شود:

  • این تکنیک بر HTTP/2 تکیه دارد: HTTP/2 اجازه می‌دهد چند درخواست به‌صورت هم‌زمان روی یک اتصال ارسال شوند، در حالی که در HTTP/1.1 درخواست‌ها ناچار پشت سر هم می‌آیند.
  • پراکندگی میانه‌ی زمان رسیدن درخواست‌ها با این روش حدود ۱ میلی‌ثانیه اندازه‌گیری شده، در برابر ۴ میلی‌ثانیه برای روش قدیمی‌تر «هم‌زمان‌سازی بایت آخر».
  • روش پیشین در HTTP/1.1، Last-Byte Synchronization بود: همه‌ی درخواست‌ها فرستاده می‌شوند اما بایت پایانی هرکدام نگه داشته و در آخر با هم رها می‌شود. این روش همچنان به جیتر حساس است.
  • گرم کردن اتصال (Connection Warming): ارسال یک یا چند درخواست مقدماتی روی همان اتصال، تا هزینه‌ی راه‌اندازی سمت سرور دسته را نامتوازن نکند و بتوان تأخیر شبکه را از تأخیر خود اندپوینت تفکیک کرد.

از نظر ابزار، در Burp Suite کافی است چند درخواست را در Repeater گروه کنید و گزینه‌ی «Send group in parallel» را بزنید؛ این گزینه در صورت پشتیبانی HTTP/2 خودِ حمله‌ی تک‌بسته‌ای را اجرا می‌کند. برای سناریوهای پیچیده‌تر با رهاسازی مرحله‌ای، افزونه‌ی Turbo Intruder با آرگومان gate استفاده می‌شود.

سناریوهای واقعی

۱. بازاستفاده از کد تخفیف. فروشگاهی که کد تخفیف ۵۰ درصدی یک‌بارمصرف دارد. ارسال هم‌زمان بیست درخواست اعمال کد، در نتیجه‌ی مشاهده‌شده باعث می‌شود تخفیف چند بار روی همان سفارش جمع شود. اثر مالی مستقیم و قابل محاسبه است — و همین آن را به یافته‌ای تبدیل می‌کند که مدیریت بلافاصله می‌فهمد.

۲. برداشت بیش از موجودی. کیف پول با موجودی مشخص. درخواست برداشت با مبلغ کامل موجودی، به‌صورت موازی چند بار ارسال می‌شود. اگر بررسی موجودی و کسر آن در یک تراکنش اتمی نباشند، چند برداشت موفق ثبت می‌شود و موجودی منفی می‌شود.

۳. دور زدن محدودیت نرخ روی کد یک‌بارمصرف. اندپوینت تأیید کد پیامکی که پس از پنج تلاش قفل می‌شود. ارسال موازی چند ده درخواست پیش از آنکه شمارنده به‌روزرسانی شود، عملاً محدودیت را بی‌اثر می‌کند و فضای جست‌وجوی کد چهاررقمی را در دسترس قرار می‌دهد.

هر سه سناریو ویژگی مشترکی دارند: هیچ درخواستی به‌تنهایی مخرب نیست. به همین دلیل نه اسکنر خودکار و نه WAF آن‌ها را تشخیص نمی‌دهد، و کشفشان کاملاً به تست نفوذ دستی وابسته است.

چگونه در تست نفوذ کشف می‌شود

  1. پیش‌بینی نقاط برخورد. هر جریانی که وضعیت مشترکی را می‌خواند و سپس تغییر می‌دهد نامزد است: موجودی، سهمیه، شمارنده، وضعیت یکتا (کد تخفیف، دعوت‌نامه، رزرو)، و هر عملیاتی که «فقط یک بار» باید انجام شود.
  2. گرم کردن اتصال با یک درخواست مقدماتی، برای پایدارسازی زمان‌بندی.
  3. کاوش انحراف رفتاری: دسته‌ای از درخواست‌های موازی بفرستید و به‌دنبال تفاوت در کد وضعیت، پیام خطا، زمان پاسخ یا وضعیت نهایی داده بگردید — نه لزوماً به‌دنبال موفقیت کامل حمله.
  4. اثبات مفهوم. پس از مشاهده‌ی انحراف، سناریو را تکرار کنید و اثر پایدار را مستند کنید (مثلاً موجودی نهایی حساب).
  5. تکرار. چون نتیجه غیرقطعی است، هر آزمون باید چند بار اجرا شود. در گزارش باید تعداد تلاش و تعداد موفقیت ذکر شود تا خواننده تصویر واقعی از قابلیت بهره‌برداری داشته باشد.
ملاحظه‌ی حرفه‌ای

آزمون شرایط رقابتی روی محیط عملیاتی می‌تواند داده‌ی واقعی را خراب کند — موجودی منفی، سفارش تکراری، رکورد ناسازگار. این آزمون باید در محیط Staging انجام شود، یا با مبالغ و حساب‌های آزمایشی و با هماهنگی صریح در دامنه‌ی کاری تست نفوذ. یافته‌ها هم باید همراه با راهنمای پاکسازی تحویل شوند.

چرا قفل سطح برنامه راه‌حل نیست

باور غلط رایج

«با یک Mutex یا قفل در سطح برنامه مشکل حل می‌شود.» قفل درون‌پردازه‌ای فقط داخل همان نمونه از برنامه معنا دارد. به‌محض اینکه سرویس به‌صورت افقی مقیاس بگیرد — دو کانتینر، دو Pod، دو ماشین پشت متعادل‌کننده‌ی بار — درخواست‌های رقیب روی نمونه‌های متفاوت می‌نشینند و هیچ‌کدام قفل دیگری را نمی‌بیند. این رایج‌ترین «رفع» شکست‌خورده‌ی این آسیب‌پذیری است و در بازآزمون دوباره باز می‌شود.

راه‌حل باید در همان لایه‌ای باشد که وضعیت مشترک زندگی می‌کند — یعنی پایگاه‌داده یا یک هماهنگ‌کننده‌ی توزیع‌شده. جزئیات در جعبه‌ی «پیشگیری / رفع» آمده است.

نگاشت به استانداردها

  • CWE-362 — Concurrent Execution using Shared Resource with Improper Synchronization، و CWE-367 — Time-of-check Time-of-use (TOCTOU) Race Condition.
  • در OWASP Top 10:2025 دسته‌ی مستقلی ندارد؛ بسته به ریشه‌ی مسئله معمولاً ذیل A06 (طراحی ناامن) یا — وقتی نتیجه دور زدن یک کنترل مجوزدهی باشد — ذیل A01 گزارش می‌شود.
  • WSTG-BUSL: آزمون زمان‌بندی فرایند و آزمون محدودیت دفعات استفاده از یک قابلیت، در بخش آزمون منطق کسب‌وکار راهنمای آزمون OWASP.

پرسش‌های متداول

آیا بهره‌برداری از Race Condition از راه اینترنت عملی است؟

بله. حمله‌ی تک‌بسته‌ای که بر HTTP/2 تکیه دارد، چند درخواست کامل را در یک بسته‌ی TCP جای می‌دهد و اثر جیتر شبکه را حذف می‌کند. پژوهش PortSwigger اجرای ۳۰ درخواست از ملبورن به دوبلین در پنجره‌ی زیر یک میلی‌ثانیه را گزارش کرده است. در Burp Repeater هم گزینه‌ی «Send group in parallel» همین کار را انجام می‌دهد.

آیا استفاده از Mutex در کد مشکل را حل می‌کند؟

خیر، مگر آنکه برنامه فقط یک نمونه داشته باشد و هرگز مقیاس نگیرد. قفل درون‌پردازه‌ای در محیط‌های چندنمونه‌ای بی‌اثر است. راه‌حل درست، به‌روزرسانی اتمی و شرطی در پایگاه‌داده، قید یکتایی، تراکنش با قفل سطری، یا قفل توزیع‌شده است.

آیا فایروال برنامه‌ی وب جلوی این حمله را می‌گیرد؟

خیر. هر درخواست در این حمله به‌تنهایی کاملاً معتبر است و هیچ الگوی مخربی ندارد؛ آنچه مشکل‌ساز است، هم‌زمانی آن‌هاست. WAF ساختاراً نسبت به این دسته نابیناست. محدودیت نرخ هم اگر خودش با شمارنده‌ی غیراتمی پیاده شده باشد، بخشی از مسئله است نه راه‌حل آن.

چطور مطمئن شویم رفع انجام‌شده واقعاً کار می‌کند؟

با بازآزمون موازی. همان دسته‌ی درخواست را چند بار به‌صورت هم‌زمان اجرا کنید و وضعیت نهایی داده را بررسی کنید. چون این آسیب‌پذیری غیرقطعی است، یک بار اجرا کافی نیست؛ در گزارش هم باید تعداد تلاش و تعداد موفقیت ثبت شود.

پیشگیری / رفع

به ترتیب اثربخشی:

  1. به‌روزرسانی اتمی و شرطی در یک دستور. مؤثرترین و ساده‌ترین راه‌حل: بررسی و تغییر را در یک عبارت واحد ادغام کنید و به تعداد سطرهای تغییریافته نگاه کنید — UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?. اگر صفر سطر تغییر کرد، عملیات باید رد شود.
  2. قید یکتایی در پایگاه‌داده به‌عنوان تور ایمنی: یک ایندکس یکتا روی «کد تخفیف + شناسه‌ی کاربر» یا «کلید Idempotency» باعث می‌شود درخواست دوم در سطح موتور پایگاه‌داده شکست بخورد، مستقل از منطق برنامه.
  3. تراکنش با سطح انزوای مناسب و قفل سطری صریح: SELECT … FOR UPDATE برای الگوی بخوان-سپس-بنویس، یا سطح انزوای SERIALIZABLE جایی که موتور از آن پشتیبانی می‌کند. توجه کنید که سطح انزوای پیش‌فرض بیشتر پایگاه‌داده‌ها این حفاظت را نمی‌دهد.
  4. کلید Idempotency برای عملیات مالی و هر عملیاتی که کلاینت ممکن است دوباره بفرستد.
  5. قفل توزیع‌شده (مثلاً بر پایه‌ی یک ذخیره‌ساز مشترک با اجاره‌ی زمان‌دار) برای جریان‌هایی که چند سرویس را در بر می‌گیرند و به یک پایگاه‌داده‌ی مشترک ختم نمی‌شوند.
  6. شمارنده‌های اتمی برای محدودیت نرخ و شمارنده‌ی تلاش — نه الگوی «بخوان، جمع کن، بنویس».
  7. هرگز قفل درون‌پردازه‌ای به‌عنوان راهکار نهایی. اگر برنامه بیش از یک نمونه دارد، این کنترل وجود ندارد.
  8. آزمون بازگشتی: پس از رفع، همان دسته‌ی درخواست موازی را دوباره اجرا کنید. بازآزمون در این دسته از یافته‌ها الزامی است، چون رفع‌های ناقص بسیار رایج‌اند.

اگر جریان‌های مالی یا سهمیه‌ای دارید و مطمئن نیستید در برابر اجرای موازی مقاوم‌اند، این دقیقاً چیزی است که تست نفوذ وب و بازبینی کد امن با هم پوشش می‌دهند.

→ بازگشت به دانشنامه
PUT IT TO THE TEST

امنیت سامانه‌ی شما را
به مهاجمان واگذار نکنید

تیم پی‌هانتر با دیدِ یک مهاجم واقعی، این آسیب‌پذیری‌ها و ده‌ها مورد دیگر را روی دارایی‌های شما می‌سنجد.