Cyber 101 · บทที่ 3
ช่องโหว่เว็บ 1: Injection และ XSS
Injection คืออะไร ทำไมมันยังอันตรายที่สุด
บทนี้เราจะเข้าใจช่องโหว่เว็บสองตระกูลที่พบบ่อยและอันตรายมาโดยตลอด คือ SQL injection และ Cross-site scripting (XSS) เราจะดูเชิงแนวคิดว่ามันเกิดขึ้นได้อย่างไร แล้วเน้นหนักไปที่วิธีอุดช่องโหว่ให้ถูกต้อง เป้าหมายไม่ใช่การไปเจาะใคร แต่คือการเขียนและดูแลเว็บของเราเองให้ปลอดภัยขึ้น
รากของปัญหาเดียวกัน: ข้อมูลปนกับคำสั่ง
ทั้ง SQL injection และ XSS มาจากความผิดพลาดแบบเดียวกัน คือโปรแกรมเอา "ข้อมูลที่ผู้ใช้พิมพ์เข้ามา" ไปปนกับ "คำสั่งที่ระบบจะรัน" โดยไม่แยกให้ชัด เมื่อขอบเขตระหว่างข้อมูลกับคำสั่งพร่าเลือน ผู้โจมตีก็แทรกคำสั่งของตัวเองเข้าไปได้ กฎทองของการป้องกันทั้งบทนี้จึงมีประโยคเดียว คือ "อย่าเชื่อ input จากผู้ใช้ และแยกข้อมูลออกจากคำสั่งเสมอ"
OWASP Top 10 กับตำแหน่งของ Injection
OWASP (Open Worldwide Application Security Project) จัดทำรายการ OWASP Top 10 ซึ่งเป็นบัญชีความเสี่ยงด้านความปลอดภัยของเว็บที่พบบ่อยที่สุด ใช้เป็นมาตรฐานอ้างอิงกันทั่วโลก หมวด Injection (ซึ่งรวม SQL injection) และ XSS อยู่ในรายการนี้มานานหลายรอบ เพราะยังพบได้เรื่อย ๆ ในแอปจริง การเรียนบทนี้จึงเท่ากับปิดความเสี่ยงกลุ่มใหญ่ที่สุดกลุ่มหนึ่งของเว็บ คุณอ่านรายละเอียดฉบับเต็มได้ที่เว็บของ OWASP ที่ owasp.org/www-project-top-ten
⚠️ จริยธรรมและกฎหมาย
ทุกเทคนิคในบทนี้มีไว้เพื่อ "เข้าใจเพื่อป้องกัน" เท่านั้น การทดลองทั้งหมดให้ทำเฉพาะกับเว็บฝึกที่ติดตั้งบนเครื่องของคุณเอง เช่น OWASP Juice Shop หรือ DVWA การส่งข้อมูลแปลก ๆ เข้าไปทดสอบเว็บของคนอื่นโดยไม่ได้รับอนุญาตเป็นลายลักษณ์อักษร เข้าข่ายความผิดตามพระราชบัญญัติว่าด้วยการกระทำผิดเกี่ยวกับคอมพิวเตอร์ พ.ศ. 2560 เช่น การเข้าถึงระบบหรือข้อมูลโดยมิชอบ ข้อความนี้เป็นความรู้ทั่วไป ไม่ใช่คำแนะนำทางกฎหมาย หากต้องทำงานจริงให้ขออนุญาตและกำหนดขอบเขตให้ชัดก่อนเสมอ
SQL Injection เชิงแนวคิด
เว็บส่วนใหญ่เก็บข้อมูลไว้ในฐานข้อมูล และคุยกับฐานข้อมูลด้วยภาษา SQL SQL injection คือช่องโหว่ที่ทำให้ผู้โจมตีแทรกคำสั่ง SQL ของตัวเองเข้าไปในคำสั่งที่แอปตั้งใจจะรัน
เกิดขึ้นได้อย่างไร
สมมติหน้าเข้าสู่ระบบเอาชื่อผู้ใช้ที่กรอกมาต่อเป็นสตริงลงในคำสั่ง SQL ตรง ๆ ถ้าโปรแกรมสร้างคำสั่งด้วยการต่อสตริง ผู้โจมตีก็พิมพ์อักขระพิเศษอย่างเครื่องหมายคำพูดหรือเงื่อนไขที่เป็นจริงเสมอเข้าไป ทำให้ตรรกะของคำสั่งเปลี่ยนไป เช่น เงื่อนไขตรวจรหัสผ่านกลายเป็นจริงเสมอ ผลคืออาจล็อกอินได้โดยไม่รู้รหัส หรือดึงข้อมูลที่ไม่ควรเห็นออกมา นี่คือเหตุผลว่าทำไมการต่อสตริงกับ input ผู้ใช้จึงอันตราย
ตัวอย่างเชิงอธิบาย: คำสั่งที่เขียนแบบเสี่ยง (ต่อสตริง) เทียบกับแบบปลอดภัย
# ❌ แบบเสี่ยง: เอา input มาต่อสตริงลงคำสั่งตรง ๆ
query = "SELECT * FROM users WHERE name = '" + user_input + "'"
# ✅ แบบปลอดภัย: ใช้ parameterized query ส่งข้อมูลแยกจากคำสั่ง
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
ในแบบปลอดภัย เครื่องหมาย ? คือช่องว่างที่บอกฐานข้อมูลว่า "ตรงนี้คือข้อมูล ไม่ใช่คำสั่ง" ไม่ว่าผู้ใช้จะพิมพ์อะไรลงไป มันจะถูกปฏิบัติเป็นข้อความค่าหนึ่งเสมอ ไม่กลายเป็นคำสั่ง
🛡️ วิธีป้องกัน SQL Injection
ป้องกันได้ค่อนข้างเบ็ดเสร็จถ้าทำตามหลักเหล่านี้ครบ
- ใช้ parameterized queries หรือ prepared statements เสมอ นี่คือมาตรการหลักและได้ผลที่สุด แยกข้อมูลออกจากคำสั่งตั้งแต่ระดับฐานข้อมูล
- ใช้ ORM หรือ query builder ที่ทำ parameterization ให้อัตโนมัติ แต่ต้องระวังไม่หลุดไปเขียน raw SQL แบบต่อสตริงเอง
- Least privilege บัญชีฐานข้อมูลที่แอปใช้ ควรมีสิทธิเท่าที่จำเป็นจริง ๆ ไม่ใช่สิทธิ admin ถ้าแอปแค่อ่านและเพิ่มออเดอร์ ก็ไม่ควรให้สิทธิลบตารางหรือให้สิทธิผู้อื่น เมื่อเกิดเหตุ ความเสียหายจะถูกจำกัด
- ตรวจสอบและกรอง input เป็นชั้นเสริม (validate ชนิดและรูปแบบข้อมูล) แต่ห้ามใช้แทน parameterization
- ปิดข้อความ error ละเอียดในหน้าสาธารณะ อย่าโชว์ error ของฐานข้อมูลให้ผู้ใช้เห็น เพราะเป็นเบาะแสให้ผู้โจมตี
ตัวอย่างเชิงอธิบาย: ให้สิทธิบัญชีแอปเท่าที่จำเป็น (least privilege)
-- บัญชีที่เว็บแอปใช้ ให้เฉพาะสิทธิที่ต้องใช้จริง
GRANT SELECT, INSERT ON shop.orders TO 'webapp'@'localhost';
-- ไม่ให้ DROP, GRANT หรือสิทธิระดับผู้ดูแลระบบ
🧪 ลองในแล็บของคุณ
ทำในเว็บฝึกที่ติดตั้งบนเครื่องคุณเองเท่านั้น เช่น DVWA หรือ OWASP Juice Shop บน localhost
- ติดตั้ง Juice Shop ในแล็บของคุณ (มักรันผ่าน Docker หรือ Node บนเครื่องตัวเอง) แล้วเปิดที่ localhost
- เปิด DevTools ดูว่าเมื่อคุณกรอกฟอร์มค้นหาหรือฟอร์มล็อกอิน แอปส่งข้อมูลอะไรไปที่เซิร์ฟเวอร์บ้าง
- ทดลองใส่เครื่องหมายคำพูดเดี่ยวในช่องค้นหาของแล็บ แล้วสังเกตว่าแอปที่มีช่องโหว่ตอบสนองต่างจากแอปปกติอย่างไร นี่คือการ "เห็นด้วยตา" ว่าการแยกข้อมูลกับคำสั่งสำคัญแค่ไหน
- จากนั้นลองเขียนแอปเล็ก ๆ ของคุณเองสองเวอร์ชัน เวอร์ชันหนึ่งต่อสตริง อีกเวอร์ชันใช้ parameterized query แล้วเทียบผลว่าทำไมแบบหลังจึงกันได้
แพลตฟอร์มฝึกถูกกฎหมายที่ออกแบบมาให้ลอง SQLi ได้อย่างปลอดภัย เช่น PortSwigger Web Security Academy และ picoCTF
⚠️ จริยธรรมและกฎหมาย
อย่านำเทคนิคนี้ไปลองกับเว็บของร้านค้า โรงเรียน โรงพยาบาล หรือหน่วยงานใด ๆ ที่ไม่ใช่ของคุณ แม้จะ "แค่ลองดูเฉย ๆ" ก็ถือว่าเข้าถึงระบบโดยมิชอบได้ การฝึกที่ถูกต้องคือใช้เว็บฝึกที่ตั้งใจสร้างมาให้เจาะ หรือโปรแกรม bug bounty ที่เจ้าของอนุญาตและกำหนดขอบเขตไว้ชัดเจนเท่านั้น
Cross-site Scripting (XSS) เชิงแนวคิด
ถ้า SQL injection คือการแทรกคำสั่งเข้าไปในฐานข้อมูล XSS คือการแทรกโค้ดฝั่งหน้าเว็บ (JavaScript) เข้าไปให้รันในเบราว์เซอร์ของผู้ใช้คนอื่น เกิดเมื่อเว็บเอาข้อมูลจากผู้ใช้ไปแสดงผลกลับโดยไม่ทำให้ปลอดภัยก่อน
Reflected XSS
โค้ดที่แทรกเข้ามาถูก "สะท้อน" กลับทันทีในหน้าตอบสนอง เช่น ค่าที่ส่งมากับลิงก์หรือช่องค้นหา แล้วถูกแสดงกลับบนหน้านั้นโดยไม่กรอง มักอาศัยการหลอกให้เหยื่อคลิกลิงก์ที่เตรียมไว้
Stored XSS
โค้ดที่แทรกถูกบันทึกไว้ในระบบ เช่น ในช่องคอมเมนต์หรือโปรไฟล์ แล้วเด้งใส่ทุกคนที่เปิดหน้านั้นภายหลัง อันตรายกว่าเพราะกระทบผู้ใช้หลายคนโดยไม่ต้องหลอกให้คลิกอะไร
DOM-based XSS
เกิดจากโค้ด JavaScript ฝั่งหน้าเว็บเองที่เอาข้อมูลจากผู้ใช้ไปเขียนลง DOM แบบไม่ปลอดภัย โดยที่เซิร์ฟเวอร์อาจไม่เกี่ยวเลย ต้องแก้ที่วิธีเขียน front-end
เกิดขึ้นได้อย่างไร
ปัญหาเกิดเมื่อโปรแกรมเอาข้อความจากผู้ใช้ไปยัดลงหน้าเว็บโดยตรงในลักษณะที่เบราว์เซอร์ตีความว่าเป็นโค้ด แทนที่จะเป็นข้อความธรรมดา ถ้าเว็บฝึกของคุณมีช่องโหว่นี้ ข้อความอย่าง <script>alert(1)</script> ที่ควรจะเป็นแค่ตัวอักษร กลับถูกเบราว์เซอร์รันเป็นสคริปต์ นั่นคือสัญญาณว่าเว็บไม่ได้ทำ output ให้ปลอดภัยก่อนแสดงผล ในของจริงผู้โจมตีอาจใช้ช่องนี้ขโมย session หรือหลอกผู้ใช้ ซึ่งเป็นเหตุผลว่าทำไมต้องอุดให้สนิท
ตัวอย่างเชิงอธิบาย: การแสดงข้อมูลผู้ใช้แบบเสี่ยง เทียบกับแบบปลอดภัย
// ❌ แบบเสี่ยง: ยัดข้อความผู้ใช้ลง DOM เป็น HTML
element.innerHTML = userComment;
// ✅ แบบปลอดภัย: ให้เบราว์เซอร์ปฏิบัติเป็นข้อความล้วน
element.textContent = userComment;
ความต่างอยู่ที่ textContent จะไม่ตีความแท็กใด ๆ ในข้อความว่าเป็นโค้ด ทำให้สคริปต์ที่แฝงมากลายเป็นแค่ตัวอักษรบนหน้าจอ
🛡️ วิธีป้องกัน XSS
ป้องกันด้วยหลายชั้นประกอบกัน
- Output encoding / escaping ก่อนแสดงข้อมูลผู้ใช้ ให้แปลงอักขระพิเศษ (เช่น < > & ") เป็นรูปแบบที่ปลอดภัย ให้ตรงกับบริบทที่แสดง (HTML, attribute, JavaScript, URL แต่ละที่ต่างกัน)
- ใช้ escaping ของเฟรมเวิร์ก เฟรมเวิร์กสมัยใหม่อย่าง React, Vue, Angular หรือ template engine ที่ auto-escape จะ escape ค่าที่นำมาแสดงให้โดยปริยาย ข้อควรระวังคืออย่าไปใช้ช่องทางที่ปิดการ escape (เช่น การ set HTML ดิบ) กับข้อมูลจากผู้ใช้
- Content-Security-Policy (CSP) เป็น HTTP header ที่จำกัดว่าเบราว์เซอร์อนุญาตให้รันสคริปต์จากแหล่งไหนได้บ้าง เป็นตาข่ายกันชั้นสุดท้ายที่ช่วยลดผลกระทบแม้จะมีช่องโหว่หลุดมา
- ตั้งค่าคุกกี้ให้ปลอดภัย ใช้แฟล็ก HttpOnly เพื่อไม่ให้ JavaScript อ่านคุกกี้ session ได้ ลดโอกาสถูกขโมย session ผ่าน XSS
- ทำ input validation เป็นชั้นเสริม แต่หัวใจของ XSS คือการทำ output ให้ปลอดภัย ณ จุดที่นำไปแสดง
ตัวอย่างเชิงอธิบาย: HTTP header จำกัดแหล่งสคริปต์ (CSP)
Content-Security-Policy: default-src 'self'; script-src 'self'
ค่านี้บอกเบราว์เซอร์ว่า ให้รันสคริปต์ได้เฉพาะจากโดเมนเดียวกับเว็บเราเท่านั้น ลดช่องที่สคริปต์แปลกปลอมจะทำงานได้
🧪 ลองในแล็บของคุณ
ทำในเว็บฝึกบนเครื่องคุณเองเท่านั้น เช่น DVWA หรือ Juice Shop บน localhost
- ในเว็บฝึกที่มีช่องคอมเมนต์ ลองกรอกข้อความที่มีแท็ก แล้วสังเกตว่าถ้าเว็บมีช่องโหว่ ข้อความนั้นจะถูกตีความเป็นโค้ดอย่างไร ให้ใช้แค่ข้อความทดสอบไม่มีพิษ เช่น alert(1) เพื่อพิสูจน์แนวคิด ไม่ใช่โค้ดขโมยข้อมูล
- เปิดโหมดความปลอดภัยระดับต่าง ๆ ใน DVWA (low/medium/high) แล้วดูว่าเมื่อโค้ดเริ่ม escape ข้อมูล ช่องโหว่หายไปอย่างไร
- เขียนหน้าเว็บเล็ก ๆ ของคุณเองที่รับคอมเมนต์ ลองทั้งแบบใช้ innerHTML และแบบใช้ textContent เทียบให้เห็นว่าแบบหลังกัน XSS ได้
- ทดลองเพิ่ม header CSP ในเว็บของตัวเอง แล้วดูใน DevTools ว่าเบราว์เซอร์บล็อกสคริปต์นอกแหล่งที่อนุญาตอย่างไร
ฝึกโจทย์ XSS แบบเป็นขั้นตอนได้ที่ PortSwigger Web Security Academy หมวด XSS
⚠️ จริยธรรมและกฎหมาย
XSS กระทบผู้ใช้คนอื่นโดยตรง การนำไปทดสอบกับเว็บสาธารณะที่ไม่ใช่ของคุณ จึงไม่ใช่แค่ผิดกติกา แต่อาจทำให้ผู้บริสุทธิ์เสียหายและเข้าข่ายความผิดตามพระราชบัญญัติว่าด้วยการกระทำผิดเกี่ยวกับคอมพิวเตอร์ เช่น การนำเข้าข้อมูลที่ก่อความเสียหายต่อระบบหรือผู้อื่น ฝึกในแล็บของตัวเองหรือแพลตฟอร์มที่อนุญาตเท่านั้น และหากพบช่องโหว่ในเว็บจริงโดยบังเอิญ ให้แจ้งเจ้าของอย่างมีความรับผิดชอบ (responsible disclosure) อย่านำไปใช้เอง
สรุปและทบทวน
SQL injection และ XSS มาจากรากเดียวกันคือการปล่อยให้ข้อมูลผู้ใช้ปนกับคำสั่ง ทางแก้จึงคล้ายกันคือ "แยกข้อมูลออกจากคำสั่ง และทำให้ปลอดภัยก่อนใช้งานเสมอ"
เช็กลิสต์ติดตัวนักพัฒนาเว็บ
- ทุก query ที่แตะฐานข้อมูล ใช้ parameterized query หรือ prepared statement
- บัญชีฐานข้อมูลของแอปได้สิทธิเท่าที่จำเป็น (least privilege)
- ทุกข้อมูลผู้ใช้ที่นำไปแสดง ผ่านการ escape/encode ตามบริบท
- ใช้ auto-escaping ของเฟรมเวิร์ก และหลีกเลี่ยงการ set HTML ดิบจาก input
- ตั้ง Content-Security-Policy และคุกกี้ session เป็น HttpOnly
- ปิด error ละเอียดในหน้าสาธารณะ และทดสอบช่องโหว่ในแล็บของตัวเองก่อนขึ้นจริง