Cloudflare’s bot-challenge system สามารถทำให้การส่งฟอร์ม HTML แบบปกติล้มเหลวได้อย่างเงียบเชียบ เปลี่ยนจากการคลิกชำระเงินง่ายๆ ให้กลายเป็นทางตันสำหรับผู้ใช้งานจริง การเปลี่ยนจากการใช้ native navigation POST มาเป็น workflow แบบ fetch-first จะช่วยกู้คืนประสบการณ์การใช้งานกลับมาได้โดยไม่ลดทอนความปลอดภัย

ทำไมปัญหานี้ถึงสำคัญ

นักพัฒนาคนหนึ่งได้ปล่อยฟอร์มชำระเงินที่ทำงานได้ปกติในทุกชุดการทดสอบ (test suite) ทั้งการใช้ curl และบนเซิร์ฟเวอร์ท้องถิ่น แต่ฟอร์มเดียวกันนี้เมื่อลูกค้าใช้งานผ่าน Chrome กลับเกิดข้อผิดพลาดด้านความปลอดภัยหลังจากการคลิกครั้งแรก และแสดงข้อความ “timeout-or-duplicate” ในการคลิกครั้งที่สอง ความล้มเหลวนี้ทำให้ต้องออก hot-fix ถึงสามครั้งและต้องใช้เวลาแก้บั๊กเต็มๆ หนึ่งวัน

ปัญหาที่ซ่อนอยู่ตรง Edge

ฟอร์มนี้อยู่ในแพ็กเกจ Astro แบบ open-source ที่พึ่งพาองค์ประกอบ <form> ของ HTML แบบธรรมดา เมื่อผู้ใช้คลิก Pay เซิร์ฟเวอร์จะตอบกลับด้วยการ redirect แบบ 303 ไปยัง Stripe และเบราว์เซอร์จะทำตามการ redirect นั้นโดยไม่ใช้ JavaScript เลย เว็บไซต์ต่างๆ มักใช้รูปแบบนี้เป็น fallback เมื่อสคริปต์ถูกปิดใช้งาน

Cloudflare ทำหน้าที่อยู่หน้าเว็บไซต์และรันเอนจินตรวจจับบอท สำหรับคำขอ GET แบบปกติ มันสามารถแสดงหน้าตรวจสอบระหว่างทาง (interstitial challenge) เช่น CAPTCHA หรือการตรวจสอบด้วย JavaScript หลังจากที่เบราว์เซอร์ผ่านการตรวจสอบแล้ว คำขอก็จะดำเนินการต่อไป

อย่างไรก็ตาม การทำ navigation POST ไม่สามารถหยุดพักเพื่อรอการตรวจสอบแล้วค่อยดำเนินการต่อโดยที่ข้อมูลใน body ยังอยู่ครบได้ ตัว edge จะทิ้งคำขอนั้นไปและส่งสถานะ 503 กลับมา ทำให้เบราว์เซอร์ค้างอยู่ที่หน้าว่างหรือแสดงข้อผิดพลาดทั่วไป ส่วนเบราว์เซอร์ที่ใช้ในการทดสอบอัตโนมัติ (automated test browsers) ซึ่งมี fingerprint แบบเดียวกับที่ Cloudflare เชื่อถือ จะไม่ถูกกระตุ้นให้เกิดการตรวจสอบ ปัญหานี้จึงไม่ปรากฏให้เห็นจนกว่าจะมีผู้ใช้งานจริงเข้ามาที่ไซต์

สิ่งที่ Log เผยให้เห็น

การไล่ดู network trace แบบสดๆ จากเซสชัน Chrome ของผู้ใช้ แสดงให้เห็นคำขอสองแบบที่ขัดแย้งกันไปยัง endpoint เดียวกัน:

  • Navigation POST → ตอบกลับด้วย 503, แท็บค้าง
  • fetch() POST → คำขอเสร็จสมบูรณ์

ทั้งสองคำขอมาจาก origin เดียวกัน ใช้ credentials ชุดเดียวกัน และเกิดขึ้นในเวลาเดียวกัน ความแตกต่างเพียงอย่างเดียวคือวิธีการรับส่งข้อมูล (transport method) คำขอแบบ fetch สามารถข้ามขั้นตอน interstitial flow ที่บล็อกการทำ navigation POSTs ได้

แนวทางการแก้ไขที่ไม่ได้ผล

นักพัฒนาพยายามแก้ไขหลายวิธีแต่กลับไม่ตรงจุดต้นเหตุ:

  • พยายามต่ออายุ Turnstile tokens เพราะคิดว่ามันหมดอายุ
  • ทำ Whitelist ช่วง IP เพราะคิดว่าการบล็อกขึ้นอยู่กับตำแหน่งที่ตั้ง
  • ปิดส่วนขยาย (extensions), ล้าง service workers และลบ cookies

การเปลี่ยนแปลงแต่ละอย่างไม่ได้ช่วยให้อาการผิดพลาดหายไป เพราะความล้มเหลวเกิดขึ้นที่ต้นทางตรงระดับ edge ไม่ใช่ที่โค้ดฝั่ง client หรือ server

การแก้ไขเชิงปฏิบัติ

แทนที่จะปิดการป้องกันของ Cloudflare ฟอร์มนี้ถูกออกแบบใหม่ให้ใช้รูปแบบ fetch-first:

  1. รวบรวมข้อมูลฟอร์ม และส่งด้วย fetch() ในรูปแบบ JSON payload
  2. จัดการการตอบกลับของเซิร์ฟเวอร์ หากเซิร์ฟเวอร์ส่ง URL สำหรับ gateway การชำระเงินกลับมา ให้เรียกใช้ location.assign() เพื่อนำทางไปยังที่นั่นด้วยคำขอ GET แบบธรรมดา

คำขอแบบ fetch จะไม่กระตุ้นให้เกิด interstitial challenge ดังนั้น POST จึงส่งไปถึง origin server ได้ ส่วนการ redirect แบบ GET ที่ตามมาสามารถผ่านการตรวจสอบใดๆ ได้อย่างปลอดภัย เนื่องจาก GET body จะว่างเปล่าและสามารถส่งซ้ำได้หลังจากผู้ใช้ผ่านการตรวจสอบแล้ว

ผลกระทบต่อผู้พัฒนา

  • ความเชื่อมั่นของผู้ใช้: ฟอร์มชำระเงินที่ล้มเหลวเงียบๆ จะทำลายความเชื่อมั่นและอาจนำไปสู่การสูญเสียรายได้
  • ภาระในการบำรุงรักษา: เหตุการณ์นี้ต้องใช้การออก patch ถึงสามครั้งและใช้เวลาตรวจสอบเต็มๆ หนึ่งวัน
  • จุดบอดในการทดสอบ: การพึ่งพาเพียงสภาพแวดล้อมการทดสอบภายในอาจทำให้พลาดความล้มเหลวในกรณีที่เป็น edge-case ซึ่งจะปรากฏขึ้นเมื่อใช้งานจริงเท่านั้น

บทเรียนสำหรับชุมชนนักพัฒนาในวงกว้าง

  • ใช้เบราว์เซอร์จริงในการตรวจสอบ (Instrument real browsers): เมื่อปัญหาเกิดขึ้นเฉพาะกับผู้ใช้งานจริง ให้เก็บ network logs จากเซสชันเหล่านั้นแทนที่จะเชื่อถือเพียงผลการทดสอบอัตโนมัติ
  • มองว่า edge เป็นส่วนหนึ่งของ stack: Cloudflare อยู่ระหว่าง client และ server พฤติกรรมของมันส่งผลต่อโครงสร้างของคำขอที่ต้องออกแบบ
  • เลือกวิธีการรับส่งข้อมูลที่เหมาะสม: Navigation POSTs และ fetch POSTs เดินทางผ่านเส้นทางที่ต่างกันที่ระดับ edge ควรออกแบบ API โดยคำนึงถึงความแตกต่างนี้
  • แสดงรายละเอียดข้อผิดพลาด: แสดงข้อความ 503 หรือ “timeout-or-duplicate” บน UI เพื่อให้นักพัฒนาเห็นรูปแบบความล้มเหลวที่ชัดเจนโดยไม่ต้องไปขุดหาใน logs

สิ่งที่ควรระวังต่อไป

นักพัฒนาควรตรวจสอบ workflow ที่ใช้ฟอร์มซึ่งพึ่งพาการทำ native POST navigation โดยเฉพาะอย่างยิ่งเมื่อมี Cloudflare หรือบริการความปลอดภัย CDN ที่คล้ายกันอยู่หน้าเว็บไซต์ การเพิ่ม fetch wrapper น้ำหนักเบาจะช่วยป้องกันความล้มเหลวในลักษณะนี้ได้ เครื่องมือตรวจสอบที่สามารถจับรหัสสถานะ (status codes) ที่สร้างโดย edge จะช่วยแจ้งเตือนปัญหาได้ก่อนที่จะถึงมือลูกค้า

สรุปประเด็นสำคัญ: เมื่อระบบตรวจสอบบอทของ Cloudflare ทำงาน การส่งฟอร์ม HTML แบบปกติอาจเสี่ยงต่อการเกิดความล้มเหลวโดยไม่แจ้งเตือน การเปลี่ยนเส้นทางการส่ง POST ผ่าน fetch() และทำให้กระบวนการเสร็จสมบูรณ์ด้วยการทำ GET redirect จะช่วยเลี่ยงข้อจำกัดของ edge ในขณะที่ยังคงรักษาความปลอดภัยไว้ได้อย่างครบถ้วน จงมองว่า edge คือโค้ด ไม่ใช่แค่จุดเชื่อมต่อเครือข่าย และออกแบบการรับส่งข้อมูลของคุณให้สอดคล้องกัน