เจาะลึกวิธีรับ Webhook ให้ปลอดภัย ไม่พลาดตกม้าตายบน Production
คู่มือปฏิบัติการรับ Webhook แบบละเอียดยิบ รับมือปัญหารั่วไหล ข้อมูลซ้ำ และลำดับเพี้ยน เพื่อระบบหลังบ้านที่เสถียร

ภาพประกอบจากคลังภาพสต็อก ไม่ใช่ภาพจากเหตุการณ์จริง
- ตรวจสอบลายเซ็นด้วย timingSafeEqual และเซ็นบน raw body เสมอ
- เช็ก Timestamp เพื่อป้องกันการโจมตีแบบ Replay Attack
- ออกแบบระบบให้เป็น Idempotent ป้องกันการทำงานซ้ำจาก Retry
- ใช้ Precedence แทน Timestamp ในการจัดการข้อมูลที่มาผิดลำดับ
การส่ง Webhook เป็นเรื่องง่าย แค่ทำ POST คำขอแบบ JSON ไปยัง URL แล้วก็จบ แต่ในทางกลับกัน ฝั่งผู้รับกลับเต็มไปด้วยความท้าทาย เนื่องจากปลายทางเป็น Public URL ที่ใครก็เรียกเข้ามาได้ แถมยังมีระบบ Retry ที่อาจทำให้คำขอถูกรันซ้ำสองรอบ ข้อมูลอาจมาถึงสลับลำดับกันจนสถานะdelivered มาถึงก่อนsent และยังอยู่บน Critical Path ของระบบอื่น หากฝั่งเราตอบสนองช้าจนหมดเวลา ผู้ส่งก็จะพยายามส่งซ้ำจนทำให้ระบบช้าลงไปอีก
ปัญหาเหล่านี้ไม่ใช่เรื่องยากหากคุณรู้ล่วงหน้า แต่ทั้งหมดนี้มักซ่อนตัวอยู่จนกระทั่งไปโผล่บน Production บทความนี้จึงรวบรวมสิ่งที่คุณต้องจัดการทั้งหมดเมื่อต้องสร้างตัวรับ Webhook โดยใช้ตัวอย่างเคสการส่งอีเมล (Bounces และ Complaints) ที่มีความยุ่งยากครบทุกรูปแบบ ทั้งเรื่องความปลอดภัย การส่งซ้ำ ลำดับที่ผิดเพี้ยน และการประมวลผลซ้ำที่จะทำลายข้อมูลจริง ซึ่งหลักการเหล่านี้สามารถนำไปใช้กับบริการอย่าง Stripe, GitHub หรือ Shopify ได้เช่นเดียวกัน
ด่านแรกคือความปลอดภัย เนื่องจาก Endpoint ของคุณเป็นสาธารณะ หากไม่มีการตรวจสอบ ผู้ไม่หวังดีอาจส่งเหตุการณ์ปลอม เช่น หลอกว่าอีเมลตีกลับเพื่อระงับบัญชีลูกค้า หรือหลอกว่าชำระเงินสำเร็จเพื่อเนียนใช้บริการฟรี ผู้ให้บริการจึงมักจะแนบ ลายเซ็น (Signature) มากับทุกคำขอด้วย Secret ร่วมกัน หน้าที่ของคุณคือคำนวณลายเซ็นใหม่แล้วนำมาเทียบกัน

ภาพประกอบจากคลังภาพสต็อก ไม่ใช่ภาพจากเหตุการณ์จริง
ในจุดนี้มีข้อควรระวังสำคัญคือ ต้องใช้ฟังก์ชัน timingSafeEqual แทนการใช้เครื่องหมายเทียบเท่าธรรมดา === เพราะการเปรียบเทียบสตริงปกติจะหยุดทำงานทันทีที่พบไบต์ที่ต่างกัน ทำให้ผู้โจมตีสามารถคาดเดาไบต์ที่ถูกต้องได้จากการวัดเวลาในการประมวลผล นอกจากนี้ ต้องเซ็นบนตัว Raw Body ไม่ใช่บน Parsed Object ซึ่งถือเป็นบั๊กยอดฮิตที่สร้างความปวดหัวให้กับนักพัฒนาจำนวนมาก
การใช้ timingSafeEqual เป็นแนวทางป้องกัน Timing Attack ซึ่งเป็นช่องโหว่ด้านความปลอดภัยที่มักถูกมองข้าม การที่ระบบคืนค่าช้าหรือเร็วต่างกันเพียงไม่กี่มิลลิวินาทีก็เพียงพอให้แฮกเกอร์ใช้เครื่องมือวิเคราะห์หาคีย์ลับได้ การใช้ฟังก์ชันเปรียบเทียบแบบ Constant-time จึงเป็นมาตรฐานสำคัญในการจัดการข้อมูลที่มีความละเอียดอ่อนสูง
ลายเซ็นที่ถูกต้องเป็นเครื่องยืนยันว่าข้อมูลมาจากผู้ให้บริการจริง แต่มันไม่ได้หมายความว่าเหตุการณ์นั้นเกิดขึ้นในตอนนี้ เพราะแฮกเกอร์ที่ดักจับคำขอไว้สามารถส่งมันกลับมาใหม่ในอีกหนึ่งเดือนข้างหน้าโดยที่ลายเซ็นยังคงใช้งานได้ ผู้ให้บริการจึงใส่ Timestamp มาใน Payload ที่เซ็นด้วย ซึ่งคุณจะต้องเขียนโค้ดตรวจสอบเงื่อนไขเวลานี้เสมอ
เรื่องต่อมาคือการบริหารจัดการ Timeout และ Status Code โดยปกติผู้ให้บริการจะให้เวลาตอบกลับสั้นๆ เพียง 5 ถึง 30 秒 หากเกินกำหนดพวกเขาจะบันทึกข้อผิดพลาดและส่งคำขอซ้ำ หาก Handler ของคุณทำงานช้าเพราะต้องส่งอีเมล อัปเดตตารางฐานข้อมูลสามแห่ง และเรียก API ภายนอก คุณก็จะเจอการส่งซ้ำในขณะที่คำขอแรกยังรันอยู่ ทำให้เกิดงานซ้ำซ้อน
การตอบกลับ Status Code ให้ถูกต้องจึงเป็นหัวใจสำคัญ โดย 200 หมายถึงได้รับแล้วหยุดส่งซ้ำ, 4xx หมายถึงคำขอนี้พัง ไม่ต้องส่งซ้ำ และ 5xx หมายถึงให้ลองส่งใหม่อีกครั้ง การเผลอคืนค่า 200 บนข้อผิดพลาดที่คุณควรจะกู้คืนได้จะทำให้เหตุการณ์นั้นหายไปจากระบบอย่างถาวร นอกจากนี้ ระบบ Retry ไม่ใช่เรื่องบังเอิญแต่มันเกิดขึ้นได้จาก Timeout การ Deploy หรือปัญหาระบบเครือข่าย คุณจึงต้องออกแบบให้ระบบเป็น Idempotent เพื่อให้มั่นใจว่าต่อให้เหตุการณ์เดิมรันสองครั้ง งานก็จะไม่ถูกทำซ้ำซ้อน
ทางออกที่ดีที่สุดคือปล่อยให้ฐานข้อมูลเป็นผู้ตัดสินใจจัดการความซ้ำซ้อนผ่าน UNIQUE constraint แทนการเช็คโค้ดในแอปพลิเคชันธรรมดา เนื่องจากสองคำขอที่เข้ามาพร้อมกันอาจอ่านค่าได้ว่ายังไม่เคยเห็นทั้งคู่ หากผู้ให้บริการไม่ได้ส่ง Stable Event ID มาให้ คุณต้องสร้างมันขึ้นมาจากฟิลด์ที่บ่งชี้เหตุการณ์นั้นๆ เช่น รหัสข้อความรวมกับประเภทเหตุการณ์และเวลา ห้ามแฮช Payload ทั้งหมดเพราะผู้ให้บริการมักจะเพิ่มฟิลด์ใหม่ๆ เข้ามาตามกาลเวลา
ปัญหาเรื่องลำดับการส่งก็เป็นอีกเรื่องที่ต้องระวัง เนื่องจากไม่มีการรับประกันลำดับการมาถึง ข้อมูลการส่งที่ถูกลองส่งใหม่อาจมาถึงช้ากว่าสถานะการส่งสำเร็จที่เกิดขึ้นทีหลัง การแก้ปัญหาทำได้โดยใช้ Precedence แทน Timestamp โดยกำหนดให้สถานะบางอย่างมีความสำคัญสูงสุดและบล็อกการมาถึงที่ล่าช้าออกไป สำหรับเหตุการณ์ที่คุณปฏิเสธการรับ ควรมีการจัดเก็บข้อมูลเหล่านั้นไว้ตรวจสอบภายหลังก่อนที่ผู้ให้บริการจะยกเลิกการส่งซ้ำ
ที่มา: Dev.to
พบข้อมูลผิดพลาดในบทความนี้? แจ้งปัญหาบทความนี้
ความคิดเห็น
แสดงความคิดเห็น