01เว็บคือบทสนทนากับคนแปลกหน้า The trust boundary
เช้าวันจันทร์ เจ้าของร้านเสื้อผ้าออนไลน์เล็ก ๆ ร้านหนึ่งเปิดอีเมลเจอข้อความจากคนที่ไม่รู้จัก เขียนมาอย่างสุภาพว่า “ผมเป็นลูกค้าของร้าน บังเอิญสังเกตว่าหน้าใบสั่งซื้อของร้านเปิดเห็นข้อมูลของลูกค้าคนอื่นได้ เลยหยุดแล้วรีบแจ้งมาครับ”
เจ้าของร้านโทรหาน้องที่เขียนเว็บให้ ทั้งคู่ไล่ดูโค้ดอยู่ครึ่งวัน แล้วก็เจอต้นเหตุ มันไม่ใช่ไวรัสล้ำยุค ไม่ใช่แฮกเกอร์ต่างชาติ แต่เป็นโค้ดหน้าใบสั่งซื้อที่ตรวจแค่ว่า “ล็อกอินหรือยัง” แล้วก็ส่งใบสั่งซื้อเลขที่ขอมาให้ทันที โดยไม่ถามสักคำว่าใบนั้นเป็นของใคร
เรื่องแบบนี้เกิดขึ้นทุกวันทั่วโลก และนี่คือหัวใจของบทนี้ทั้งบท: ช่องโหว่เว็บเกือบทั้งหมดเกิดจากการไว้ใจข้อมูลที่ผู้ใช้ส่งมามากเกินไป เราจะมองผ่านตาผู้โจมตีเพียงเพื่อเข้าใจว่าเขามองหาความผิดพลาดแบบไหน จากนั้นจะใช้เวลาส่วนใหญ่ไปกับการปิดมัน
อะไรบ้างที่ “มาจากผู้ใช้”
ทุกครั้งที่เราเปิดเว็บ เบราว์เซอร์ส่ง “คำขอ” (request) ไปหาเซิร์ฟเวอร์ แล้วเซิร์ฟเวอร์ส่ง “คำตอบ” (response) กลับมา ฟังดูเหมือนการคุยกันระหว่างเพื่อน แต่จากมุมของเซิร์ฟเวอร์ ทุกคำขอมาจากคนแปลกหน้าที่มองไม่เห็นหน้า
สิ่งที่หลายคนไม่ทันคิดคือ เบราว์เซอร์อยู่ในมือผู้ใช้ทั้งหมด ข้อความในช่องกรอกฟอร์ม ตัวเลขในที่อยู่เว็บ คุกกี้ ส่วนหัวของคำขอ ชื่อไฟล์ที่อัปโหลด แม้แต่ช่องที่ซ่อนไว้ในหน้าเว็บ ผู้ใช้แก้ได้ทุกอย่าง ปุ่มที่เราซ่อน ตัวเลือกที่เราล็อกไว้ในเมนู หรือโค้ด JavaScript ที่ตรวจว่ากรอกถูกไหม ล้วนเป็นแค่ “คำแนะนำ” สำหรับคนที่ใช้งานตามปกติ ไม่ใช่กำแพง
FIG 01เส้นแบ่งความไว้ใจระหว่างเบราว์เซอร์กับเซิร์ฟเวอร์
บันทึกเป็นภาพอ่านภาพนี้เป็นข้อความ
- ฝั่งเบราว์เซอร์ (ไว้ใจไม่ได้): ช่องกรอกฟอร์ม ที่อยู่เว็บและพารามิเตอร์ คุกกี้ ส่วนหัวของคำขอ และไฟล์ที่อัปโหลด
- การตรวจด้วย JavaScript ในเบราว์เซอร์ช่วยให้ผู้ใช้กรอกถูก แต่ผู้ใช้ปิดหรือแก้ได้ จึงไม่ใช่ด่านความปลอดภัย
- เส้นแบ่งความไว้ใจ (trust boundary) อยู่ระหว่างสองฝั่ง
- ด่านตรวจฝั่งเซิร์ฟเวอร์: ตรวจรูปแบบข้อมูล ตรวจสิทธิ์ แยกข้อมูลออกจากคำสั่ง และเข้ารหัสผลลัพธ์ก่อนแสดง
- ปลายทางที่ต้องปกป้อง: ฐานข้อมูล หน้าเว็บที่ส่งให้ผู้ใช้คนอื่น และบริการหรือเครื่องภายในองค์กร
ลองนึกถึงรปภ.หน้าคอนโด ถ้ารปภ.ปล่อยทุกคนที่ “บอกว่า” ตัวเองอยู่ห้อง 1204 ขึ้นลิฟต์ไปได้เลย ระบบบัตรคีย์การ์ดที่แพงแค่ไหนก็ไม่มีความหมาย รปภ.ที่ดีต้องตรวจทุกครั้ง และตรวจจากสิ่งที่ปลอมยาก ไม่ใช่จากคำพูด เซิร์ฟเวอร์ของเราก็ต้องเป็นรปภ.แบบนั้น
- ศัพท์น่ารู้: เส้นแบ่งความไว้ใจ (Trust boundary)
- เส้นสมมติที่ข้อมูลข้ามจากที่ที่เราควบคุมไม่ได้ เข้ามายังที่ที่เราควบคุม ทุกครั้งที่ข้อมูลข้ามเส้นนี้ ต้องมีการตรวจ จุดที่ไม่มีใครตรวจคือจุดที่ช่องโหว่ชอบเกิด
- ศัพท์น่ารู้: ช่องโหว่ (Vulnerability)
- จุดอ่อนในโค้ด การออกแบบ หรือการตั้งค่า ที่ทำให้ระบบทำสิ่งที่เจ้าของไม่ได้ตั้งใจ เช่น เปิดข้อมูลให้คนที่ไม่มีสิทธิ์เห็น
02แผนที่ OWASP Top 10 OWASP Top 10:2025
ถ้าจะเรียนเรื่องช่องโหว่เว็บโดยไม่หลงทาง เราต้องมีแผนที่ แผนที่ที่คนทั้งวงการใช้คือ OWASP Top 10 จาก OWASP (Open Worldwide Application Security Project) มูลนิธิไม่แสวงกำไรที่ทำงานด้านความปลอดภัยของซอฟต์แวร์มายาวนาน
OWASP Top 10 ไม่ใช่รายชื่อเทคนิคการโจมตี แต่เป็น รายการ “หมวดความเสี่ยง” ของเว็บแอป ที่จัดลำดับจากข้อมูลที่เก็บจากแอปจริงจำนวนมาก ประกอบกับความเห็นของคนในวงการ แต่ละหมวดรวมความผิดพลาดหลายแบบที่มีรากเดียวกันไว้ด้วยกัน ฉบับล่าสุดคือ OWASP Top 10:2025 ซึ่งเปิดตัวช่วงปลายปี 2025 ถือเป็นการปรับใหญ่ครั้งแรกนับจากฉบับ 2021
FIG 02สิบหมวดความเสี่ยงของ OWASP Top 10:2025
บันทึกเป็นภาพอ่านภาพนี้เป็นข้อความ
- A01:2025 การควบคุมสิทธิ์บกพร่อง (Broken Access Control): รวม IDOR, CSRF และ SSRF
- A02:2025 ตั้งค่าระบบผิดพลาด (Security Misconfiguration)
- A03:2025 ห่วงโซ่ซอฟต์แวร์ล้มเหลว (Software Supply Chain Failures)
- A04:2025 การเข้ารหัสล้มเหลว (Cryptographic Failures)
- A05:2025 การฉีดคำสั่ง (Injection): รวม SQL injection และ XSS
- A06:2025 ออกแบบไม่ปลอดภัย (Insecure Design)
- A07:2025 ยืนยันตัวตนล้มเหลว (Authentication Failures)
- A08:2025 ความถูกต้องของซอฟต์แวร์และข้อมูลล้มเหลว (Software or Data Integrity Failures)
- A09:2025 บันทึกและแจ้งเตือนไม่พอ (Security Logging and Alerting Failures)
- A10:2025 รับมือเหตุผิดปกติไม่ดี (Mishandling of Exceptional Conditions)
มีสามเรื่องที่ควรสังเกตในฉบับนี้ หนึ่ง การควบคุมสิทธิ์บกพร่องยังครองอันดับหนึ่ง และในฉบับ 2025 OWASP จัด SSRF เข้ามาอยู่ในหมวดนี้ด้วย สอง หมวดห่วงโซ่ซอฟต์แวร์ (A03) ขยายมาจากเรื่อง “ไลบรารีเก่าที่มีช่องโหว่” ในฉบับก่อน ให้ครอบคลุมทั้งเส้นทางที่ซอฟต์แวร์ถูกสร้างและส่งต่อ สาม หมวดใหม่ A10 พูดถึงโปรแกรมที่รับมือกับเหตุไม่คาดฝันไม่ดี เช่น เจอข้อผิดพลาดแล้ว “ปล่อยผ่าน” แทนที่จะปฏิเสธ
วิธีใช้แผนที่นี้ให้คุ้มคือ อย่าท่องเป็นรายชื่อ แต่ใช้เป็น เช็กลิสต์ตอนออกแบบและรีวิวโค้ด เช่น ทุกหน้าที่แสดงข้อมูลส่วนตัว ถามตัวเองว่าผ่าน A01 ไหม ทุกจุดที่เอาข้อมูลไปคุยกับฐานข้อมูล ถามว่าผ่าน A05 ไหม หัวข้อถัดไปเราจะเดินดูทีละหมวดที่นักพัฒนาเว็บเจอบ่อยที่สุด
- ศัพท์น่ารู้: OWASP
- Open Worldwide Application Security Project มูลนิธิที่ทำงานแบบเปิด ผลงานของเขาใช้ได้ฟรี นอกจาก Top 10 แล้วยังมีชุดคู่มือ Cheat Sheet Series สำหรับนักพัฒนา และมาตรฐาน ASVS สำหรับตรวจว่าแอปปลอดภัยพอหรือยัง
- ศัพท์น่ารู้: CWE
- Common Weakness Enumeration รายการชื่อเรียกมาตรฐานของ “ความผิดพลาดแบบต่าง ๆ” ในซอฟต์แวร์ แต่ละหมวดของ OWASP Top 10 คือการรวม CWE ที่เกี่ยวข้องกันไว้เป็นกลุ่ม
03Injection: เมื่อข้อมูลปนเข้าไปในคำสั่ง A05 · Injection
เว็บแอปแทบทุกตัวต้องคุยกับระบบอื่น ส่วนใหญ่คือฐานข้อมูล ภาษาที่ใช้คุยกับฐานข้อมูลเรียกว่า SQL ประโยค SQL ประโยคหนึ่งมีสองส่วนปนกันอยู่เสมอ คือ ส่วนที่เป็นคำสั่ง (เช่น “ค้นหาสมาชิกที่มีรหัสเท่ากับ…”) และ ส่วนที่เป็นข้อมูล (รหัสสมาชิกที่ผู้ใช้ส่งมา)
ความผิดพลาดคลาสสิกคือ นักพัฒนาเอาข้อความที่ผู้ใช้พิมพ์มา ต่อสตริงเข้าไปในประโยคคำสั่งตรง ๆ แล้วส่งทั้งก้อนไปให้ฐานข้อมูล ฐานข้อมูลได้รับประโยคเดียวยาว ๆ มันไม่มีทางรู้ว่าตรงไหนนักพัฒนาเขียน ตรงไหนผู้ใช้พิมพ์ ถ้าข้อความของผู้ใช้บังเอิญหรือจงใจมีหน้าตาเหมือนคำสั่ง ฐานข้อมูลก็จะทำตามด้วยความซื่อสัตย์ ผลคืออาจเปิดข้อมูลที่ไม่ควรเปิด แก้ หรือลบข้อมูลได้
ลองนึกถึงร้านข้าวมันไก่ที่ให้ลูกค้าเขียนเมนูลงกระดาษแผ่นเดียวกับที่พนักงานเขียนคำสั่งถึงครัว แล้วพนักงานอ่านทั้งแผ่นออกเสียงให้ครัวฟัง ครัวไม่รู้ว่าบรรทัดไหนลูกค้าเขียน ถ้าลูกค้าเขียนอะไรที่ฟังเหมือนคำสั่งของร้านเอง ครัวก็ทำตาม ทางแก้ไม่ใช่การจ้างคนมาอ่านกระดาษทุกแผ่นว่ามีคำต้องห้ามไหม แต่คือ ใช้แบบฟอร์มที่มีช่องตายตัว ครัวรู้ว่าช่อง “เมนู” คือชื่อเมนูเสมอ ไม่ว่าลูกค้าจะเขียนอะไรลงไป
ทางแก้: แยกคำสั่งกับข้อมูลออกจากกันตั้งแต่ต้น
แบบฟอร์มช่องตายตัวในโลกโค้ดเรียกว่า parameterized query (หรือ prepared statement) เราส่ง “โครงคำสั่ง” ที่มีช่องว่างจองไว้ไปก่อน แล้วส่ง “ข้อมูล” ตามไปแยกต่างหาก ฐานข้อมูลจะเอาข้อมูลไปใส่ในช่องนั้นในฐานะค่าเท่านั้น ไม่มีวันตีความเป็นคำสั่ง
แบบปลอดภัยPython · parameterized query
# โครงคำสั่งคงที่ เครื่องหมาย ? คือช่องที่จองไว้สำหรับ “ข้อมูล” เท่านั้น sql = "SELECT name, email FROM members WHERE member_id = ?" cursor.execute(sql, (member_id,)) # ข้อมูลส่งแยก ไม่ถูกต่อเข้าไปในประโยค # ถ้าใช้ ORM ก็ได้ผลแบบเดียวกัน: ORM สร้าง parameterized query ให้เอง member = Member.objects.filter(member_id=member_id).first()
หลักเดียวกันใช้ได้กับทุกที่ที่ข้อมูลจากผู้ใช้ไปคุยกับ “ตัวแปลคำสั่ง” ตัวอื่น ไม่ว่าจะเป็นฐานข้อมูลแบบ NoSQL ระบบไดเรกทอรีอย่าง LDAP หรือคำสั่งของระบบปฏิบัติการ ทางที่ปลอดภัยคือใช้ไลบรารีหรือ API ที่รับข้อมูลเป็นค่าแยกต่างหาก และหลีกเลี่ยงการประกอบคำสั่งเป็นข้อความยาว ๆ แล้วส่งให้เชลล์ทำงาน
อีกสองนิสัยที่ช่วยจำกัดความเสียหายแม้มีอะไรหลุด คือ ให้บัญชีที่แอปใช้ต่อฐานข้อมูล มีสิทธิ์เท่าที่จำเป็น (เช่น หน้าค้นหาสินค้าอ่านได้อย่างเดียว ลบตารางไม่ได้) และ อย่าโชว์ข้อความผิดพลาดของฐานข้อมูลให้ผู้ใช้เห็น เพราะมันเผยโครงสร้างภายในโดยไม่จำเป็น
- ศัพท์น่ารู้: Injection
- ความผิดพลาดที่ข้อมูลจากผู้ใช้ถูกส่งไปให้ตัวแปลคำสั่ง (ฐานข้อมูล เชลล์ เบราว์เซอร์) โดยไม่แยกออกจากตัวคำสั่ง ทำให้ข้อมูลกลายเป็นส่วนหนึ่งของคำสั่งได้ ใน OWASP Top 10:2025 อยู่ในหมวด A05
- ศัพท์น่ารู้: Parameterized query
- การส่งโครงคำสั่งกับข้อมูลแยกกัน ให้ฐานข้อมูลรู้ชัดว่าส่วนไหนคือค่าที่ต้องค้นหา ถือเป็นวิธีหลักที่ OWASP แนะนำสำหรับป้องกัน SQL injection
04XSS: เมื่อข้อความของคนหนึ่งกลายเป็นโค้ดในเครื่องอีกคน Cross-site scripting
Cross-site scripting หรือ XSS เป็นญาติสนิทของ injection และใน OWASP Top 10:2025 ก็อยู่หมวด A05 เหมือนกัน ต่างกันตรงที่ “ตัวแปลคำสั่ง” ที่ถูกหลอกไม่ใช่ฐานข้อมูล แต่เป็น เบราว์เซอร์ของผู้ใช้คนอื่น
หน้าเว็บที่เราเห็นคือ HTML ซึ่งมีทั้งเนื้อหาและโค้ดที่เบราว์เซอร์รันได้ปนอยู่ด้วยกัน เวลาเว็บเอาข้อความที่ผู้ใช้คนหนึ่งส่งมา เช่น ความเห็นใต้สินค้า ชื่อโปรไฟล์ หรือคำค้นหา ไปวางในหน้าที่คนอื่นเปิด ถ้าวางลงไป ในฐานะ HTML ดิบ ๆ โดยไม่แปลงอักขระพิเศษ เบราว์เซอร์ของคนอ่านก็ตีความไม่ออกว่านี่คือข้อความที่ควรแสดง หรือโค้ดที่ควรรัน
ที่ร้ายคือ โค้ดที่หลุดเข้ามาแบบนี้จะรัน ในนามของเว็บไซต์เรา เบราว์เซอร์จึงให้สิทธิ์มันเท่ากับโค้ดของเว็บจริง มันอาจทำรายการแทนผู้ใช้ เปลี่ยนหน้าตาเว็บให้หลอกกรอกรหัสผ่าน หรือเข้าถึงข้อมูลในหน้านั้นได้
นึกถึงกระดานประกาศของหมู่บ้านจัดสรร นิติบุคคลรับประกาศจากลูกบ้านมาติดในกรอบทางการของหมู่บ้าน คนเดินผ่านเห็นกรอบแล้วก็เชื่อว่าเป็นประกาศของนิติฯ ถ้าลูกบ้านคนหนึ่งเขียนว่า “แจ้งลูกบ้านทุกท่าน ค่าส่วนกลางปีนี้ให้โอนเข้าบัญชีนี้” ประกาศนั้นจะดูน่าเชื่อเท่าประกาศจริง ทางแก้คือติดประกาศของลูกบ้านในซองใสที่มีตราชัดเจนว่า “ข้อความจากลูกบ้าน” อ่านได้ทุกตัวอักษร แต่ไม่มีใครเข้าใจผิดว่าเป็นคำสั่ง
สามแบบที่ควรรู้จักชื่อ
Stored XSS ข้อความถูกเก็บไว้ในฐานข้อมูล แล้วถูกแสดงให้ทุกคนที่เปิดหน้านั้น Reflected XSS ข้อความจากคำขอถูก “สะท้อน” กลับมาในหน้าคำตอบทันที เช่น หน้าผลการค้นหาที่แสดงคำค้น DOM-based XSS เกิดในโค้ด JavaScript ฝั่งเบราว์เซอร์เอง เมื่อโค้ดหยิบข้อมูลจากที่อยู่เว็บหรือแหล่งอื่นมาใส่หน้าเว็บแบบ HTML ทั้งสามแบบมีรากเดียวกัน และแก้ด้วยหลักเดียวกัน
ทางแก้: เข้ารหัสตอนแสดงผล ให้ข้อความเป็นแค่ข้อความ
หลักหลักคือ output encoding แปลงอักขระที่มีความหมายพิเศษใน HTML ให้กลายเป็นตัวหนังสือธรรมดาก่อนวางลงหน้าเว็บ ข่าวดีคือเฟรมเวิร์กสมัยใหม่ส่วนใหญ่ทำให้เราโดยอัตโนมัติ เช่น React แสดงค่าที่ใส่ในวงเล็บปีกกาเป็นข้อความเสมอ ส่วนเทมเพลตของ Django หรือ Jinja2 ที่เปิด autoescape ก็แปลงอักขระให้เอง ปัญหามักเกิดตอนที่เราจงใจ “ปิด” ระบบนี้ หรือเขียนโค้ดเองที่ใส่ข้อความของผู้ใช้ลงไปในหน้าเว็บเป็น HTML
แบบปลอดภัยJavaScript · ใส่ข้อความเป็นตัวหนังสือ
// ใส่ความเห็นของผู้ใช้ลงหน้าเว็บในฐานะ “ข้อความ” เบราว์เซอร์จะไม่ตีความเป็นโค้ด const p = document.createElement("p"); p.textContent = comment.body; commentList.appendChild(p);
ถ้าแอปจำเป็นต้องให้ผู้ใช้จัดรูปแบบข้อความได้ (ตัวหนา ลิงก์) อย่าเขียนตัวกรองเอง ให้ใช้ไลบรารีทำความสะอาด HTML (HTML sanitizer) ที่ได้รับการดูแลต่อเนื่อง ซึ่งเก็บไว้เฉพาะแท็กที่อนุญาต และในชั้นถัดไป ให้ตั้ง Content Security Policy (CSP) สั่งเบราว์เซอร์ว่าโค้ดจากแหล่งไหนบ้างที่รันได้ กับตั้งคุกกี้สำคัญเป็น HttpOnly เพื่อให้โค้ดในหน้าเว็บอ่านคุกกี้นั้นไม่ได้ เราจะเห็นตัวอย่าง CSP ในหัวข้อ 08
- ศัพท์น่ารู้: Output encoding (escaping)
- การแปลงอักขระพิเศษให้เป็นรูปที่ปลอดภัยสำหรับ “บริบท” ที่จะวางลงไป เช่น ในเนื้อหา HTML ในค่าของแอตทริบิวต์ หรือในที่อยู่เว็บ แต่ละบริบทมีกฎต่างกัน จึงควรให้เฟรมเวิร์กหรือไลบรารีที่เชื่อถือได้ทำให้
05สิทธิ์รั่ว: IDOR และ CSRF A01 · Broken access control
ในบท พิสูจน์ตัวตน & สิทธิ์การเข้าถึง เราแยกสองคำถามออกจากกัน “คุณคือใคร” (authentication) กับ “คุณทำสิ่งนี้ได้ไหม” (authorization) หมวด A01 คือความผิดพลาดของคำถามที่สอง และมันครองอันดับหนึ่งของ OWASP Top 10 มาตั้งแต่ฉบับ 2021 จนถึงฉบับ 2025
IDOR: ตรวจบัตรที่ประตู แต่ไม่ดูว่าตู้ไหนเป็นของใคร
กลับไปที่ร้านเสื้อผ้าตอนต้นบท ความผิดพลาดแบบนั้นมีชื่อเรียกว่า IDOR (Insecure Direct Object Reference) เซิร์ฟเวอร์รับ “เลขอ้างอิง” ของข้อมูลมาจากคำขอ เช่น เลขใบสั่งซื้อ เลขเอกสาร หรือเลขบัญชี แล้วหยิบข้อมูลนั้นส่งให้เลย โดยเชื่อว่าคนขอคงขอเฉพาะของตัวเอง
เหมือนฟิตเนสที่พนักงานตรวจบัตรสมาชิกที่ประตูอย่างเข้มงวด แต่ล็อกเกอร์ข้างในเปิดได้ด้วยการบอกเลขตู้กับพนักงาน โดยไม่มีใครดูว่าตู้นั้นเป็นของคนที่ยืนอยู่หรือเปล่า การตรวจบัตรที่ประตูคือ authentication ซึ่งทำถูกแล้ว ที่หายไปคือ authorization ที่ระดับ “ตู้แต่ละใบ”
ญาติของ IDOR อีกแบบคือ การซ่อนปุ่มหรือเมนูของแอดมินไว้ในหน้าเว็บ แต่ฟังก์ชันฝั่งเซิร์ฟเวอร์เบื้องหลังปุ่มนั้นไม่ได้ตรวจว่าผู้เรียกเป็นแอดมินจริง จำไว้ว่าการซ่อนในหน้าจอไม่ใช่การป้องกัน เพราะทุกอย่างทางซ้ายของเส้นแบ่งความไว้ใจ ผู้ใช้ควบคุมได้
แบบปลอดภัยNode.js · ตรวจความเป็นเจ้าของที่เซิร์ฟเวอร์ทุกครั้ง
app.get("/orders/:id", requireLogin, async (req, res) => { const order = await db.orders.findById(req.params.id); // ถามทุกครั้ง: ใบนี้เป็นของคนที่ล็อกอินอยู่หรือเปล่า if (!order || order.ownerId !== req.user.id) { return res.status(404).end(); // ไม่ใช่ของคุณ = ตอบเหมือนไม่มีอยู่ } res.json(order); });
นิสัยที่ช่วยได้มากคือ ปฏิเสธไว้ก่อน (deny by default) ทุกหน้าและทุก API ต้องประกาศชัดว่าใครเข้าได้ ถ้าไม่ได้ประกาศ ถือว่าห้าม และควรรวมกฎสิทธิ์ไว้ที่เดียว (เช่น middleware หรือ policy กลาง) แทนการเขียน if กระจายไปทั่วโค้ด ซึ่งลืมง่าย การใช้เลขอ้างอิงแบบสุ่มยาว (UUID) แทนเลขเรียงทำให้เดายากขึ้น แต่ ไม่ได้แทนการตรวจสิทธิ์ เพราะเลขสุ่มก็หลุดได้ผ่านลิงก์ที่แชร์ต่อหรือบันทึกต่าง ๆ
CSRF: เบราว์เซอร์แนบบัตรผ่านให้เองโดยอัตโนมัติ
Cross-site request forgery หรือ CSRF เกิดจากพฤติกรรมปกติของเบราว์เซอร์ที่หลายคนไม่ทันคิด คือ เบราว์เซอร์ แนบคุกกี้ของเว็บหนึ่งไปให้อัตโนมัติ ทุกครั้งที่มีคำขอไปหาเว็บนั้น แม้คำขอนั้นจะถูกเริ่มจากหน้าเว็บอื่นก็ตาม ถ้าเว็บธนาคารหรือหน้าตั้งค่าบัญชีของเราดูแค่ว่า “มีคุกกี้ล็อกอินไหม” แล้วทำรายการเลย มันก็แยกไม่ออกว่าผู้ใช้ตั้งใจกดเอง หรือมีหน้าเว็บอื่นส่งคำขอแทนในขณะที่ผู้ใช้ยังล็อกอินค้างอยู่
เหมือนเราฝากตรายางลายเซ็นไว้ที่เคาน์เตอร์ธนาคาร แล้วธนาคารยอมทำทุกรายการที่มีตรายางนั้นประทับ ไม่ว่าใครจะเป็นคนยื่นใบ ทางแก้คือให้ทุกรายการสำคัญต้องมี “รหัสลับประจำหน้า” ที่เฉพาะหน้าเว็บของเราเองเท่านั้นรู้ ซึ่งหน้าเว็บอื่นมองไม่เห็นและเดาไม่ได้
แบบปลอดภัยคุกกี้ SameSite + CSRF token ในฟอร์ม
# ส่วนหัวคำตอบ: คุกกี้ไปเฉพาะ HTTPS, JavaScript อ่านไม่ได้, ไม่แนบไปกับคำขอข้ามเว็บส่วนใหญ่ Set-Cookie: session=…; Secure; HttpOnly; SameSite=Lax; Path=/ <!-- ฟอร์มที่เปลี่ยนแปลงข้อมูล ต้องพก token ที่เซิร์ฟเวอร์ออกให้และตรวจกลับทุกครั้ง --> <form method="post" action="/account/email"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> … </form>
เฟรมเวิร์กหลักอย่าง Django, Rails, Laravel และ ASP.NET Core มีระบบป้องกัน CSRF ในตัว งานของเราคืออย่าปิดมันทิ้งเพราะ “ฟอร์มส่งไม่ผ่าน” นอกจากนี้ อย่าให้คำขอแบบ GET เปลี่ยนแปลงข้อมูล (GET มีไว้อ่าน) และสำหรับรายการที่สำคัญมาก เช่น เปลี่ยนอีเมล เปลี่ยนรหัสผ่าน หรือโอนเงิน ให้ถามรหัสผ่านหรือ OTP ซ้ำอีกครั้ง
- ศัพท์น่ารู้: IDOR
- Insecure Direct Object Reference เซิร์ฟเวอร์ใช้เลขอ้างอิงที่ผู้ใช้ส่งมาหยิบข้อมูลโดยตรง โดยไม่ตรวจว่าผู้ขอมีสิทธิ์กับข้อมูลชิ้นนั้น เป็นรูปแบบหนึ่งของหมวด A01
- ศัพท์น่ารู้: CSRF token
- ค่าสุ่มที่เซิร์ฟเวอร์ผูกไว้กับเซสชันของผู้ใช้และใส่ไว้ในฟอร์มของเว็บเรา เมื่อมีคำขอเปลี่ยนแปลงข้อมูลเข้ามา เซิร์ฟเวอร์ตรวจว่ามี token ที่ถูกต้องหรือไม่ หน้าเว็บอื่นไม่รู้ค่านี้ จึงปลอมคำขอที่ผ่านการตรวจไม่ได้
06ตั้งค่าพลาด ชิ้นส่วนเก่า และ SSRF A02 · A03 · SSRF
ไม่ใช่ทุกช่องโหว่จะอยู่ในโค้ดที่เราเขียนเอง หลายครั้งโค้ดของเราเรียบร้อยดี แต่ระบบรอบตัวมันเปิดประตูทิ้งไว้ หัวข้อนี้รวมสามเรื่องที่ทีมเว็บมองข้ามบ่อยที่สุด
ตั้งค่าระบบผิดพลาด (A02)
นึกถึงบ้านจัดสรรที่โครงการส่งมอบพร้อมกุญแจชุดเดียวกับที่ช่างใช้ตอนก่อสร้าง ถ้าเจ้าของไม่เปลี่ยนกุญแจ ทุกคนที่เคยถือกุญแจชุดนั้นก็ยังเข้าบ้านได้ ซอฟต์แวร์เว็บก็มี “ค่าตั้งต้นจากโรงงาน” ที่เหมาะกับการพัฒนา แต่อันตรายเมื่อขึ้นระบบจริง
ตัวอย่างที่พบบ่อย ได้แก่ เปิดโหมดดีบักทิ้งไว้ จนหน้าข้อผิดพลาดโชว์โค้ดและค่าตั้งค่าภายใน บัญชีแอดมินที่ใช้รหัสผ่านเริ่มต้น หน้าจัดการระบบที่เปิดให้ทั้งอินเทอร์เน็ตเข้าถึง เซิร์ฟเวอร์ที่แสดงรายชื่อไฟล์ทั้งโฟลเดอร์ ที่เก็บไฟล์บนคลาวด์ที่ตั้งเป็นสาธารณะโดยไม่ตั้งใจ และการไม่ใส่ส่วนหัวความปลอดภัย (security headers) เรื่องสุดท้ายนี้โยงกับหมวดใหม่ A10 ด้วย เพราะแอปที่ล้มแล้วพ่นรายละเอียดข้อผิดพลาดออกมา หรือเจอเหตุไม่คาดฝันแล้วปล่อยผ่านแทนที่จะปฏิเสธ คือแอปที่ “ล้มอย่างไม่ปลอดภัย”
ทางแก้คือทำให้การตั้งค่าเป็นสิ่งที่ ทำซ้ำได้และตรวจได้ เขียนค่าตั้งค่าของระบบจริงไว้เป็นไฟล์ (configuration as code) แยกค่าของเครื่องพัฒนากับเครื่องจริงออกจากกันชัดเจน ปิดทุกบริการและฟีเจอร์ที่ไม่ได้ใช้ และตั้งให้หน้าข้อผิดพลาดบอกผู้ใช้แค่ “เกิดข้อผิดพลาด” พร้อมเลขอ้างอิง ส่วนรายละเอียดเก็บไว้ใน log ฝั่งเรา
ชิ้นส่วนเก่าและห่วงโซ่ซอฟต์แวร์ (A03)
เว็บแอปสมัยนี้ไม่ได้เขียนขึ้นจากศูนย์ โค้ดที่ทีมเขียนเองอาจเป็นส่วนน้อย ที่เหลือคือเฟรมเวิร์ก ไลบรารี และไลบรารีที่ไลบรารีเหล่านั้นดึงมาต่ออีกที เหมือนร้านอาหารที่รับวัตถุดิบจากซัพพลายเออร์หลายเจ้า ต่อให้พ่อครัวเก่งแค่ไหน ถ้าวัตถุดิบเสีย อาหารก็เสีย OWASP ฉบับ 2025 จึงขยายหมวดนี้ให้ครอบคลุมทั้งห่วงโซ่ ตั้งแต่ชิ้นส่วนที่มีช่องโหว่ที่รู้กันแล้ว ไปจนถึงเส้นทางที่ซอฟต์แวร์ถูกสร้างและส่งต่อ (อ่านต่อในบท Zero‑day & การโจมตีห่วงโซ่อุปทาน)
เมื่อมีการประกาศช่องโหว่ในไลบรารียอดนิยม ผู้ไม่หวังดีจำนวนมากรู้พร้อมกับเรา สิ่งที่ตัดสินผลคือ ใครปิดได้ก่อน ทีมที่รู้ว่าแอปตัวเองใช้ชิ้นส่วนอะไรบ้าง และมีกระบวนการอัปเดตที่ซ้อมไว้แล้ว จะปิดได้ในเวลาไม่นาน ทีมที่ไม่รู้ อาจไม่รู้ตัวเลยว่าตัวเองเปิดประตูทิ้งไว้
SSRF: เมื่อเซิร์ฟเวอร์ยอมไปเปิดที่ไหนก็ได้ตามที่ถูกขอ
เว็บหลายแห่งมีฟีเจอร์ที่ให้ เซิร์ฟเวอร์ไปดึงข้อมูลจากที่อยู่เว็บที่ผู้ใช้ระบุ เช่น แสดงตัวอย่างลิงก์ที่แปะในแชต ดึงรูปโปรไฟล์จากลิงก์ หรือรับการแจ้งเตือนผ่าน webhook ความผิดพลาดคือ ปล่อยให้ผู้ใช้กำหนดปลายทางได้อย่างอิสระ
ปัญหาคือ เซิร์ฟเวอร์ของเราอยู่ “ข้างใน” เครือข่าย มันมองเห็นระบบภายในที่คนข้างนอกมองไม่เห็น เหมือนพนักงานต้อนรับที่ยอมเดินไปหยิบเอกสารจากห้องไหนก็ได้ตามกระดาษที่ลูกค้ายื่นมา รวมถึงห้องเก็บเอกสารลับหลังร้าน ลูกค้าเข้าห้องนั้นเองไม่ได้ แต่พนักงานเข้าได้ ช่องโหว่นี้เรียกว่า Server-Side Request Forgery หรือ SSRF และในฉบับ 2025 OWASP จัดไว้ใต้หมวด A01
วิธีป้องกันคือ กำหนดรายชื่อปลายทางที่อนุญาต (allow-list) แทนการเปิดกว้าง ตรวจที่อยู่ปลายทางหลังแปลงชื่อเป็นหมายเลข IP แล้ว ไม่ใช่แค่ดูจากข้อความ กั้นที่ชั้นเครือข่ายไม่ให้เครื่องที่ทำหน้าที่ดึงข้อมูลคุยกับเครือข่ายภายในหรือบริการ metadata ของคลาวด์ได้ แยกงานดึงข้อมูลไปไว้ในเครื่องที่โดดเดี่ยว และไม่ส่งคำตอบดิบจากปลายทางกลับไปให้ผู้ใช้ ผู้ให้บริการคลาวด์รายใหญ่ก็มีโหมดที่ป้องกันบริการ metadata เพิ่มเติม เช่น IMDSv2 ของ AWS ซึ่งควรเปิดใช้
- ศัพท์น่ารู้: Security misconfiguration
- ระบบที่โค้ดอาจถูกต้อง แต่การตั้งค่ารอบ ๆ เปิดช่องไว้ เช่น ค่าเริ่มต้นที่ไม่ปลอดภัย ฟีเจอร์ที่ไม่ได้ใช้แต่เปิดอยู่ หรือข้อความผิดพลาดที่เผยข้อมูลภายใน อยู่ในหมวด A02
- ศัพท์น่ารู้: SSRF
- Server-Side Request Forgery ช่องโหว่ที่ผู้ใช้กำหนดได้ว่าเซิร์ฟเวอร์จะส่งคำขอไปที่ไหน จนเซิร์ฟเวอร์กลายเป็นทางผ่านไปยังระบบภายในที่ไม่ควรเข้าถึงได้จากข้างนอก
07ท่อส่งข้อมูลที่ปลอดภัย Validate · authorize · separate · encode
ถ้าย้อนดูทุกหัวข้อที่ผ่านมา จะเห็นว่ามันคือเรื่องเดียวกันในหลายร่าง Injection คือการไว้ใจว่าข้อมูลจะไม่มีหน้าตาเหมือนคำสั่ง XSS คือการไว้ใจว่าข้อความจะไม่มีหน้าตาเหมือนโค้ด IDOR คือการไว้ใจว่าคนจะขอเฉพาะของตัวเอง CSRF คือการไว้ใจว่าคำขอที่มีคุกกี้คือคำขอที่ผู้ใช้ตั้งใจ SSRF คือการไว้ใจว่าที่อยู่ที่ส่งมาจะชี้ไปที่ที่ปลอดภัย
เพราะรากเดียวกัน ทางแก้จึงเป็นนิสัยชุดเดียวกัน เราลองวาดเส้นทางของข้อมูลหนึ่งชิ้น ตั้งแต่ข้ามเส้นแบ่งความไว้ใจเข้ามา จนออกไปเป็นหน้าเว็บที่คนอื่นเปิด
FIG 03เส้นทางของข้อมูลหนึ่งชิ้นผ่านด่านทั้งห้า
บันทึกเป็นภาพอ่านภาพนี้เป็นข้อความ
- 01 ตรวจรูปแบบ (validate): ชนิด ความยาว และรูปแบบตรงกับที่ยอมรับไหม ใช้รายการที่อนุญาต (allow-list)
- 02 ตรวจสิทธิ์ (authorize): คนนี้มีสิทธิ์กับข้อมูลชิ้นนี้จริงไหม
- 03 แยกจากคำสั่ง (separate): ข้อมูลลงช่องที่จองไว้ เช่น parameterized query
- 04 เก็บค่าดิบ (store): เก็บตามที่รับมา ไม่แปลงล่วงหน้า
- 05 แสดงผลแบบปลอดภัย (encode): แปลงอักขระพิเศษตามบริบทตอนวางลงหน้าเว็บ
- ถ้าไม่ผ่านขั้น 01 หรือ 02: ปฏิเสธ ตอบกลับด้วยข้อความกลาง ๆ และบันทึกไว้ให้ทีมตรวจ
ตรวจข้อมูลเข้า กับ เข้ารหัสตอนแสดงผล ต่างกันอย่างไร
สองคำนี้ถูกใช้สลับกันบ่อยจนสับสน ทั้งที่ทำหน้าที่ต่างกันและต้องมีทั้งคู่
| เรื่อง | ตรวจข้อมูลเข้า (input validation) | เข้ารหัสตอนแสดงผล (output encoding) |
|---|---|---|
| ทำตอนไหน | ทันทีที่ข้อมูลข้ามเส้นเข้ามาในเซิร์ฟเวอร์ | ตอนที่กำลังจะวางข้อมูลลงหน้าเว็บ ไฟล์ หรือคำสั่งอื่น |
| ถามว่าอะไร | ข้อมูลนี้มีหน้าตาอย่างที่เราคาดไว้ไหม เช่น อายุต้องเป็นตัวเลข 1–120 | จะวางข้อความนี้ลงบริบทนี้อย่างไรให้มันเป็นแค่ข้อความ |
| เก่งเรื่อง | ตัดข้อมูลแปลกปลอมทิ้งตั้งแต่ต้น ลดพื้นที่ให้ความผิดพลาดอื่น | ป้องกัน XSS ได้ตรงจุด แม้ข้อมูลจะเป็นข้อความอิสระอย่างความเห็น |
| ข้อจำกัด | ข้อความอิสระ (ชื่อคน ความเห็น) จำกัดรูปแบบได้ไม่มาก | ต้องเลือกวิธีให้ตรงบริบท จึงควรให้เฟรมเวิร์กทำให้ |
หลักสำคัญของการตรวจข้อมูลเข้าคือ บอกว่าอะไรได้ ดีกว่าบอกว่าอะไรห้าม การเขียนรายการคำต้องห้าม (block-list) เหมือนรปภ.ที่จำหน้าคนร้ายได้แค่สิบคน คนที่สิบเอ็ดก็เดินผ่านไปได้ ส่วนรายการที่อนุญาต (allow-list) เหมือนรปภ.ที่ถือรายชื่อลูกบ้าน ไม่อยู่ในรายชื่อก็ไม่ได้เข้า ไม่ต้องรู้ว่าคนร้ายหน้าตาแบบไหน
แบบปลอดภัยPython · ประกาศรูปแบบที่ยอมรับไว้ชัด ๆ
from pydantic import BaseModel, Field
class NewComment(BaseModel):
post_id: int = Field(gt=0) # ต้องเป็นจำนวนเต็มบวก
body: str = Field(min_length=1, max_length=2000) # มีความยาวจำกัด
rating: int = Field(ge=1, le=5) # 1 ถึง 5 เท่านั้น
# ข้อมูลที่ไม่ตรงรูปแบบจะถูกปฏิเสธทันที ก่อนถึงตรรกะของแอป
สังเกตว่าแม้จะตรวจรูปแบบแล้ว ช่อง body ก็ยังเป็นข้อความอิสระ จึงยังต้องผ่าน parameterized query ตอนบันทึก และผ่าน output encoding ตอนแสดง การตรวจข้อมูลเข้าไม่ได้ทำให้ข้อมูล “สะอาด” ไปตลอดกาล มันแค่ทำให้ข้อมูลมีหน้าตาตามที่คาด
08ป้องกันหลายชั้นสำหรับเว็บแอป Defence in depth
ไม่มีมาตรการไหนสมบูรณ์แบบ โค้ดที่ดีที่สุดก็ยังมีวันพลาด แนวคิด ป้องกันหลายชั้น (defence in depth) คือวางด่านซ้อนกันหลายชั้น ให้ความผิดพลาดของชั้นหนึ่งถูกจับได้ที่ชั้นถัดไป เหมือนคอนโดที่มีทั้งรปภ.หน้าตึก คีย์การ์ดลิฟต์ กุญแจห้อง และกล้องวงจรปิด แต่ละอย่างไม่สมบูรณ์ แต่ทั้งหมดรวมกันทำให้การบุกรุกยากมาก
FIG 04ชั้นป้องกันของเว็บแอปหนึ่งตัว
บันทึกเป็นภาพอ่านภาพนี้เป็นข้อความ
- ชั้น 1 ขอบนอก: HTTPS, WAF, จำกัดจำนวนคำขอ WAF กรองรูปแบบที่รู้จัก แต่ไม่รู้ตรรกะของแอป
- ชั้น 2 เบราว์เซอร์: ส่วนหัวความปลอดภัย เช่น CSP, HSTS และคุกกี้ Secure / HttpOnly / SameSite
- ชั้น 3 โค้ดของแอป: เฟรมเวิร์กที่ปลอดภัยเป็นค่าเริ่มต้น ตรวจสิทธิ์ทุกคำขอ แยกคำสั่งกับข้อมูล เข้ารหัสผลลัพธ์ (คำขออันตรายถูกหยุดที่ชั้นนี้)
- ชั้น 4 ข้อมูล: บัญชีฐานข้อมูลสิทธิ์น้อยที่สุด เข้ารหัสข้อมูลสำคัญ เก็บรหัสลับแยกจากโค้ด
- ชั้น 5 ชิ้นส่วน: รู้ว่าใช้ไลบรารีและเซิร์ฟเวอร์อะไร อัปเดตแพตช์ ปิดบริการที่ไม่ได้ใช้
- ครอบทุกชั้น: บันทึกและแจ้งเตือน เพื่อให้รู้เร็วเมื่อมีอะไรหลุด
เฟรมเวิร์กที่ปกป้องเราโดยปริยาย
ชั้นที่คุ้มค่าที่สุดมักเป็นชั้นที่เราแทบไม่ต้องทำอะไร เฟรมเวิร์กเว็บหลักในปัจจุบันออกแบบให้ทางที่ง่ายที่สุดคือทางที่ปลอดภัย ORM สร้าง parameterized query ให้ เทมเพลตเข้ารหัสผลลัพธ์ให้ ระบบฟอร์มใส่ CSRF token ให้ งานของนักพัฒนาคือ อยู่บนทางนั้น และระวังเป็นพิเศษทุกครั้งที่โค้ดออกนอกทาง เช่น การเขียน SQL ดิบ การปิด autoescape หรือการใช้ฟังก์ชันที่ชื่อบอกตรง ๆ ว่าอันตราย อย่าง dangerouslySetInnerHTML ของ React จุดเหล่านี้ควรถูกตั้งธงให้รีวิวทุกครั้ง
ส่วนหัวความปลอดภัย: ให้เบราว์เซอร์ช่วยเฝ้า
เซิร์ฟเวอร์ส่ง “ส่วนหัว” (headers) ไปพร้อมทุกหน้าเว็บได้ บางตัวเป็นคำสั่งให้เบราว์เซอร์เข้มงวดขึ้น เช่น CSP บอกว่าโค้ดจากแหล่งไหนรันได้ HSTS บังคับให้คุยผ่าน HTTPS เสมอ (ทบทวนได้ในบท HTTPS & ใบรับรองดิจิทัล) X-Content-Type-Options ห้ามเบราว์เซอร์เดาชนิดไฟล์เอง และ frame-ancestors ใน CSP ห้ามเว็บอื่นเอาหน้าเราไปซ้อนในกรอบ
ตั้งค่าป้องกันnginx · ส่วนหัวความปลอดภัยพื้นฐาน
# ให้เบราว์เซอร์คุยกับเราผ่าน HTTPS เสมอ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # รันสคริปต์ได้เฉพาะจากโดเมนเราเอง ห้ามปลั๊กอิน ห้ามถูกซ้อนในกรอบของเว็บอื่น add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
CSP ที่เข้มงวดอาจทำให้ฟีเจอร์บางอย่างพัง ถ้าเว็บเดิมมีสคริปต์ฝังในหน้าเยอะ วิธีที่ทีมนิยมคือเปิดแบบ Content-Security-Policy-Report-Only ก่อน ให้เบราว์เซอร์รายงานสิ่งที่จะถูกบล็อกโดยยังไม่บล็อกจริง แล้วค่อย ๆ ปรับจนพร้อมเปิดใช้
WAF: ตาข่ายชั้นหนึ่ง ไม่ใช่ยารักษา
Web Application Firewall (WAF) คือด่านที่ตั้งหน้าเว็บแอป คอยดูคำขอที่เข้ามา แล้วบล็อกคำขอที่มีหน้าตาตรงกับรูปแบบอันตรายที่รู้จัก มันมีประโยชน์จริง โดยเฉพาะช่วงที่เพิ่งมีช่องโหว่ใหม่ประกาศออกมาและเรายังแพตช์ไม่ทัน WAF ช่วยซื้อเวลาได้ แต่ WAF ไม่รู้ว่าใบสั่งซื้อเลข 1052 เป็นของใคร มันจึงจับ IDOR ไม่ได้ และรูปแบบที่มันรู้จักก็มีวันตกหล่น
ความเชื่อ
“ติด WAF แล้ว เว็บเราปลอดภัย ไม่ต้องรีบแก้โค้ดหรือแพตช์ก็ได้”
ความจริง
WAF เป็นหนึ่งชั้นในหลายชั้น มันกรองสิ่งที่รู้จัก แต่ไม่เข้าใจตรรกะของแอป ช่องโหว่ด้านสิทธิ์ส่วนใหญ่มองไม่เห็นจากหน้าตาของคำขอ ที่แก้ได้จริงคือโค้ดที่ถูกต้องและชิ้นส่วนที่อัปเดต WAF แค่ช่วยซื้อเวลาระหว่างทาง
ชั้นสุดท้ายที่มักถูกลืมคือ บันทึกและแจ้งเตือน (หมวด A09) ให้แอปบันทึกเหตุการณ์สำคัญ เช่น การล็อกอินล้มเหลวซ้ำ ๆ การถูกปฏิเสธสิทธิ์ และข้อผิดพลาดของการตรวจข้อมูล ส่งไปเก็บที่ส่วนกลาง และตั้งการแจ้งเตือนเมื่อมีรูปแบบผิดปกติ (อ่านต่อในบท SOC & การตรวจจับภัย) ระวังอย่าบันทึกรหัสผ่าน โทเค็น หรือข้อมูลส่วนบุคคลเกินจำเป็นลงไปใน log ด้วย
09ความปลอดภัยในวงจรพัฒนา และเส้นทางที่ถูกกฎหมาย Secure SDLC & disclosure
ความปลอดภัยของเว็บไม่ได้เกิดจากการสแกนครั้งใหญ่ก่อนเปิดตัวหนึ่งครั้ง แต่เกิดจากนิสัยเล็ก ๆ ที่แทรกอยู่ในทุกช่วงของการพัฒนา แนวคิดนี้มักเรียกว่า “shift left” คือขยับงานความปลอดภัยไปทางซ้ายของเส้นเวลา ให้เจอปัญหาตั้งแต่ตอนที่ยังแก้ได้ง่าย
FIG 05งานความปลอดภัยในแต่ละช่วงของการพัฒนาเว็บ
บันทึกเป็นภาพอ่านภาพนี้เป็นข้อความ
- ออกแบบ: วาดว่าข้อมูลข้ามเส้นไว้ใจตรงไหน แล้วถามว่าอะไรพังได้บ้าง (threat modeling)
- เขียนโค้ด: ใช้เฟรมเวิร์กที่ปลอดภัยโดยปริยาย ไม่ฝังรหัสลับไว้ในโค้ด
- รีวิวโค้ด: อ่านด้วยเช็กลิสต์ ตรวจสิทธิ์ครบไหม แยกคำสั่งกับข้อมูลไหม
- ทดสอบ: เทสต์ว่าคนอื่นเปิดข้อมูลเราไม่ได้ สแกนโค้ดและไลบรารีอัตโนมัติ
- ขึ้นระบบจริง: ปิดโหมดดีบัก เปลี่ยนรหัสเริ่มต้น ใส่ security headers
- ดูแลต่อเนื่อง: อัปเดตแพตช์ เฝ้าดู log เปิดช่องรับแจ้งช่องโหว่
- ยิ่งพบปัญหาช้า ยิ่งแก้ยากและกระทบคนมากขึ้น
ออกแบบโดยถามว่า “อะไรพังได้บ้าง”
ก่อนเขียนฟีเจอร์ใหม่ วาดภาพง่าย ๆ ว่าข้อมูลไหลจากไหนไปไหน ข้ามเส้นแบ่งความไว้ใจตรงจุดใด แล้วไล่ถามทีละจุด กระบวนการนี้เรียกว่า threat modeling ซึ่งมีบทของมันเองใน Threat Modeling & เขียนโปรแกรมให้ปลอดภัย คำถามเรื่องสิทธิ์ (A01) และการออกแบบที่ไม่ปลอดภัย (A06) ตอบได้ดีที่สุดในขั้นนี้
มีมาตรฐานกลางให้ทีมอ้างอิง
แทนที่จะให้แต่ละคนจำเอง ให้ทีมยึด OWASP Cheat Sheet Series เป็นคู่มือวิธีทำ และใช้ OWASP ASVS เป็นรายการข้อกำหนดว่าแอปต้องผ่านอะไรบ้าง เลือกระดับที่เหมาะกับความสำคัญของระบบ
รีวิวโค้ดด้วยสายตาของผู้ป้องกัน
ทุก pull request ที่แตะการล็อกอิน สิทธิ์ การคุยกับฐานข้อมูล หรือการแสดงข้อความของผู้ใช้ ควรมีคนอ่านด้วยเช็กลิสต์สั้น ๆ: ตรวจสิทธิ์ที่เซิร์ฟเวอร์หรือยัง ใช้ parameterized query ไหม ออกนอกทางที่เฟรมเวิร์กปูไว้ตรงไหน มีรหัสลับหลุดเข้ามาในโค้ดไหม
ให้เครื่องช่วยตรวจทุกครั้งที่ส่งโค้ด
เครื่องมือวิเคราะห์โค้ดแบบไม่ต้องรัน (SAST) จับรูปแบบโค้ดเสี่ยง เครื่องมือตรวจชิ้นส่วน (SCA) เช่น
npm auditหรือระบบแจ้งอัปเดตไลบรารีอัตโนมัติ บอกว่าไลบรารีไหนมีช่องโหว่ที่รู้กันแล้ว ส่วนการทดสอบขณะแอปทำงาน (DAST) ให้รันกับ ระบบทดสอบของเราเอง เท่านั้น และอย่าลืมเขียนเทสต์ธรรมดาที่ยืนยันว่า “ผู้ใช้ A เปิดข้อมูลของผู้ใช้ B ไม่ได้”ทำบัญชีชิ้นส่วนและแผนแพตช์ไว้ล่วงหน้า
รู้ว่าแต่ละระบบใช้ไลบรารีและเวอร์ชันอะไร (บางทีมทำเป็น SBOM หรือรายการส่วนประกอบซอฟต์แวร์) ตั้งเป้าว่าช่องโหว่ระดับร้ายแรงต้องปิดภายในกี่วัน และซ้อมขั้นตอนอัปเดตให้คล่อง จะได้ไม่ซ้ำรอย Equifax
จ้างทดสอบเจาะระบบแบบมีขอบเขตเป็นลายลักษณ์อักษร
การทดสอบเจาะระบบ (penetration test) ที่ถูกต้อง ทำโดยผู้เชี่ยวชาญที่เจ้าของระบบว่าจ้าง มีหนังสืออนุญาต ขอบเขต และช่วงเวลาที่ตกลงกันชัดเจน ผลลัพธ์คือรายงานที่ทีมนำไปแก้
อย่าทดสอบเว็บของคนอื่นโดยไม่ได้รับอนุญาต
ต่อให้ตั้งใจดี “แค่อยากรู้ว่ามีช่องโหว่ไหม” การเข้าถึงหรือทดสอบระบบของผู้อื่นโดยไม่ได้รับอนุญาตเป็นความผิดตามกฎหมายไทย โดยเฉพาะ พ.ร.บ.ว่าด้วยการกระทำความผิดเกี่ยวกับคอมพิวเตอร์ฯ เช่น มาตรา 5 ที่ว่าด้วยการเข้าถึงระบบที่มีมาตรการป้องกันโดยมิชอบ รายละเอียดอ่านได้ในบท กฎหมาย จริยธรรม & เส้นทางอาชีพ
เส้นทางที่ถูกต้อง: bug bounty และการเปิดเผยอย่างรับผิดชอบ
ถ้าสนใจด้านนี้จริงจัง โลกความปลอดภัยมีทางที่ถูกกฎหมายรองรับ องค์กรจำนวนมากเปิด โครงการ bug bounty ที่ประกาศขอบเขตชัดเจนว่าระบบไหนทดสอบได้ ทำอะไรได้และไม่ได้ และให้รางวัลแก่ผู้รายงานช่องโหว่ที่ถูกต้อง บางองค์กรมี นโยบายรับแจ้งช่องโหว่ (vulnerability disclosure policy) ที่ไม่มีรางวัล แต่ให้ช่องทางและคำรับรองว่าจะไม่เอาผิดผู้ที่แจ้งโดยสุจริตตามกติกา หลายเว็บประกาศช่องทางติดต่อไว้ในไฟล์ /.well-known/security.txt ตามมาตรฐาน RFC 9116
หลักของ การเปิดเผยอย่างรับผิดชอบ (responsible disclosure) คือแบบที่ลูกค้าในฉากเปิดบททำ เมื่อบังเอิญเห็นสิ่งผิดปกติระหว่างใช้งานตามปกติ ให้ หยุดทันที ไม่เปิดดู ไม่เก็บ ไม่แชร์ข้อมูลของคนอื่นเพิ่ม แจ้งเจ้าของผ่านช่องทางทางการ ให้รายละเอียดพอให้เขาแก้ได้ และให้เวลาเขาแก้ก่อนจะพูดถึงเรื่องนี้ในที่สาธารณะ ใครอยากฝึกฝีมือ ให้ฝึกกับแอปที่สร้างมาให้ฝึกโดยเฉพาะบนเครื่องของตัวเอง หรือแล็บที่จัดไว้ให้ (ดูหัวข้อ สนามฝึกที่ถูกกฎหมาย) และอ่านหลักการรายงานเพิ่มได้ที่ การเปิดเผยช่องโหว่
- ศัพท์น่ารู้: SAST, DAST, SCA
- SAST (Static Application Security Testing) อ่านโค้ดหาจุดเสี่ยงโดยไม่ต้องรัน DAST (Dynamic Application Security Testing) ทดสอบแอปที่กำลังทำงานในสภาพแวดล้อมทดสอบของเรา SCA (Software Composition Analysis) ตรวจว่าไลบรารีที่ใช้มีช่องโหว่ที่ประกาศแล้วหรือไม่
- ศัพท์น่ารู้: Bug bounty
- โครงการที่เจ้าของระบบเชิญให้คนภายนอกช่วยหาช่องโหว่ภายใต้กติกาและขอบเขตที่ประกาศไว้ แล้วให้รางวัลตามความร้ายแรง ทำนอกขอบเขตหรือนอกกติกา ก็ไม่ได้รับความคุ้มครอง
10สรุปให้จำ Recap
ทุกช่องโหว่ในบทนี้มีรากเดียวกัน คือการไว้ใจสิ่งที่ข้ามเส้นแบ่งความไว้ใจเข้ามา ถ้าจำได้แค่หกข้อ ให้จำหกข้อนี้
- ทุกอย่างจากเบราว์เซอร์คือข้อมูลจากคนแปลกหน้า ด่านที่นับเป็นความปลอดภัยต้องอยู่ฝั่งเซิร์ฟเวอร์
- แยกคำสั่งกับข้อมูล ใช้ parameterized query หรือ ORM อย่าต่อสตริงประกอบคำสั่ง
- เข้ารหัสตอนแสดงผล ให้เทมเพลตของเฟรมเวิร์กทำ และเสริมด้วย CSP
- ตรวจสิทธิ์ทุกคำขอ ที่ระดับข้อมูลแต่ละชิ้น ปฏิเสธไว้ก่อน และป้องกัน CSRF ด้วย token กับคุกกี้ SameSite
- รู้จักชิ้นส่วนที่ใช้ และแพตช์ให้ไว ปิดโหมดดีบักและค่าเริ่มต้นก่อนขึ้นระบบจริง
- ทดสอบเฉพาะระบบที่ได้รับอนุญาต เจอช่องโหว่โดยบังเอิญ ให้หยุดแล้วแจ้งเจ้าของผ่านช่องทางทางการ
ลองทบทวนดู
ทำไมการตรวจข้อมูลด้วย JavaScript ในเบราว์เซอร์อย่างเดียวจึงไม่พอ
เพราะเบราว์เซอร์อยู่ในมือผู้ใช้ทั้งหมด ผู้ใช้ปิดหรือแก้โค้ดนั้นได้ และส่งคำขอไปหาเซิร์ฟเวอร์ตรง ๆ ได้ การตรวจในเบราว์เซอร์ช่วยให้กรอกง่าย แต่ด่านความปลอดภัยต้องอยู่ฝั่งเซิร์ฟเวอร์ ซึ่งอยู่อีกฝั่งของเส้นแบ่งความไว้ใจ
parameterized query ป้องกัน SQL injection ได้อย่างไร
มันส่งโครงคำสั่งกับข้อมูลไปให้ฐานข้อมูลแยกกัน ฐานข้อมูลจึงรู้ชัดว่าส่วนไหนเป็นค่าที่ต้องค้นหา ข้อมูลจากผู้ใช้จะไม่มีวันถูกตีความเป็นส่วนหนึ่งของคำสั่ง ไม่ว่าจะเขียนว่าอะไร
เว็บหนึ่งเปลี่ยนเลขใบสั่งซื้อจากเลขเรียงเป็นเลขสุ่มยาว ๆ แล้ว ถือว่าแก้ IDOR เสร็จหรือยัง
ยัง เลขสุ่มทำให้เดายากขึ้น แต่เลขอ้างอิงก็หลุดได้ผ่านลิงก์ที่แชร์ต่อหรือบันทึกต่าง ๆ การแก้ที่แท้จริงคือให้เซิร์ฟเวอร์ตรวจทุกครั้งว่าใบสั่งซื้อนั้นเป็นของผู้ใช้ที่ล็อกอินอยู่
ทำไม output encoding ควรทำตอนแสดงผล ไม่ใช่ตอนรับข้อมูลเข้า
เพราะวิธีแปลงที่ถูกต้องขึ้นกับบริบทที่จะวางข้อมูลลงไป เช่น เนื้อหา HTML ค่าของแอตทริบิวต์ หรือที่อยู่เว็บ เราจะรู้บริบทก็ต่อเมื่อกำลังจะแสดงผล การเก็บค่าดิบไว้แล้วแปลงตอนแสดงจึงถูกต้องกว่า และไม่ทำให้ข้อมูลเพี้ยน
ถ้าองค์กรติด WAF แล้ว ทำไมยังต้องรีบแพตช์ไลบรารีที่มีช่องโหว่
เพราะ WAF กรองได้เฉพาะรูปแบบที่มันรู้จัก และมีวันตกหล่น มันช่วยซื้อเวลาระหว่างรอแพตช์ได้ แต่ไม่ได้ปิดช่องโหว่ในตัวซอฟต์แวร์ กรณี Equifax แสดงให้เห็นว่าช่องว่างระหว่าง “มีแพตช์แล้ว” กับ “ติดตั้งแล้ว” อันตรายแค่ไหน