เจาะลึก 4 บทเรียนจากบั๊ก SQL Injection ที่ผมเจอในระบบฐานข้อมูล

7 นาที 14 views บันทึกเป็น PDF
เจาะลึก 4 บทเรียนจากบั๊ก SQL Injection ที่ผมเจอในระบบฐานข้อมูล

เคยสงสัยไหมว่าทำไมบั๊กเล็ก ๆ ถึงกลายเป็นช่องโหว่ร้ายแรงได้? มาดูวิธีป้องกัน SQL Injection และการออกแบบฐานข้อมูลให้ปลอดภัยสำหรับโปรแกรมเมอร์มือใหม่

ทำไมโค้ดที่ดูไม่มีพิษมีภัย ถึงกลายเป็นช่องโหว่ร้ายแรงได้

เคยสงสัยไหมว่าทำไมเราถึงเจอบั๊ก (Bug - ข้อผิดพลาดในโปรแกรม) ที่ดูเหมือนไม่มีอะไร แต่กลับสร้างปัญหาใหญ่ให้ระบบได้ เรื่องนี้เริ่มจาก ฐานข้อมูล (Database - ที่เก็บข้อมูลของโปรแกรม) ของผมมีชั้นข้อมูลที่ชื่อว่า "1" โผล่ขึ้นมาเฉยๆ ซึ่งมันไม่ควรจะมีอยู่จริง

มันเป็นชั้นข้อมูลที่ว่างเปล่าและไม่เคยถูกเรียกใช้งานเลยสักครั้ง ทีมงานทุกคนมองข้ามมันไปเพราะคิดว่ามันแค่ ขยะดิจิทัล ที่ไม่มีผลอะไร แต่สุดท้ายผมกลับพบว่ามันคือร่องรอยของ SQL Injection (ช่องโหว่ที่ผู้ไม่หวังดีส่งคำสั่งฐานข้อมูลปลอมเข้ามา) ที่อันตรายมาก

บทเรียนนี้สอนให้รู้ว่า ความผิดพลาดเล็กๆ ในการออกแบบระบบ (Design) มักเป็นสัญญาณเตือนว่ามีบางอย่างไม่ถูกต้องในโค้ดของเรา วันนี้ผมจะพาไปดู 4 จุดที่ผมพลาดไปตอนเขียนโปรแกรมจัดการฐานข้อมูลระบบแผนที่ GIS (ระบบจัดการข้อมูลแผนที่) ของผมเองครับ

ออกแบบให้ยืดหยุ่น แต่ลืมระวังความปลอดภัย

ในระบบ GIS ของผม มีข้อมูลมากกว่า 2.7 ล้านรายการ ผมตัดสินใจไม่สร้าง ตาราง (Table - ที่เก็บข้อมูลในฐานข้อมูล) แยกตามแต่ละชั้นข้อมูล เพราะมันจะทำให้โครงสร้างฐานข้อมูลพังง่ายเมื่อมีคนสร้างชั้นข้อมูลใหม่บ่อยๆ

ผมเลยเลือกใช้ตารางหลักแค่ 3 ตาราง คือจุด เส้น และพื้นที่ แล้วใช้ Foreign Key (ตัวเชื่อมข้อมูลระหว่างตาราง) เพื่อระบุว่าข้อมูลนี้เป็นของชั้นไหน วิธีนี้ช่วยให้ระบบจัดการได้ง่ายขึ้นมาก แต่ก็แลกมาด้วยความเสี่ยงที่ผมคาดไม่ถึง

การที่ผมให้โปรแกรมสร้าง View (ตารางจำลองที่ดึงข้อมูลจากตารางจริงมาแสดง) ทุกครั้งที่มีคนสร้างชั้นข้อมูลใหม่ ผ่านคำสั่งที่รับค่ามาจากผู้ใช้โดยตรง คือจุดที่ทำให้เกิดปัญหาความปลอดภัยอย่างร้ายแรง

# โค้ดที่สร้าง View จากชื่อที่ผู้ใช้พิมพ์เข้ามาตรงๆ
view_name = f"{instance.name.replace(' ', '_')}"
# อันตรายมาก! ถ้าผู้ใช้ตั้งชื่อว่า 'x" AS SELECT 1; DROP TABLE project_layer; --'
cursor.execute(f"CREATE OR REPLACE VIEW {view_name} AS ...")

ตัวอย่างด้านบนแสดงให้เห็นว่า ถ้าเราเอาชื่อจากหน้าจอผู้ใช้ไปใส่ในคำสั่ง SQL ตรงๆ ผู้ใช้ที่หวังร้ายสามารถแอบใส่คำสั่งลบตาราง (DROP TABLE) เข้ามาได้เลย นี่คือเหตุผลว่าทำไมเราห้ามเชื่อใจข้อมูลที่มาจากผู้ใช้เด็ดขาด

เมื่อชื่อที่ตั้งเอง กลายเป็นปัญหาทางเทคนิค

สาเหตุที่ผมพบช่องโหว่นี้ ไม่ใช่เพราะผมนั่งไล่หาบั๊ก แต่เพราะผมเห็นชั้นข้อมูลชื่อ "1" ในระบบ ซึ่งมันสร้าง View ไม่สำเร็จ โปรแกรมเมอร์หลายคนอาจจะมองข้าม Error Log (บันทึกข้อผิดพลาด) เล็กๆ แบบนี้ไป แต่ในฐานข้อมูล มันคือสัญญาณเตือน

ในภาษา SQL ชื่อตารางหรือชื่อ View ห้ามขึ้นต้นด้วยตัวเลขโดดๆ ทำให้คำสั่งสร้าง View ของผมพังทันที โปรแกรมจึงมองข้ามการสร้างชั้นข้อมูลนั้นไป และนั่นคือสาเหตุที่มันว่างเปล่ามาตลอดหลายเดือน

การที่โปรแกรมพยายามทำตามคำสั่งผู้ใช้แล้วทำไม่ได้ คือจุดที่มือใหม่ต้องสังเกตให้ดีครับ ถ้ามันรันไม่ได้เพราะชื่อผิดปกติ นั่นแสดงว่าระบบเราไม่มีมาตรการป้องกันการตั้งชื่อที่อาจจะแฝงคำสั่งอันตรายเอาไว้

วิธีป้องกันด้วยการทำคำสั่งให้ปลอดภัย

วิธีแก้ปัญหาที่ถูกต้องคือการใช้เครื่องมือที่ช่วยจัดการชื่อตัวแปรให้เป็น Identifier (ชื่อที่ระบบยอมรับ) ที่ปลอดภัย แทนที่จะใช้การพิมพ์ชื่อต่อกันตรงๆ เราต้องใช้ฟังก์ชันที่ออกแบบมาเพื่อป้องกัน SQL Injection โดยเฉพาะ

เราควรใช้ไลบรารีอย่าง psycopg2.sql ใน Python เพื่อจัดการชื่อให้เป็นรูปแบบที่ฐานข้อมูลเข้าใจ ไม่ว่าผู้ใช้จะตั้งชื่อแปลกแค่ไหน มันจะถูกแปลงเป็นข้อความที่ปลอดภัยต่อคำสั่ง SQL เสมอ

  1. ใช้ sql.Identifier เพื่อบอกให้ระบบรู้ว่านี่คือชื่อของ View
  2. ใช้ sql.Literal เพื่อจัดการค่าข้อมูลที่นำไปใส่ในคำสั่ง
  3. ผลลัพธ์คือ ระบบจะมองชื่อนั้นเป็นแค่ข้อความก้อนหนึ่ง ไม่ใช่คำสั่งที่สั่งให้ลบตารางได้
from psycopg2 import sql
# วิธีที่ปลอดภัย: ใช้ Identifier จัดการชื่อให้เป็นข้อความที่ถูกต้อง
stmt = sql.SQL("CREATE OR REPLACE VIEW {view} AS ... WHERE f.layer_id = {lid}").format(
    view=sql.Identifier(view_name), # ชื่อจะถูกครอบด้วยเครื่องหมายคำพูดที่ถูกต้อง
    lid=sql.Literal(layer.id),      # ค่า ID จะถูกจัดการให้ปลอดภัย
)

การใช้เครื่องมือเหล่านี้ช่วยให้เราไม่ต้องกังวลว่าผู้ใช้จะพิมพ์อะไรมา เพราะทุกอย่างจะถูกแปลงเป็นรูปแบบที่ปลอดภัยก่อนส่งไปประมวลผลเสมอ

ระวังเรื่องตัวพิมพ์เล็กและตัวพิมพ์ใหญ่

หลังจากแก้เรื่องความปลอดภัยแล้ว ผมเกือบจะสร้างปัญหาใหม่ให้ตัวเองอีกรอบ เพราะในฐานข้อมูล Postgres (ระบบจัดการฐานข้อมูลยอดนิยม) ถ้าเราไม่ใส่เครื่องหมายคำพูด ชื่อจะถูกแปลงเป็นตัวพิมพ์เล็กทั้งหมด แต่ถ้าใส่เครื่องหมายคำพูด ชื่อจะคงสภาพเดิมไว้

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

นี่คือจุดที่โปรแกรมเมอร์มือใหม่มักพลาด คือการเปลี่ยนวิธีเขียนโค้ดโดยไม่ดู Legacy Data (ข้อมูลเดิมที่มีอยู่ก่อนแล้ว) เราต้องทำฟังก์ชันแปลงชื่อให้ตรงกับมาตรฐานเดิมของระบบก่อน เพื่อไม่ให้ข้อมูลเดิมที่ใช้งานอยู่พัง

สรุป: บทเรียนที่ได้จากบั๊กชั้นข้อมูลเลข 1

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

ถ้าคุณกำลังฝึกเขียนโค้ดและเจอ Error ที่อธิบายไม่ได้ อย่าเพิ่งกดผ่าน ให้ลองหาคำตอบว่าทำไมมันถึงเกิดขึ้น เพราะนั่นคือโอกาสทองที่จะทำให้คุณเก่งขึ้นกว่าเดิมหลายเท่าตัว

ตัวอย่างการนำไปใช้จริง: ก่อนจะส่งข้อมูลที่ผู้ใช้กรอกผ่าน Form (แบบฟอร์มบนหน้าเว็บ) เข้าสู่ฐานข้อมูลเสมอ ให้ลองถามตัวเองว่า "ถ้าฉันใส่คำสั่ง SQL ลงไปในช่องนี้ ระบบของฉันจะพังไหม" ถ้าคำตอบคือพัง ให้รีบใช้เครื่องมืออย่าง Parameterized Query (วิธีส่งค่าข้อมูลแยกกับคำสั่งหลัก) เพื่ออุดช่องโหว่นั้นทันทีครับ


ที่มา: One View Per Layer: Four Sharp Edges I Found in My Own Code — DEV Community

แชร์บทความ

Facebook X LINE

บทความที่เกี่ยวข้อง

เทคนิคเขียนแอป Flutter สำหรับ Meta Smart Glasses ให้ลื่นไหลและมีประสิทธิภาพ

เทคนิคเขียนแอป Flutter สำหรับ Meta Smart Glasses ให้ลื่นไหลและมีประสิทธิภาพ

เรียนรู้วิธีเขียนแอป Flutter เชื่อมต่อ Meta Smart Glasses ให้ทำงานเร็ว ไม่กระตุก ด้วยการวางสถาปัตยกรรมโค้ดและการจัดการข้อมูลแบบมือโปรที่มือใหม่ทำตามได้จริง

ที่มา: DEV Community

3 hours ago 10 นาที
5 views
เปรียบเทียบ WebSocket, SSE และ Polling เลือกวิธีทำระบบ Real-Time ให้เหมาะกับงาน

เปรียบเทียบ WebSocket, SSE และ Polling เลือกวิธีทำระบบ Real-Time ให้เหมาะกับงาน

อยากทำระบบ Real-Time แต่ไม่รู้จะเลือกใช้ Polling, SSE หรือ WebSocket ดี? มาดูวิธีเลือกใช้ให้เหมาะกับงาน เพื่อให้แอปของคุณทำงานลื่นไหลและประหยัดทรัพยากรเซิร์ฟเวอร์

ที่มา: DEV Community

6 hours ago 10 นาที
4 views