ช่องโหว่ Stored HTML injection ในกระดานหางาน RamenHire ทำให้ใครก็ตามสามารถกรอกฟอร์มสาธารณะและเปลี่ยนอีเมลแจ้งเตือนของทีมให้กลายเป็นแพลตฟอร์มฟิชชิง (phishing) ได้
ช่องโหว่นี้หลุดรอดเข้ามาได้อย่างไร
RamenHire ทำงานบน Next.js และ Supabase และอนุญาตให้สตาร์ทอัพลงประกาศงานได้โดยไม่ต้องล็อกอิน ทุกการส่งข้อมูลจะกระตุ้นให้มีการส่งอีเมลไปยังทีมสรรหาบุคลากรภายใน เนื้อหาในอีเมลถูกสร้างขึ้นโดยการนำฟิลด์ข้อมูลดิบจากฟอร์ม เช่น ชื่อบริษัท, รายละเอียดงาน และชื่อผู้ติดต่อ มาต่อกันเป็นสตริง HTML โดยตรง โดยไม่มีการทำ escaping หรือ sanitisation ใดๆ ก่อนที่สตริงจะถูกส่งไปยังบริการอีเมล
เนื่องจากเทมเพลตปฏิบัติกับข้อมูลที่ผู้ใช้ป้อนเสมือนเป็น HTML ที่ปลอดภัย ผู้สมัครที่มีเจตนาร้ายจึงสามารถฉีด (inject) ลิงก์ปลอม "คลิกที่นี่เพื่อยืนยัน" เข้าไปได้ ตัว payload จะถูกจัดเก็บไว้บนเซิร์ฟเวอร์ในฐานะส่วนหนึ่งของรายการประกาศงาน ดังนั้นอีเมลแจ้งเตือนทุกฉบับหลังจากนั้นจะพ่วงรหัสที่เป็นอันตรายไปด้วย ผู้โจมตีสามารถซ่อนลิงก์นั้นไว้ข้างๆ ปุ่ม "View Dashboard" ของจริง เพื่อหลอกล่อให้สมาชิกในทีมเปิดเผยข้อมูลประจำตัวหรือเข้าไปยังเว็บไซต์ฟิชชิง
ทำไมเรื่องนี้ถึงสำคัญ
อีเมลเหล่านี้จะถูกส่งไปยังทีมสรรหาหลัก ซึ่งเป็นกลุ่มคนที่ต้องคลิกลิงก์อยู่ตลอดเวลาเพื่อดำเนินการคัดเลือกผู้สมัครในขั้นตอนต่างๆ
ช่องว่างทางเทคนิค
การทำ HTML escaping ไม่ใช่ขั้นตอนเดียว แต่มีสองบริบทที่ต้องได้รับการป้องกัน:
- Text nodes – เนื้อหาระหว่างแท็ก การเปลี่ยน
<เป็น<และ>เป็น>สามารถหยุดแท็กได้ แต่ไม่สามารถหยุดผู้โจมตีจากการหลุดออกจากค่าของ 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 ที่เหมาะสมเป็นเรื่องที่ต่อรองไม่ได้ และการละเลยเรื่องนี้อาจเป็นการเปิดช่องทางตรงไปยังกล่องจดหมายของผู้ใช้งานของคุณ
