การโจมตีแบบ flood จาก scraper บนแผน Free ของ Cloudflare: อะไรหยุดมันได้ และอะไรที่เราปล่อยทิ้งไว้
ปฏิบัติการ
เช้าวันหนึ่ง เว็บไซต์ listings ของเราเปิดไม่เข้า Cloudflare แสดง error 524 ให้ผู้เข้าชม ต้นทาง (origin) ใช้เวลาสองถึงห้านาทีต่อหน้า และไม่มีอะไรแจ้งเตือนเราเลยนานราวสิบชั่วโมง สาเหตุมาจาก scraper ที่วิ่งผ่าน residential proxies ด้วยความเร็วประมาณ 225 requests ต่อวินาที
ลักษณะของ flood
- หลายพัน IP address แต่ละตัวส่งมาหนึ่งถึงสาม requests เป็นอินเทอร์เน็ตบ้านในสหรัฐฯ สเปน ฝรั่งเศส บราซิล สหราชอาณาจักร และที่อื่น ๆ ซึ่งเป็นลักษณะทราฟฟิกที่เครือข่าย residential proxy ให้เช่า
- User agent เป็น Chrome บน Mac โดยมีเลขเวอร์ชันตั้งแต่ 103 ถึง 151
- แทบไม่มี cookie หรือ Referer เลย จาก 2,697 requests ที่เราสุ่มตรวจที่เซิร์ฟเวอร์ มีเพียง 4 requests ที่มี cookie และ 4 requests ที่มี Referer
- มี client สองประเภท ประมาณครึ่งหนึ่งไม่ส่ง
Sec-Fetch-*headers และไม่มี client hints: เป็นสคริปต์ที่คัดลอก user agent string มา อีกครึ่งหนึ่งส่ง browser headers ครบถ้วน: เป็น headless browser
ทุกหน้าถูก render สำหรับทุก request ทำให้แต่ละ worker ต้องเข้าคิว render หลายร้อยครั้งจนชนเพดาน CPU
ทำไมการจำกัดจำนวนต่อ IP address จึงช่วยไม่ได้
Rate limit นับจำนวน requests ต่อ IP address เมื่อแต่ละ IP address จากหลายพันตัวส่งมาเพียงหนึ่งถึงสาม requests ไม่มี IP address ใดเข้าใกล้ขีดจำกัดเลย การบล็อกประเทศจะบล็อกผู้เข้าชมของเราไปด้วย สิ่งที่ scraper ทำได้ยากในราคาถูกคือ การทำตัวให้เหมือน browser ที่เคยเข้าเว็บมาก่อน
ที่ Cloudflare's edge: กฎฟรีสามข้อ
แผน Free อนุญาตให้ตั้ง WAF custom rules ได้ห้าข้อต่อ zone เราใช้สามข้อ โดยทั้งหมดตั้งค่าเป็น managed challenge แทนการบล็อก เพื่อให้คนที่ถูกจับผิดคลิกผ่านได้:
- Browser ปลอม: GET request ที่ user agent มีคำว่า
Chrome/และไม่มีSec-Fetch-Modeheader ซึ่ง Chrome ทุกเวอร์ชันตั้งแต่ปี 2019 ส่ง header นี้ - Cloud ของ scraper: การขอหน้าเว็บจากเครือข่ายของ Alibaba, Tencent และ Huawei Cloud ซึ่งเป็นแหล่งที่มาส่วนใหญ่ของสิ่งที่เรา challenge
- กรณีฉุกเฉิน ปิดไว้เป็นค่าเริ่มต้น: GET request ที่ไม่มี cookie เลยและไม่มี Referer คนที่เคยเข้าเว็บ หรือคนที่เข้ามาผ่านลิงก์ จะผ่านไปได้โดยไม่ถูกแตะต้อง ส่วน scraper ที่ไม่เก็บ cookie จะเจอ challenge ทุก request เราเปิดใช้เฉพาะตอนเกิด flood เท่านั้น เพราะมันจะ challenge คนที่พิมพ์ URL เข้าไปใน browser ใหม่ด้วยเช่นกัน
ทั้งสามอย่างเปิดทางให้บอทที่ยืนยันแล้วของ Cloudflare, user agent ที่ระบุตัวเองว่าเป็น robots, ที่อยู่ของเราเอง, เส้นทาง API และ webhook รวมถึงไฟล์ที่ไม่ใช่หน้าเว็บ สองวันต่อมา คลื่นข้อมูลยังคงไหลต่อเนื่อง: ในหนึ่งชั่วโมง กฎสำหรับเบราว์เซอร์ปลอมส่งคำขอไปตรวจสอบ (challenge) มากกว่า 10,000 ครั้ง เหตุการณ์ 10,000 รายการที่ Cloudflare ส่งกลับมาต่อการค้นหาหนึ่งครั้งมาจาก 9,627 ที่อยู่ โดยไม่มีที่อยู่ใดเกิน 4 ครั้ง
ที่ต้นทาง: ประตูในแอปพลิเคชัน
ฝั่ง headless มี headers ของเบราว์เซอร์ครบถ้วน ดังนั้นกฎที่ขอบ (edge) เพียงอย่างเดียวจึงจับมันไม่ได้ขณะปิดกฎฉุกเฉิน ในตัวแอปพลิเคชัน เราวาง middleware ขนาดเล็กไว้หน้าตัวเรนเดอร์หน้าเว็บ:
- user agent ของ Chrome ที่ไม่มีทั้ง
Sec-Fetch-Modeและ client hints จะได้รับสถานะ 403 ก่อนเริ่มประมวลผล; - คนแปลกหน้า ซึ่งไม่มีคุกกี้ของเราและไม่มี Referer จะได้รับหน้าเว็บเฉพาะเมื่อ worker มีกำลังเหลือเท่านั้น ในอัตราไม่กี่หน้าต่อวินาที หากเกินกว่านั้น หน้า 503 ขนาด 400 ไบต์จะตั้งคุกกี้และโหลดหน้าใหม่หลังผ่านไปสองวินาที คนจริงจะได้หน้าเว็บในครั้งที่สอง ส่วน scraper ที่ไม่เก็บคุกกี้จะไม่ได้เลย
ในนาทีแรก ประตูปฏิเสธเบราว์เซอร์ปลอมประมาณ 6,700 ครั้ง และกันคนแปลกหน้าประมาณ 6,700 ครั้ง ขณะให้บริการคนแปลกหน้าประมาณ 9 หน้าต่อวินาที คำขอช้าลดลงเหลือศูนย์ CPU ของ worker ลดลงจากเพดานสูงสุดเหลือ 12 ถึง 15 เปอร์เซ็นต์ และหน่วยความจำลดลงจาก 4 ถึง 5 GB ต่อตัว เหลือ 0.65 GB
สิ่งที่เราปล่อยให้ปิดอยู่
- Bot Fight Mode. มันส่งคำขออัตโนมัติไปตรวจสอบ (challenge) และข้ามไม่ได้ด้วยกฎกำหนดเองในแผน Free อีกทั้งยังจับ webhook ของผู้ให้บริการชำระเงินของเรา
- Under Attack mode สำหรับทั้งโซน. มันส่งผู้เข้าชมทุกคน, API client และ webhook ผ่านหน้า interstitial กฎฉุกเฉินทำหน้าที่เดียวกันสำหรับคำขอหน้าเว็บของคนแปลกหน้าเท่านั้น
- กฎจำกัดอัตราของแผน Free. มันนับต่อที่อยู่ ซึ่งคลื่นข้อมูลนี้ไม่เคยทำให้ถึงเกณฑ์
- การบล็อก AI crawlers. เราต้องการให้ AI search assistants พบเรา และ crawlers เหล่านั้นไม่ใช่ปัญหา
บทสรุป
คลื่นข้อมูลผ่าน residential proxies ระบุได้ง่ายจากสิ่งที่ขาดหายไป: browser fetch headers, คุกกี้ และ Referers ให้ส่งคำขอเหล่านั้นไปตรวจสอบ (challenge) ที่ขอบ จำกัดคนแปลกหน้าให้อยู่ในขอบเขตที่ worker ของคุณเรนเดอร์ได้ และตรวจสอบให้แน่ใจว่ามีสิ่งนอกเซิร์ฟเวอร์ที่สังเกตได้เมื่อหน้าเว็บหยุดเปิด Night Watch รวมทั้งสามอย่างไว้ในชุดเครื่องมือเดียว: ตัวตรวจสอบระบบ, กฎพร้อมสวิตช์อัตโนมัติสำหรับกฎฉุกเฉิน และประตูสำหรับเว็บแอป Python