Prepared Statements: Preventing SQL Injection
ในบทเรียนที่แล้ว เราได้เรียนรู้วิธีการสร้างและ execute SQL queries โดยใช้ MySQLi extension ใน PHP อย่างไรก็ตาม การสร้าง query โดยตรงจาก string ที่ประกอบขึ้นจากข้อมูลที่ผู้ใช้ป้อนเข้ามานั้น มีความเสี่ยงที่จะถูกโจมตีด้วยเทคนิคที่เรียกว่า SQL injection ในบทเรียนนี้ เราจะมาเรียนรู้วิธีการใช้ prepared statements ซึ่งเป็นกลไกที่ช่วยป้องกัน SQL injection ได้อย่างมีประสิทธิภาพ
SQL Injection คืออะไร?
SQL injection คือช่องโหว่ด้านความปลอดภัยที่เกิดขึ้นเมื่อ application อนุญาตให้ผู้ใช้สามารถแทรก SQL code ที่เป็นอันตรายเข้าไปใน query ที่ application จะ execute ตัวอย่างเช่น หาก application มี form ที่ให้ผู้ใช้ป้อน username และ password แล้วนำข้อมูลนั้นไปสร้าง SQL query เพื่อตรวจสอบการ login ดังนี้:
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = $conn->query($sql);
หากผู้ใช้ป้อน ' OR '1'='1 เป็น username และ password จะเป็นอะไรก็ได้ query ที่ได้จะเป็น:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'whatever'
เนื่องจาก '1'='1' เป็นจริงเสมอ query นี้จะ return ข้อมูลของผู้ใช้ทั้งหมดใน database ทำให้ผู้โจมตีสามารถเข้าสู่ระบบได้โดยไม่ต้องรู้ username และ password ที่ถูกต้อง
Prepared Statements คืออะไร?
Prepared statements เป็นกลไกที่ช่วยป้องกัน SQL injection โดยการแยก query ออกเป็นสองส่วน:
- Statement template: เป็น query ที่มี placeholder สำหรับ parameters ที่จะถูกแทนที่ในภายหลัง
- Parameters: เป็นข้อมูลที่ผู้ใช้ป้อนเข้ามา
เมื่อใช้ prepared statements application จะส่ง statement template ไปยัง database server ก่อน จากนั้นจึงส่ง parameters ไปแยกกัน ทำให้ database server สามารถแยกแยะระหว่าง SQL code กับข้อมูลที่ผู้ใช้ป้อนเข้ามาได้ และป้องกันไม่ให้ SQL code ที่เป็นอันตรายถูก execute
การใช้งาน Prepared Statements ใน PHP
ต่อไปนี้เป็นขั้นตอนการใช้งาน prepared statements ใน PHP โดยใช้ MySQLi extension:
- เตรียม statement: ใช้ method
prepare()ของmysqliobject เพื่อเตรียม statement template - Bind parameters: ใช้ method
bind_param()ของmysqli_stmtobject เพื่อ bind parameters กับ placeholder ใน statement template - Execute statement: ใช้ method
execute()ของmysqli_stmtobject เพื่อ execute statement - Fetch results (ถ้ามี): ใช้ method
get_result()และfetch_assoc()ของmysqli_stmtobject เพื่อ fetch ข้อมูลที่ได้จากการ query
ตัวอย่าง:
<?php
$servername = "localhost";
$username = "username";
$password = "password";
$dbname = "myDB";
// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Prepare statement
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
// Bind parameters
$username = $_POST['username'];
$password = $_POST['password'];
$stmt->bind_param("ss", $username, $password); // "ss" หมายถึง string, string
// Execute statement
$stmt->execute();
// Get result
$result = $stmt->get_result();
// Fetch results
if ($result->num_rows > 0) {
// output data of each row
while($row = $result->fetch_assoc()) {
echo "id: " . $row["id"]. " - Name: " . $row["username"]. " " . $row["password"]. "<br>";
}
} else {
echo "0 results";
}
// Close statement and connection
$stmt->close();
$conn->close();
?>
ในตัวอย่างนี้ ? เป็น placeholder สำหรับ parameters ที่จะถูก bind ในภายหลัง bind_param() method รับ parameters สองอย่าง:
- Type string: ระบุชนิดข้อมูลของ parameters แต่ละตัว "s" หมายถึง string, "i" หมายถึง integer, "d" หมายถึง double, "b" หมายถึง blob
- Parameters: ค่าของ parameters ที่จะถูก bind
ข้อดีของการใช้ Prepared Statements:
- ป้องกัน SQL injection: Prepared statements ช่วยแยกแยะระหว่าง SQL code กับข้อมูลที่ผู้ใช้ป้อนเข้ามา ทำให้ SQL code ที่เป็นอันตรายไม่สามารถถูก execute ได้
- ประสิทธิภาพ: Prepared statements สามารถถูก compile และ optimize ได้เพียงครั้งเดียว แล้วนำไปใช้ซ้ำได้หลายครั้ง ทำให้มีประสิทธิภาพมากกว่าการสร้าง query ใหม่ทุกครั้ง
- ความปลอดภัย: Prepared statements ช่วยลดความเสี่ยงในการเกิดข้อผิดพลาดที่อาจนำไปสู่ช่องโหว่ด้านความปลอดภัย
ข้อควรระวัง:
- ตรวจสอบ type string ให้ถูกต้อง: การระบุ type string ที่ไม่ถูกต้องอาจทำให้เกิดข้อผิดพลาด หรือทำให้ prepared statements ไม่สามารถป้องกัน SQL injection ได้อย่างมีประสิทธิภาพ
- หลีกเลี่ยงการใช้ string concatenation: การใช้ string concatenation เพื่อสร้าง SQL query ยังคงมีความเสี่ยงที่จะถูกโจมตีด้วย SQL injection แม้ว่าจะใช้ prepared statements แล้วก็ตาม
Trade-offs และแนวปฏิบัติที่ดี
การใช้ prepared statements มีข้อดีมากมาย แต่ก็มี trade-offs ที่ควรพิจารณา:
- Complexity: Prepared statements มีความซับซ้อนกว่าการสร้าง query โดยตรงจาก string
- Performance Overhead: แม้ว่าโดยทั่วไปแล้ว prepared statements จะมีประสิทธิภาพมากกว่า แต่ในบางกรณีอาจมี performance overhead เล็กน้อย
แนวปฏิบัติที่ดี:
- ใช้ prepared statements เสมอ: เมื่อใดก็ตามที่คุณต้องการสร้าง SQL query จากข้อมูลที่ผู้ใช้ป้อนเข้ามา ให้ใช้ prepared statements เพื่อป้องกัน SQL injection
- ใช้ parameterized queries ใน ORM: หากคุณใช้ Object-Relational Mapper (ORM) เช่น Doctrine หรือ Eloquent ให้ตรวจสอบให้แน่ใจว่า ORM นั้นใช้ parameterized queries เพื่อป้องกัน SQL injection
- Validate User Input: ตรวจสอบและ sanitize ข้อมูลที่ผู้ใช้ป้อนเข้ามาเสมอ เพื่อลดความเสี่ยงในการเกิด SQL injection และช่องโหว่อื่นๆ
- Least Privilege Principle: กำหนดสิทธิ์การเข้าถึง database ให้กับ application ของคุณเท่าที่จำเป็นเท่านั้น
สรุป
Prepared statements เป็นเครื่องมือสำคัญในการป้องกัน SQL injection ซึ่งเป็นหนึ่งในช่องโหว่ด้านความปลอดภัยที่พบบ่อยที่สุดใน web application การเรียนรู้วิธีการใช้ prepared statements อย่างถูกต้อง จะช่วยให้คุณสามารถสร้าง application ที่ปลอดภัยและมีประสิทธิภาพมากยิ่งขึ้น ในบทเรียนถัดไป เราจะมาเรียนรู้วิธีการทำงานกับ JSON data ใน PHP