מערכת ה-bot-challenge של Cloudflare עלולה לחסום בשקט שליחת טפסי HTML רגילים, ולהפוך לחיצה פשוטה על תשלום למבוי סתום עבור משתמשים אמיתיים. מעבר מ-navigation POST טבעי לזרימת fetch-first משחזר את החוויה מבלי להתפשר על האבטחה.

למה הבעיה הזו חשובה

מפתח שחרר טופס תשלום שעבד בכל סדרת הבדיקות, עם curl ועל השרת המקומי. אותו טופס, כאשר לקוח השתמש ב-Chrome, זרק שגיאת אבטחה לאחר הלחיצה הראשונה והודעת “timeout-or-duplicate” בשנייה. הכישלון אילץ שלושה שחרורי תיקונים דחופים (hot-fix) ויום שלם של ניפוי שגיאות (debugging).

ה-edge הנסתר

הטופס נמצא בחבילת Astro בקוד פתוח המסתמכת על אלמנט <form> ב-HTML פשוט. כאשר המשתמש לוחץ על Pay, השרת משיב עם הפניה (redirect) בסטטוס 303 ל-Stripe, והדפדפן עוקב אחר ההפניה ללא JavaScript כלשהו. אתרים משתמשים בתבנית זו כגיבוי (fallback) כאשר סקריפטים מבוטלים.

Cloudflare יושבת לפני האתר ומריצה מנוע לזיהוי בוטים. עבור בקשות GET רגילות, היא יכולה להציג אתגר ביניים (CAPTCHA או בדיקת JavaScript). לאחר שהדפדפן עובר את האתגר, הבקשה ממשיכה.

עם זאת, לא ניתן להשהות navigation POST לצורך אתגר ואז להמשיך אותו כשהגוף (body) שלו נשאר שלם. ה-edge משליכה את הבקשה ומחזירה סטטוס 503, מה שמותיר את הדפדפן עם דף ריק או שגיאה כללית. דפדפני בדיקה אוטומטיים, הנושאים את אותה טביעת אצבע (fingerprint) ש-Cloudflare סומכת עליו, לעולם לא מפעילים את האתגר, ולכן הבעיה נשארת בלתי נראית עד שמשתמש אמיתי נכנס לאתר.

מה הלוגים חשפו

מעקב רשת (network trace) חי מסשן Chrome של משתמש הראה שתי בקשות מנוגדות לאותו endpoint:

  • Navigation POST ← תגובת 503, הלשונית נתקעה.
  • fetch() POST ← הבקשה הושלמה.

שתי הבקשות מקורן באותו origin, הן נשאו את אותם אישורים (credentials), והתרחשו באותו רגע. ההבדל היחיד היה בשיטת ההעברה (transport method). בקשת ה-fetch עקפה את זרימת הביניים שחוסמת navigation POSTs.

נתיבים שהובילו לשום מקום

המפתח ניסה סדרה של תיקונים שפספסו את שורש הבעיה:

  • חידוש טוקנים של Turnstile, מתוך הנחה שהם פגו תוקף.
  • הוספת טווחי IP לרשימה לבנה (whitelist), מתוך מחשבה שהחסימה מבוססת מיקום.
  • ביטול תוספים, ניקוי service workers ומחיקת עוגיות (cookies).

כל שינוי השאיר את השגיאה ללא שינוי, כיוון שהכישלון מקורו ב-upstream ב-edge, ולא בקוד הלקוח או השרת.

הפתרון הפרגמטי

במקום לכבות את ההגנה של Cloudflare, הטופס תוכנן מחדש כדי להשתמש בתבנית fetch-first:

  1. איסוף נתוני הטופס ושליחתם באמצעות fetch() כ-JSON payload.
  2. טיפול בתגובת השרת. אם השרת מחזיר URL עבור שער התשלום (payment gateway), יש להפעיל את location.assign() כדי לנווט לשם באמצעות בקשת GET פשוטה.

בקשות fetch אינן מפעילות את אתגר הביניים, ולכן ה-POST מגיע לשרת המקור (origin server). הפניית ה-GET העוקבת יכולה לעבור בבטחה דרך כל אתגר, שכן גופי (bodies) של בקשות GET הם ריקים וניתן לשלוח אותם שוב לאחר שהמשתמש עובר את האתגר.

המשמעויות עבור מפתחים

  • אמון משתמשים: טופס תשלום שנכשל בשקט שוחק את הביטחון ועלול להוביל לאובדן הכנסות.
  • עלויות תחזוקה: האירוע דרש שלושה שחרורי תיקון ויום שלם של חקירה.
  • נקודות עיוורות בבדיקות: הסתמכות בלעדית על סביבות בדיקה פנימיות עלולה להחמיץ כישלונות במקרי קצה שמופיעים רק "בשטח" (in the wild).

לקחים לקהילה הרחבה

  • בצעו instrumentation בדפדפנים אמיתיים. כאשר בעיה מופיעה רק עבור משתמשים אמיתיים, עצבו לוגים של רשת מסשנים אלו במקום לסמוך על הרצות בדיקה אוטומטיות.
  • התייחסו ל-edge כחלק מה-stack. Cloudflare יושבת בין הלקוח לשרת; ההתנהגות שלה משפיעה על האופן שבו יש לבנות את הבקשות.
  • בחרו ב-transport המתאים. בקשות navigation POST ו-fetch POST עוברות בנתיבים שונים ב-edge. תכננו APIs תוך התחשבות בהבדל הזה.
  • חשפו את פרטי השגיאה. הציגו את הודעות ה-503 או ה-"timeout-or-duplicate" בממשק המשתמש (UI) כדי שמפתחים יוכלו לראות את אופן הכישלון המדויק מבלי לחפור בלוגים.

מה כדאי לעקוב אחריו בהמשך

על מפתחים לבצע audit על כל תהליכי עבודה מבוססי טפסים המסתמכים על ניווט POST טבעי, במיוחד כאשר Cloudflare או שירותי אבטחת CDN דומים יושבים לפני האתר. הוספת עטיפת fetch קלה יכולה למנוע כישלונות דומים. כלי ניטור שתופסים קודי סטטוס שנוצרו ב-edge יתריעו על הבעיה לפני שהיא מגיעה ללקוחות.

שורה תחתונה: כאשר אתגרי הבוטים של Cloudflare פעילים, שליחת טופס HTML רגילה חשופה לכשל שקט. ניתוב מחדש של ה-POST באמצעות fetch() והשלמת התהליך באמצעות הפניה (redirect) מסוג GET עוקפים את המגבלה של ה-edge תוך שמירה על האבטחה. התייחסו ל-edge כאל קוד, ולא רק כאל קפיצה ברשת (network hop), ותכננו את ה-transports שלכם בהתאם.