ข้ามไปยังเนื้อหา

Rate Limiting and Validation

WebSocket ที่เปิดอยู่คือคำเชิญที่เปิดกว้าง เมื่อ handshake เสร็จแล้ว client ส่ง message ได้มากเท่าที่ต้องการ, ใหญ่เท่าที่ต้องการ, ในรูปแบบใดก็ได้ที่ต้องการ — และ server จะพยายามจัดการทุกอันอย่างซื่อสัตย์ client ที่มี bug หรือที่ประสงค์ร้าย สามารถเปลี่ยนความใจกว้างนั้นให้เป็น denial of service บทเรียนนี้เป็นเรื่องของการป้องกันราคาถูกสามอย่างที่กันไม่ให้การเชื่อมต่อเดียวทำลายงานเลี้ยง: rate limiting, size cap, และ defensive validation

WebSocket ไม่มี throttle มาให้ในตัว loop while (true) ws.send(...) ในเบราว์เซอร์ยิง frame ได้หลายพันตัวต่อวินาที แต่ละตัวปลุก event loop, allocate, parse แล้วรัน handler ของคุณ คูณด้วยจำนวน client ที่เชื่อมต่ออยู่ แค่ peer ที่ประสงค์ร้ายตัวเดียวก็ทำ CPU อิ่มตัว memory หมด หรือยัดงานเข้า loop จนหน่วง message ของ client คนอื่นทุกคน ได้ คุณจึงต้องมีเพดาน และเพดานนั้นต้องเป็น ราย connection เพื่อไม่ให้ client เจ้าปัญหาตัวเดียวกินงบของคนอื่นไปหมด

อัลกอริทึมคลาสสิกคือ token bucket แต่ละ connection เป็นเจ้าของ bucket ที่มี capacity สูงสุดของ token Token จะ refill ในอัตราคงที่เมื่อเวลาผ่านไป ทุก message มีต้นทุน หนึ่ง token: ถ้ามี token อยู่ message จะถูกประมวลผลและ token หนึ่งตัวถูกเอาออก; ถ้า bucket ว่าง message จะถูกปฏิเสธ (หรือ connection ถูกปิดถ้าใช้งานในทางที่ผิดซ้ำ ๆ)

รูปร่างของสิ่งนี้คือสิ่งที่คุณต้องการพอดีสำหรับ traffic แบบ chat: ผู้ใช้ส่ง burst สั้น ๆ ได้ (ใช้ทั้ง bucket ในคราวเดียว) แต่อัตรา sustained ของพวกเขาถูกจำกัดด้วยอัตรา refill เลือก capacity เป็น เช่น 10 และ refill 5 ต่อวินาที แล้วคุณจะได้ “10 ใน burst แล้วหลังจากนั้น 5/วินาที”

flowchart TB
  R["Refill: +rate tokens/sec<br/>(capped at capacity)"] --> B["Bucket<br/>(0 … capacity tokens)"]
  M["Incoming message"] --> Q{"tokens >= 1 ?"}
  B --> Q
  Q -- "yes: spend 1 token" --> P["Process message"]
  Q -- "no: bucket empty" --> D["Reject / throttle<br/>(close on repeat abuse)"]
Token bucket: token เติมเมื่อเวลาผ่านไป; แต่ละ message ใช้หนึ่งตัว

Rate limiting จำกัด จำนวนเท่าไร; validation จำกัด อะไร จงมองทุก frame ที่เข้ามาว่าเป็นศัตรูจนกว่าจะพิสูจน์ได้ว่าไม่ใช่:

  • จำกัดขนาดก่อน ปฏิเสธ frame ที่ใหญ่กว่าลิมิตที่สมเหตุสมผล (ไม่กี่ KB สำหรับ chat, มากกว่านั้นสำหรับ payload ที่รู้จัก) ก่อน ลงมือ parse WebSocket library ส่วนใหญ่ให้ตั้ง maxPayload ได้ frame ที่ใหญ่เกินจะโดนปฏิเสธตั้งแต่ระดับ protocol — ตั้งค่านี้ไว้ แล้วตรวจซ้ำอีกทีใน handler ของคุณ
  • Parse ใน try/catch การ JSON.parse บน input ของผู้โจมตี ต้อง throw สักวัน ดักไว้ให้หมด อย่าปล่อยให้ frame ที่ผิดรูปทำการเชื่อมต่อหรือ process crash
  • Validate รูปร่าง ยืนยันว่า type เป็น string ที่รู้จัก, field ที่จำเป็นมีอยู่และมี type ถูกต้อง, string อยู่ในความยาว, number อยู่ในช่วง ปฏิเสธอะไรก็ตามที่ไม่ตรงกับรูปร่าง message ที่รู้จัก — อย่า “พยายามเต็มที่” ฝ่าขยะไป

เพราะ rate limiting เป็น logic ล้วน ๆ เดโมด้านล่างจึงรัน ในหน้าเว็บ — ไม่ต้องใช้ server โดย implement token bucket กับ validator แบบระแวดระวัง จากนั้น replay burst ของ message (บางอันถูกต้อง, บางอันผิดรูป, บางอันใหญ่เกิน) แล้วแสดงว่าอันไหนถูกยอมรับ, ถูก throttle, หรือถูกปฏิเสธ ฟังก์ชัน consume() และ validate() เดียวกันนี้หย่อนลงใน ws.on('message') handler จริงได้ทันที

JavaScript

ลองรันแล้วอ่านคำตัดสิน: frame ที่ผิดรูป ใหญ่เกิน และ type ที่ไม่รู้จัก จะโดน validator ปฏิเสธ และเมื่อ burst ดูด bucket ห้า token จนหมด frame ที่ตามมาก็โดน throttle ไม่ว่าเนื้อหาจะเป็นอย่างไร การ์ดสองอันที่เป็นอิสระต่อกัน — อันหนึ่งคุม rate, อันหนึ่งคุม รูปร่าง — และการเชื่อมต่อเดียวจะถล่มหรือทำให้ server สับสนไม่ได้อีกต่อไป

Rate Limit Typeจำกัดอะไรเหมาะกับ
Connection rateconnection ใหม่ต่อ IP ต่อวินาทีป้องกัน connection flood
Message ratemessage ต่อ connection ต่อวินาทีป้องกัน spam, abuse
Payload sizeขนาด message สูงสุดป้องกัน memory exhaustion
Room join ratejoin room ต่อวินาทีป้องกัน room flooding

ไม่มี Payload Size Limit อาการ:

  • client ส่ง message ขนาด 100MB — server buffer ล้น
  • ทำให้ memory spike และ อาจ crash server
  • ตั้ง max message size: ปฏิเสธ message ที่ใหญ่เกิน limit และปิด connection

ไม่ Validate Message Content อาการ:

  • รับ message JSON แล้วส่งต่อโดยไม่ validate schema
  • malformed message ทำให้ consumer crash
  • ใช้ Zod หรือ JSON Schema validate ทุก message ที่รับ ก่อน process

💡 ตัวอย่างจากของจริง

Discord:

  • rate limit message: 5 message ต่อ 5 วินาทีต่อ user
  • ส่งเกิน → 4008 opcode (Rate limited) → client ต้องรอก่อน retry

Binance WebSocket:

  • connection rate limit ต่อ IP
  • subscription limit: subscribe ได้สูงสุด N stream ต่อ connection
คุณสมบัติอะไรทำให้ token bucket เหมาะกับ traffic แบบแชท?
ทำไม rate limiting ถึงควรเป็นราย connection แทนที่จะเป็น global?
วิธีที่ปลอดภัยในการจัดการ JSON message frame ที่เข้ามาคืออะไร?