การใช้ Prepared Statements เพื่อป้องกัน SQL Injection ใน SQLite
ในบทความก่อนหน้า เราได้เรียนรู้วิธีการใช้งาน SQLite ร่วมกับ PHP เพื่อจัดการข้อมูลในฐานข้อมูล ไม่ว่าจะเป็นการเพิ่ม, แก้ไข, ลบ, หรืออ่านข้อมูล แต่สิ่งหนึ่งที่สำคัญอย่างยิ่งในการพัฒนาแอปพลิเคชันที่ทำงานกับฐานข้อมูลคือเรื่องของความปลอดภัย โดยเฉพาะอย่างยิ่งการป้องกัน SQL Injection ในบทนี้ เราจะมาเรียนรู้วิธีการใช้ Prepared Statements เพื่อป้องกันภัยคุกคามนี้ใน SQLite
SQL Injection คืออะไร?
SQL Injection เป็นช่องโหว่ด้านความปลอดภัยที่เกิดขึ้นเมื่อแอปพลิเคชันอนุญาตให้ผู้ใช้งานแทรกโค้ด SQL ที่เป็นอันตรายเข้าไปในคำสั่ง SQL ที่แอปพลิเคชันสร้างขึ้น โค้ด SQL ที่แทรกเข้ามานี้อาจทำให้ผู้โจมตีสามารถเข้าถึงข้อมูลที่ไม่ได้รับอนุญาต, แก้ไขข้อมูล, หรือแม้กระทั่งลบข้อมูลในฐานข้อมูลได้
ตัวอย่าง:
สมมติว่าเรามีฟอร์มล็อกอินที่รับชื่อผู้ใช้และรหัสผ่าน และสร้างคำสั่ง SQL เพื่อตรวจสอบข้อมูลดังนี้:
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '" . $username . "' AND password = '" . $password . "'";
// Execute the query (อันตราย!)
หากผู้ใช้งานใส่ข้อมูลต่อไปนี้ในช่องชื่อผู้ใช้:
' OR '1'='1
คำสั่ง SQL ที่ถูกสร้างขึ้นจะเป็นดังนี้:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '...'
เนื่องจากเงื่อนไข '1'='1' เป็นจริงเสมอ คำสั่งนี้จะดึงข้อมูลผู้ใช้ทั้งหมดในตาราง users ออกมา ทำให้ผู้โจมตีสามารถล็อกอินเข้าสู่ระบบได้โดยไม่ต้องรู้รหัสผ่านที่ถูกต้อง นี่คือตัวอย่างง่ายๆ ของ SQL Injection ซึ่งอาจนำไปสู่ความเสียหายที่ร้ายแรงกว่านี้ได้
Prepared Statements คืออะไร?
Prepared Statements หรือ Parameterized Queries เป็นวิธีการสร้างคำสั่ง SQL ที่แยกส่วนของคำสั่ง (โครงสร้าง SQL) ออกจากข้อมูลที่จะใช้ในคำสั่ง (พารามิเตอร์) โดยโครงสร้าง SQL จะถูกเตรียมไว้ล่วงหน้า (pre-compiled) และพารามิเตอร์จะถูกส่งเข้าไปในคำสั่ง SQL ที่เตรียมไว้ในภายหลัง
ข้อดีของการใช้ Prepared Statements:
- ป้องกัน SQL Injection: เนื่องจากข้อมูลถูกส่งไปเป็นพารามิเตอร์แยกต่างหากจากโครงสร้าง SQL ฐานข้อมูลจะถือว่าข้อมูลเหล่านี้เป็นข้อมูลธรรมดา ไม่ใช่ส่วนหนึ่งของคำสั่ง SQL ทำให้ไม่สามารถแทรกโค้ด SQL ที่เป็นอันตรายได้
- ประสิทธิภาพ: เนื่องจากโครงสร้าง SQL ถูกเตรียมไว้ล่วงหน้า การ execute คำสั่ง SQL ที่มีพารามิเตอร์เดียวกันหลายครั้งจะทำได้รวดเร็วกว่าการสร้างคำสั่ง SQL ใหม่ทุกครั้ง
- ความปลอดภัย: ช่วยลดความเสี่ยงจากข้อผิดพลาดในการเขียนโค้ดที่อาจนำไปสู่ช่องโหว่ด้านความปลอดภัย
วิธีการใช้งาน Prepared Statements ใน SQLite ด้วย PHP
ในการใช้งาน Prepared Statements ใน SQLite ด้วย PHP เราจะใช้ PDO (PHP Data Objects) ซึ่งเป็น extension ที่ช่วยให้เราสามารถเข้าถึงฐานข้อมูลต่างๆ ได้อย่างง่ายดายและปลอดภัย
- เชื่อมต่อกับฐานข้อมูล SQLite ด้วย PDO:
new PDO('sqlite:mydatabase.db'): สร้าง object PDO เพื่อเชื่อมต่อกับฐานข้อมูล SQLite ชื่อmydatabase.db$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION): ตั้งค่าให้ PDO แสดง error ในรูปแบบ exception ซึ่งช่วยให้เราจัดการ error ได้ง่ายขึ้น- เตรียมคำสั่ง SQL ด้วย Prepared Statement:
$db->prepare("..."): เตรียมคำสั่ง SQL โดยใช้ placeholder:usernameและ:passwordแทนที่ค่าที่จะใส่ในภายหลัง- ผูกค่า (bind) พารามิเตอร์:
$stmt->bindParam(':username', $username): ผูกค่าของตัวแปร$usernameกับ placeholder:username$stmt->bindParam(':password', $password): ผูกค่าของตัวแปร$passwordกับ placeholder:password- Execute คำสั่ง SQL:
$stmt->execute(): Execute คำสั่ง SQL ที่เตรียมไว้- ดึงข้อมูล:
$stmt->fetchAll(PDO::FETCH_ASSOC): ดึงข้อมูลทั้งหมดที่ได้จากการ query ในรูปแบบ associative array
try {
$db = new PDO('sqlite:mydatabase.db');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // Enable error reporting
} catch (PDOException $e) {
echo "Connection failed: " . $e->getMessage();
exit;
}
คำอธิบาย:
$stmt = $db->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
คำอธิบาย:
$username = $_POST['username'];
$password = $_POST['password'];
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $password);
คำอธิบาย:
ข้อควรระวัง: การใช้ bindParam จะส่งค่าของตัวแปรโดย reference ซึ่งหมายความว่าถ้าค่าของตัวแปรเปลี่ยนไปหลังจาก bind แล้ว ค่าที่ใช้ใน query ก็จะเปลี่ยนตามไปด้วย ถ้าต้องการส่งค่าโดย value สามารถใช้ bindValue แทนได้
$stmt->execute();
คำอธิบาย:
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($result) > 0) {
// Login successful
echo "Login successful!";
} else {
// Login failed
echo "Login failed!";
}
คำอธิบาย:
โค้ดตัวอย่างทั้งหมด:
<?php
try {
$db = new PDO('sqlite:mydatabase.db');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
echo "Connection failed: " . $e->getMessage();
exit;
}
$username = $_POST['username'];
$password = $_POST['password'];
$stmt = $db->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $password);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($result) > 0) {
echo "Login successful!";
} else {
echo "Login failed!";
}
?>
ด้วยการใช้ Prepared Statements เราสามารถป้องกัน SQL Injection ได้อย่างมีประสิทธิภาพ และทำให้แอปพลิเคชันของเราปลอดภัยมากยิ่งขึ้น
Trade-offs: การใช้ Prepared Statements อาจทำให้โค้ดดูยาวขึ้นเล็กน้อย แต่สิ่งที่ได้กลับมาคือความปลอดภัยที่มากขึ้น ซึ่งคุ้มค่าอย่างยิ่ง
แนวปฏิบัติที่ดี:
- ใช้ Prepared Statements เสมอเมื่อทำงานกับข้อมูลที่มาจากผู้ใช้งาน
- หลีกเลี่ยงการสร้างคำสั่ง SQL โดยตรงจากการต่อ string
- ตรวจสอบและ validate ข้อมูลที่ได้รับจากผู้ใช้งานก่อนนำไปใช้งาน
ในบทความนี้ เราได้เรียนรู้วิธีการป้องกัน SQL Injection โดยใช้ Prepared Statements ใน SQLite ซึ่งเป็นสิ่งสำคัญอย่างยิ่งในการพัฒนาแอปพลิเคชันที่ปลอดภัย ในบทต่อไป เราจะสรุปเนื้อหาทั้งหมดที่เราได้เรียนรู้ในบทเรียน SQLite ฉบับเริ่มต้นนี้