سیستم چالش ربات Cloudflare می‌تواند ارسال‌های معمولی فرم HTML را به‌طور بی‌صدا از کار بیندازد و یک کلیک ساده برای پرداخت را به بن‌بستی برای کاربران واقعی تبدیل کند. تغییر درخواست از یک POST ناوبری بومی (native navigation POST) به یک جریان fetch-first، تجربه کاربری را بدون به خطر انداختن امنیت بازیابی می‌کند.

چرا این مشکل اهمیت دارد

یک توسعه‌دهنده فرم پرداختی را منتشر کرد که در هر مجموعه تست، با curl و روی سرور محلی به‌درستی کار می‌کرد. همان فرم، وقتی مشتری از Chrome استفاده می‌کرد، پس از اولین کلیک یک خطای امنیتی و در کلیک دوم پیام “timeout-or-duplicate” را نمایش می‌داد. این شکست باعث انتشار سه نسخه اصلاحی فوری (hot-fix) و یک روز کامل عیب‌یابی (debugging) شد.

لبه‌ی پنهان (The hidden edge)

این فرم در یک بسته متن‌باز Astro قرار دارد که به یک عنصر ساده <form> در HTML متکی است. وقتی کاربر روی Pay کلیک می‌کند، سرور با یک ریدایرکت 303 به Stripe پاسخ می‌دهد و مرورگر بدون هیچ جاوااسکریپتی، ریدایرکت را دنبال می‌کند. سایت‌ها از این الگو به‌عنوان یک جایگزین (fallback) زمانی که اسکریپت‌ها غیرفعال هستند، استفاده می‌کنند.

Cloudflare در جلوی سایت قرار دارد و یک موتور تشخیص ربات را اجرا می‌کند. برای درخواست‌های معمولی GET، می‌تواند یک چالش میانی (interstitial challenge) مانند CAPTCHA یا بررسی جاوااسکریپت نشان دهد. پس از اینکه مرورگر از چالش عبور کرد، درخواست ادامه می‌یابد.

با این حال، یک درخواست POST ناوبری (navigation POST) را نمی‌توان برای چالش متوقف کرد و سپس با بدنه (body) دست‌نخورده آن از سر گرفت. لبه (edge) درخواست را دور می‌اندازد و وضعیت 503 را برمی‌گرداند که باعث می‌شود مرورگر با یک صفحه خالی یا یک خطای عمومی مواجه شود. مرورگرهای تست خودکار، که دارای همان fingerprint مورد اعتماد Cloudflare هستند، هرگز چالش را فعال نمی‌کنند، بنابراین مشکل تا زمانی که یک کاربر واقعی وارد سایت نشود، پنهان می‌ماند.

آنچه لاگ‌ها فاش کردند

یک ردگیری زنده شبکه (network trace) از نشست Chrome یک کاربر، دو درخواست متضاد به یک نقطه پایانی (endpoint) یکسان را نشان داد:

  • Navigation POST ← پاسخ 503، تب (tab) هنگ کرد.
  • fetch() POST ← درخواست با موفقیت انجام شد.

هر دو درخواست از یک مبدأ (origin) بودند، اعتبارنامه‌های (credentials) یکسانی داشتند و در یک لحظه رخ دادند. تنها تفاوت در روش انتقال (transport method) بود. درخواست fetch از جریان میانی که مانع POSTهای ناوبری می‌شود، عبور کرد.

مسیرهایی که به جایی نرسیدند

توسعه‌دهنده مجموعه‌ای از اصلاحات را امتحان کرد که علت اصلی را نادیده می‌گرفتند:

  • بازسازی توکن‌های Turnstile، با این فرض که منقضی شده‌اند.
  • لیست سفید کردن محدوده‌های IP، با این تصور که مسدودسازی مبتنی بر مکان است.
  • غیرفعال کردن افزونه‌ها، پاک کردن service workerها و حذف کوکی‌ها.

هر تغییر، خطا را بدون تغییر باقی گذاشت زیرا شکست در بخش بالادستی در لبه (upstream at the edge) رخ می‌داد، نه در کد کلاینت یا سرور.

راهکار عملی

به‌جای خاموش کردن محافظت Cloudflare، فرم برای استفاده از الگوی fetch-first بازطراحی شد:

  1. داده‌های فرم را جمع‌آوری کنید و آن‌ها را به عنوان یک payload JSON با fetch() ارسال کنید.
  2. پاسخ سرور را مدیریت کنید. اگر سرور یک URL برای درگاه پرداخت برگرداند، از location.assign() برای ناوبری به آنجا با یک درخواست ساده GET استفاده کنید.

درخواست‌های fetch چالش میانی را فعال نمی‌کنند، بنابراین POST به سرور اصلی می‌رسد. ریدایرکت GET بعدی می‌تواند به‌راحتی از هر چالشی عبور کند، زیرا بدنه (body) درخواست‌های GET خالی است و می‌تواند پس از عبور کاربر از چالش، دوباره ارسال شود.

مخاطرات برای توسعه‌دهندگان

  • اعتماد کاربر: یک فرم پرداخت که به‌طور بی‌صدا با شکست مواجه می‌شود، اعتماد را از بین می‌برد و می‌تواند منجر به از دست رفتن درآمد شود.
  • هزینه نگهداری: این حادثه مستلزم انتشار سه نسخه اصلاحی و یک روز کامل تحقیق بود.
  • نقاط کور تست: تکیه صرف بر محیط‌های تست داخلی می‌تواند باعث از دست رفتن شکست‌های حالت‌های خاص (edge-case) شود که فقط در دنیای واقعی ظاهر می‌شوند.

درس‌هایی برای جامعه گسترده‌تر

  • از مرورگرهای واقعی استفاده کنید. وقتی مشکلی فقط برای کاربران واقعی ظاهر می‌شود، به‌جای اعتماد به اجرای تست‌های خودکار، لاگ‌های شبکه را از همان نشست‌ها استخراج کنید.
  • با لبه (edge) به‌عنوان بخشی از پشته (stack) برخورد کنید. Cloudflare بین کلاینت و سرور قرار دارد؛ رفتار آن بر نحوه ساختار درخواست‌ها تأثیر می‌گذارد.
  • روش انتقال مناسب را انتخاب کنید. درخواست‌های Navigation POST و fetch POST از مسیرهای متفاوتی در لبه عبور می‌کنند. APIها را با در نظر گرفتن این تفاوت طراحی کنید.
  • جزئیات خطا را نمایش دهید. پیام‌های 503 یا “timeout-or-duplicate” را در رابط کاربری (UI) نمایش دهید تا توسعه‌دهندگان بتوانند بدون جستجو در لاگ‌ها، حالت دقیق شکست را ببینند.

آنچه باید در آینده زیر نظر داشت

توسعه‌دهندگان باید هر جریان کاری مبتنی بر فرم را که به ناوبری POST بومی متکی است، بازرسی (audit) کنند، به‌ویژه زمانی که Cloudflare یا سرویس‌های امنیتی CDN مشابه در جلوی سایت قرار دارند. افزودن یک wrapper سبک fetch می‌تواند از شکست‌های مشابه جلوگیری کند. ابزارهای مانیتورینگ که کدهای وضعیت تولید شده در لبه را ثبت می‌کنند، مشکل را پیش از رسیدن به مشتریان شناسایی خواهند کرد.

نکته کلیدی: زمانی که چالش‌های ربات Cloudflare فعال هستند، ارسال فرم ساده HTML در معرض شکست بی‌صدا قرار می‌گیرد. تغییر مسیر درخواست POST از طریق fetch() و تکمیل فرآیند با یک redirect از نوع GET، محدودیت‌های edge را دور می‌زند و در عین حال امنیت را حفظ می‌کند. با edge مانند کد رفتار کنید، نه صرفاً یک گام شبکه، و پروتکل‌های انتقال خود را بر همین اساس طراحی کنید.