ช่องโหว่ Stored HTML injection ในกระดานหางาน RamenHire ทำให้ใครก็ตามสามารถกรอกฟอร์มสาธารณะและเปลี่ยนอีเมลแจ้งเตือนของทีมให้กลายเป็นแพลตฟอร์มฟิชชิง (phishing) ได้

ช่องโหว่นี้หลุดรอดเข้ามาได้อย่างไร

RamenHire ทำงานบน Next.js และ Supabase และอนุญาตให้สตาร์ทอัพลงประกาศงานได้โดยไม่ต้องล็อกอิน ทุกการส่งข้อมูลจะกระตุ้นให้มีการส่งอีเมลไปยังทีมสรรหาบุคลากรภายใน เนื้อหาในอีเมลถูกสร้างขึ้นโดยการนำฟิลด์ข้อมูลดิบจากฟอร์ม เช่น ชื่อบริษัท, รายละเอียดงาน และชื่อผู้ติดต่อ มาต่อกันเป็นสตริง HTML โดยตรง โดยไม่มีการทำ escaping หรือ sanitisation ใดๆ ก่อนที่สตริงจะถูกส่งไปยังบริการอีเมล

เนื่องจากเทมเพลตปฏิบัติกับข้อมูลที่ผู้ใช้ป้อนเสมือนเป็น HTML ที่ปลอดภัย ผู้สมัครที่มีเจตนาร้ายจึงสามารถฉีด (inject) ลิงก์ปลอม "คลิกที่นี่เพื่อยืนยัน" เข้าไปได้ ตัว payload จะถูกจัดเก็บไว้บนเซิร์ฟเวอร์ในฐานะส่วนหนึ่งของรายการประกาศงาน ดังนั้นอีเมลแจ้งเตือนทุกฉบับหลังจากนั้นจะพ่วงรหัสที่เป็นอันตรายไปด้วย ผู้โจมตีสามารถซ่อนลิงก์นั้นไว้ข้างๆ ปุ่ม "View Dashboard" ของจริง เพื่อหลอกล่อให้สมาชิกในทีมเปิดเผยข้อมูลประจำตัวหรือเข้าไปยังเว็บไซต์ฟิชชิง

ทำไมเรื่องนี้ถึงสำคัญ

อีเมลเหล่านี้จะถูกส่งไปยังทีมสรรหาหลัก ซึ่งเป็นกลุ่มคนที่ต้องคลิกลิงก์อยู่ตลอดเวลาเพื่อดำเนินการคัดเลือกผู้สมัครในขั้นตอนต่างๆ

ช่องว่างทางเทคนิค

การทำ HTML escaping ไม่ใช่ขั้นตอนเดียว แต่มีสองบริบทที่ต้องได้รับการป้องกัน:

  • Text nodes – เนื้อหาระหว่างแท็ก การเปลี่ยน < เป็น &lt; และ > เป็น &gt; สามารถหยุดแท็กได้ แต่ไม่สามารถหยุดผู้โจมตีจากการหลุดออกจากค่าของ attribute ได้
  • Attribute values – สตริงที่อยู่ภายในแท็ก เช่น href="…" การฉีดเครื่องหมายอัญประกาศ (" หรือ ') จะช่วยให้ผู้โจมตีสามารถปิด attribute ก่อนกำหนดและแทรก markup ของตนเองลงไปได้

โค้ดเดิมจัดการเพียงแค่ข้อความธรรมดา (plain text) จึงทำให้การฉีด attribute (attribute injection) เปิดกว้างและเสี่ยงต่อการถูกโจมตี

การแก้ไขปัญหา

นักพัฒนาได้เพิ่มฟังก์ชันช่วยขนาดเล็กชื่อ escapeHtml และนำไปใช้กับทุกค่าที่ผู้ใช้ป้อนมาก่อนการ interpolation ไม่ว่าค่านั้นจะไปอยู่ใน text node หรือ attribute ก็ตาม

ขั้นตอนการตรวจสอบ

  • Local testing – มีการใช้ชุดสตริงสำหรับการฉีด (injection strings) ป้อนเข้าสู่ฟังก์ชันเทมเพลต ผลการทดสอบแต่ละครั้งแสดงให้เห็นเพียงตัวอักษรที่ถูก escape แล้วเท่านั้น โดยไม่มีแท็กที่สามารถรันคำสั่งได้
  • Production testing – หลังจากติดตั้ง patch แล้ว ได้มีการส่ง payload ผ่านเว็บไซต์จริงโดยผ่านการตรวจสอบบอทของ Cloudflare Turnstile อีเมลที่ได้รับในกล่องจดหมายของนักพัฒนาแสดงให้เห็นว่า ลิงก์ที่เป็นอันตรายและแท็ก script ใดๆ ปรากฏเป็นเพียงข้อความที่อ่านไม่รู้เรื่อง ไม่ใช่ส่วนประกอบที่สามารถคลิกได้

หมายเหตุเกี่ยวกับ Gmail: บริการนี้อาจยังคงแสดง URL เป็นสีน้ำเงินและทำให้คลิกได้ แต่นั่นเป็นความสะดวกสบายในฝั่ง client และไม่ได้หมายความว่าการฉีด HTML ประสบความสำเร็จ การแก้ไขนี้ช่วยป้องกันไม่ให้การฉีดสร้างปุ่มจริงหรือรันสคริปต์ได้

สิ่งที่นักพัฒนาควรระวัง

  • อย่าเชื่อใจข้อมูลจากฟอร์มสาธารณะ – แม้แต่ฟอร์มที่ดูไม่มีพิษมีภัยก็สามารถสร้าง output แบบ stored ที่ถูกนำไปใช้ในที่อื่นๆ ได้
  • ทำ escaping ณ จุดที่ใช้งาน – ใช้การ escape ที่คำนึงถึงบริบท (text เทียบกับ attribute) ทันทีก่อนการ rendering ไม่ใช่ทำตั้งแต่ช่วงต้นของ pipeline
  • ทดสอบทั้งฝั่ง client และ server – Unit tests ช่วยตรวจจับความผิดพลาดที่เห็นได้ชัด ส่วนการทดสอบแบบ manual end-to-end ผ่าน UI จริงจะช่วยยืนยันว่าระบบป้องกันยังทำงานได้ดีภายใต้ทราฟฟิกจริง
  • ระวังพฤติกรรมเฉพาะตัวของ email client – โปรแกรมอีเมลบางตัวจะสร้างลิงก์จาก URL โดยอัตโนมัติ ซึ่งอาจทำให้เกิดความรู้สึกปลอดภัยที่ผิดพลาด ควรตรวจสอบให้แน่ใจว่า HTML พื้นฐานไม่มีองค์ประกอบที่ทำงานได้ (active elements)

บทสรุป

ความผิดพลาดเพียงจุดเดียวในการจัดการข้อมูลที่ผู้ใช้ป้อน เปลี่ยนอีเมลแจ้งเตือนตามปกติให้กลายเป็นช่องทางฟิชชิง การนำกระบวนการ HTML-escaping ที่ครอบคลุมมาใช้ และการตรวจสอบการแก้ไขทั้งในเครื่อง local และบน production ช่วยกู้คืนความปลอดภัยของกล่องจดหมายกลับมาได้ เหตุการณ์นี้เน้นย้ำถึงบทเรียนอมตะสำหรับเว็บแอปใดๆ ที่สร้าง HTML จากข้อมูลที่ไม่น่าเชื่อถือ: การทำ sanitisation ที่เหมาะสมเป็นเรื่องที่ต่อรองไม่ได้ และการละเลยเรื่องนี้อาจเป็นการเปิดช่องทางตรงไปยังกล่องจดหมายของผู้ใช้งานของคุณ