Rate Limiting and Validation
WebSocket ที่เปิดอยู่คือคำเชิญที่เปิดกว้าง เมื่อ handshake เสร็จแล้ว client ส่ง message ได้มากเท่าที่ต้องการ, ใหญ่เท่าที่ต้องการ, ในรูปแบบใดก็ได้ที่ต้องการ — และ server จะพยายามจัดการทุกอันอย่างซื่อสัตย์ client ที่มี bug หรือที่ประสงค์ร้าย สามารถเปลี่ยนความใจกว้างนั้นให้เป็น denial of service บทเรียนนี้เป็นเรื่องของการป้องกันราคาถูกสามอย่างที่กันไม่ให้การเชื่อมต่อเดียวทำลายงานเลี้ยง: rate limiting, size cap, และ defensive validation
client ถล่มคุณได้
หัวข้อที่มีชื่อว่า “client ถล่มคุณได้”WebSocket ไม่มี throttle มาให้ในตัว loop while (true) ws.send(...) ในเบราว์เซอร์ยิง frame ได้หลายพันตัวต่อวินาที แต่ละตัวปลุก event loop, allocate, parse แล้วรัน handler ของคุณ คูณด้วยจำนวน client ที่เชื่อมต่ออยู่ แค่ peer ที่ประสงค์ร้ายตัวเดียวก็ทำ CPU อิ่มตัว memory หมด หรือยัดงานเข้า loop จนหน่วง message ของ client คนอื่นทุกคน ได้ คุณจึงต้องมีเพดาน และเพดานนั้นต้องเป็น ราย connection เพื่อไม่ให้ client เจ้าปัญหาตัวเดียวกินงบของคนอื่นไปหมด
Token bucket: rate limit ที่ยอมให้มี burst
หัวข้อที่มีชื่อว่า “Token bucket: rate limit ที่ยอมให้มี burst”อัลกอริทึมคลาสสิกคือ 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)"] Validate ทุก message อย่างระแวดระวัง
หัวข้อที่มีชื่อว่า “Validate ทุก 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 ที่รู้จัก — อย่า “พยายามเต็มที่” ฝ่าขยะไป
เดโม token-bucket และ validation ที่รันได้จริง
หัวข้อที่มีชื่อว่า “เดโม token-bucket และ validation ที่รันได้จริง”เพราะ rate limiting เป็น logic ล้วน ๆ เดโมด้านล่างจึงรัน ในหน้าเว็บ — ไม่ต้องใช้ server โดย implement token bucket กับ validator แบบระแวดระวัง จากนั้น replay burst ของ message (บางอันถูกต้อง, บางอันผิดรูป, บางอันใหญ่เกิน) แล้วแสดงว่าอันไหนถูกยอมรับ, ถูก throttle, หรือถูกปฏิเสธ ฟังก์ชัน consume() และ validate() เดียวกันนี้หย่อนลงใน ws.on('message') handler จริงได้ทันที
ลองรันแล้วอ่านคำตัดสิน: frame ที่ผิดรูป ใหญ่เกิน และ type ที่ไม่รู้จัก จะโดน validator ปฏิเสธ และเมื่อ burst ดูด bucket ห้า token จนหมด frame ที่ตามมาก็โดน throttle ไม่ว่าเนื้อหาจะเป็นอย่างไร การ์ดสองอันที่เป็นอิสระต่อกัน — อันหนึ่งคุม rate, อันหนึ่งคุม รูปร่าง — และการเชื่อมต่อเดียวจะถล่มหรือทำให้ server สับสนไม่ได้อีกต่อไป
| Rate Limit Type | จำกัดอะไร | เหมาะกับ |
|---|---|---|
| Connection rate | connection ใหม่ต่อ IP ต่อวินาที | ป้องกัน connection flood |
| Message rate | message ต่อ connection ต่อวินาที | ป้องกัน spam, abuse |
| Payload size | ขนาด message สูงสุด | ป้องกัน memory exhaustion |
| Room join rate | join 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