สิ่งที่ทุกคนต้องรู้ (ทั้งมือใหม่และเก่า) ในการเขียนโปรแกรมติดต่อกับฐานข้อมูลด้วย PHP หากไม่อยากให้ระบบที่เขียนนั้นถูก HACK ได้ !!!
โค้ด PHP ที่เกี่ยวกับ mysql ที่เขียนโดยมือสมัครเล่น (หรือแม้แต่มืออาชีพ)
ที่สามารถพบเห็นได้ทั่วไปในเว็บบอร์ดและในบทความของเว็บต่างๆ
หากพิจารณาดูแล้ว ส่วนใหญ่เป็นโค้ดที่มีความปลอดภัยต่ำ บทความนี้จะอธิบายว่าทำไม และจะแก้ไขปรับปรุงให้ดีขึ้นได้อย่างไร
การเขียนโปรแกรมติดต่อกับฐานข้อมูลนั้น จะต้องมีการสั่งให้ PHP อ่านหรือเพิ่มเติมแก้ไขข้อมูลในฐานข้อมูล
ซึ่งมักจะต้องเกี่ยวพันกับข้อมูลที่รับมาจากผู้ใช้ เพื่อที่จะทำการค้นหา หรือเปลี่ยนแปลงข้อมูล
ซึ่งตัวอย่างที่พบเห็นได้ทั่วไป จะคล้ายๆ แบบนี้
$sql = "SELECT * FROM `member` WHERE `username` = '$_POST[username]' AND `password` = '$_POST[password]'";
$result = mysql_query($sql);
if (!$result) {
die(mysql_error());
}
$user = mysql_fetch_array($result);
โค้ดดังกล่าวเป็นระบบ login เพื่อค้นหาบัญชีผู้ใช้ในระบบ และตรวจว่ารหัสผ่านตรงหรือไม่
หากเจอบัญชีผู้ใช้ และรหัสผ่านตรงกับที่ส่งมา ก็จะดำเนินการต่อไป
จะเห็นได้ว่า มีการรับข้อมูลจากผู้ใช้ ($_POST['username'] และ $_POST['password'] )
และนำไปประกอบกับ query โดยตรง
ซึ่งโค้ดลักษณะนี้เสี่ยงต่อการกระทำที่เรียกว่า SQL Injection ครับ
มาดูกันว่า SQL Injection เป็นอย่างไร
สมมติว่าเรา login ด้วย
username: cookiephp
password: 0123456789
ก็จะได้ตัวแปร $sql ที่มีค่า
SELECT * FROM `member` WHERE `username` = 'cookiephp' AND `password` = '0123456789'
ซึ่งจะไม่มีปัญหาอะไร
แต่ถ้าสมมติว่าเรา login ด้วย
username: cookiephp
password: -_-'
ก็จะได้ตัวแปร $sql ที่มีค่า
SELECT * FROM `member` WHERE `username` = 'cookiephp' AND `password` = '-_-''
ซึ่งจะทำให้เกิด error แบบนี้
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''-_-''
สังเกตตรง `password` = '-_-''
กลายเป็นว่ารหัสผ่านของเราซึ่งก็คือ -_-' จะไปปิด string เสียก่อน
และทำให้ค่าของรหัสผ่านที่จะใช้เปรียบเทียบนั้นเปลี่ยนเป็น -_-
และทำให้มี ' เกินมา 1 ตัว
กรณีแบบนี้เรียกว่า SQL Injection แปลแบบบ้านๆ ได้ว่า การเสียบเข้าไปใน SQL ซึ่งในที่นี้ เราเสียบเครื่องหมาย ' เข้าไปนั่นเอง
ซึ่งในตัวอย่างนี้มีผลเสียเพียงแค่เกิด error
แต่ หากเรา login ด้วย
username: cookiephp
password: ' OR username = 'cookiephp' AND TRUE <> '
ก็จะได้ตัวแปร $sql ที่มีค่า
SELECT * FROM `member` WHERE `username` = 'cookiephp' AND `password` = '' OR username = 'cookiephp' AND TRUE <> ''
ซึ่งจะตรงกันข้ามกับตัวอย่างก่อนหน้านี้
คือไม่ทำให้ SQL มันผิดพลาด แต่ทำให้เป็น SQL ที่มีความหมายเปลี่ยนไป
หากมีชื่อผู้ใช้ดังกล่าวอยู่ในฐานข้อมูล แม้รหัสผ่านจะไม่ตรงก็ไม่มีผล
เพราะเงือนไขที่สองหลัง OR จะเป็นจริงเสมอ
ทำให้เราสามารถ login ในนามของใครก็ได้ เพียงแค่รู้จักชื่อผู้ใช้
วิธีป้องกัน SQL Injection
ในการเอาค่าที่รับมาจากผู้ใช้ไปรวมเป็น query นั้น
เราต้อง "escape" ค่าที่รับมาจากผู้ใช้ด้วยฟังก์ชั่นที่เหมาะสม (ซึ่งแล้วแต่ชนิดของฐานข้อมูล) ก่อนที่จะนำไปใช้
ซึ่งสำหรับ MySQL นั้นมีสองฟังก์ชั่นคือ
mysql_escape_string() และ mysql_real_escape_string()
ซึ่งแนะนำให้ใช้ตัวหลัง
ตัวอย่าง
$_POST['username'] = mysql_real_escape_string($_POST['username']);
$_POST['password'] = mysql_real_escape_string($_POST['password']);
$sql = "SELECT * FROM `member` WHERE `username` = '$_POST[username]' AND `password` = '$_POST[password]'";
หากเรา "escape" ค่าที่รับมาจากผู้ใช้แล้ว ต่อให้มีเครื่องหมาย ' อยู่ในค่านั้นๆ ก็จะไม่มีปัญหาอีกต่อไป
สมมติว่าเราส่งพาสเวิร์ดที่มีค่า -_-' ตามตัวอย่างก่อนหน้านี้
ก็จะได้ตัวแปร $sql ที่มีค่า
SELECT * FROM `member` WHERE `username` = 'cookiephp' AND `password` = '-_-\''
ซึ่ง \' จะไม่ทำให้ SQL ผิดเพี้ยน เพราะ MySQL จะตีความเป็นเครื่องหมาย ' ให้เป็น "ข้อมูล" ไม่ใช่ "ไวยากรณ์ ของ SQL"
มาเปลี่ยนวิธีการสร้าง query กันเถอะ
จากตัวอย่างข้างบน สามารถทำให้โค้ดกระชับขึ้นได้ด้วยการหลีกเลี่ยงการกำหนดค่าลงตัวแปรด้วยการเชื่อมต่อสตริง
ใช้การเชื่อมต่อสตริงเพื่อสร้าง SELECT query
// ไม่ต้องมีการกำหนดค่าลงตัวแปรก่อนนำไปใช้
// ลดขั้นตอนการทำงานของ PHP ลง
// ประหยัดหน่วยความจำ
$sql = "SELECT * FROM `member` WHERE `username` = '"
. mysql_real_escape_string($_POST['username'])
. "' AND `password` = '"
. mysql_real_escape_string($_POST['password'])
. "'";
แต่ไม่แนะนำ เพราะเขียนผิดพลาดได้ง่ายและแก้ไขยาก (แต่ยกตัวอย่างให้ดูเพราะนักพัฒนาของไทยส่วนใหญ่นิยมใช้วิธีนี้กัน)
แนะนำให้ใช้ฟังก์ชั่น sprintf() เพื่อการแก้ไขที่สะดวกขึ้นในภายหลัง และทำให้อ่านง่ายมากขึ้นด้วย
sprintf() นั้นเป็นฟังก์ชั่นที่ใช้ "แทนที่ค่าต่างๆ ลงใน string ตามรูปแบบที่ต้องการ"
โดยมีรูปแบบการใช้ดังนี้ sprintf(รูปแบบ , ค่าแทนที่ )
รูปแบบ คือ string ที่เครื่องหมาย % แล้วตามด้วยอักษรภาษาอังกฤษบางตัว เช่น %s , %d (แต่สำหรับการสร้าง query เราใช้แบบเดียวคือ %s ) จะทำให้กลายเป็นจุดที่ค่าอื่นจะมาแทนที่ หากจะแสดงเครื่องหมาย % ต้องใช้ %%
ค่าแทนที่ สามารถมีได้หลายค่า
ตัวอย่างการใช้ sprintf()
echo sprintf('I am %s years old.', 10); // %s คือจุดที่จะแทนที่ด้วยค่าอื่น ในที่นี้คือ 10 จะได้ผลเป็น
// I am 10 years old.
echo sprintf('I have %s brothers and %s sisters.', 2, 4); // ค่าแทนที่มากกว่า 1 ค่า
// I have 2 brothers and 4 sisters.
echo sprintf('<div style="width: %s%%">Hello World</div>', 50); // %% จะกลายเป็น %
// <div style="width: 50%">Hello World</div>
ใช้ sprintf() เพื่อสร้าง SELECT query
// อาจจะทำงานช้ากว่าการเชื่อมต่อสตริง
// แต่ประหยัดหน่วยความจำเหมือนกัน
// และอ่านง่าย แก้ไขง่าย กว่ากันเยอะ
$sql = sprintf(
"SELECT * FROM `member` WHERE `username` = '%s' AND `password` = '%s'",
mysql_real_escape_string($_POST['username']), // %s ตัวที่ 1
mysql_real_escape_string($_POST['password']) // %s ตัวที่ 2
);
ใช้ sprintf() เพื่อสร้าง INSERT query
$sql = sprintf(
"
INSERT INTO `members`
(`id`, `username`, `password`, `first_name`, `last_name`)
VALUES
('', '%s', '%s', '%s', '%s')
",
mysql_real_escape_string($_POST['username']), // %s ตัวที่ 1
mysql_real_escape_string($_POST['password']), // %s ตัวที่ 2
mysql_real_escape_string($_POST['first_name']), // %s ตัวที่ 3
mysql_real_escape_string($_POST['last_name']) // %s ตัวที่ 4
);
ใช้ sprintf() เพื่อสร้าง UPDATE query
$sql = sprintf(
"
UPDATE `members`
SET
`username` = '%s',
`password` = '%s',
`first_name` = '%s',
`last_name` = '%s'
WHERE `id` = '%s'
LIMIT 1
",
mysql_real_escape_string($_POST['username']), // %s ตัวที่ 1
mysql_real_escape_string($_POST['password']), // %s ตัวที่ 2
mysql_real_escape_string($_POST['first_name']), // %s ตัวที่ 3
mysql_real_escape_string($_POST['last_name']), // %s ตัวที่ 4
mysql_real_escape_string($_POST['id']) // %s ตัวที่ 5
);
แหล่งข้อมูลอ้างอิง
mysql_escape_string() - http://www.php.net/manual/en/function.mysql-escape-string.php
mysql_real_escape_string() - http://www.php.net/manual/en/function.mysql-real-escape-string.php
sprintf() - http://www.php.net/manual/en/function.sprintf.php
PHP - Basic MySQL Injection - http://www.youtube.com/watch?v=SDHlfqd4CjM
How to prevent SQL injection in PHP? - http://stackoverflow.com/questions/60174/how-to-prevent-sql-injection-in-php Tag : PHP
ประวัติการแก้ไข 2013-02-18 13:13:25
Date :
2013-02-18 12:56:33
By :
cookiephp
View :
2555
Reply :
37
น่าจะเขียเป็นบทความไปเลยนะครับ ดีครับๆ ความรู้ใหม่
ขอบคุณครับ
Date :
2013-02-18 13:11:34
By :
ALTELMA
เขียนไปแล้วตั้งแต่เมื่อวานครับ แต่ยังไม่ได้รับการรับรองครับ เลยโพสต์ไว้ในนี้ด้วย
อาจจะเห็นกันได้มากกว่า และเผื่อจะได้มาถกเถียงกันด้วย
Date :
2013-02-18 13:14:55
By :
cookiephp
ถ้า setting PHP ของคุณมีการตั้ง magic_quotes_gpc ให้ทำงานไว้ มันจะ escape พวกตัวแปร $_GET, $_POST ให้อัตโนมัติครับ
แต่นั่นหมายความว่าเมื่อไหร่ที่ย้ายโค้ดไปเครื่องที่ปิด magic_quotes_gpc ก็จะมีช่องโหว่ทันทีครับ
แต่จะทำแบบตัวอย่างได้ คุณต้องใช้วิธีเขียนแบบไม่ได้ escape และเช็คค่า password ใน SQL เหมือนในตัวอย่างนะครับ
Date :
2013-02-18 14:01:01
By :
cookiephp
ประวัติการแก้ไข 2013-02-18 14:12:04
Date :
2013-02-18 14:11:33
By :
triplea
Thank ๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆๆ
Date :
2013-02-18 14:27:29
By :
apisitp
magic_quotes_gpc คือ setting อันนึงของ PHP
ที่จะทำการ escape ตัวแปร $_GET, $_POST ให้เอง
เช่น
เรามี input ชื่อ username
<input name="username" type="text" />
แล้วเราพิมพ์ '_' ส่งไปที่ PHP ที่ค่า magic_quotes_gpc เปิดอยู่
ค่าที่รับใน PHP จะเป็น \'_\' แทนที่จะเป็น '_' เฉย คือมี \ เพิ่มเข้ามาให้
Date :
2013-02-18 14:55:25
By :
cookiephp
ยอดเยี่ยมมากๆครับ
Date :
2013-02-18 15:08:36
By :
Dragons_first
เพิ่มเติม บางทีถ้าคุณเขียน script เช็ค value ไม่ดีพอคุณก็อาจจะโดนแบบนี้ได้ครับ
เหตุการเกิดจาก กรอกข้อมูล input แต่ไม่มีการเช็คค่าใดๆ Hacker ยัด script prompt() หรือทดสอบ alert เพื่อทีจะเขียนโค๊ดยัดใส่ในขั้นต่อไป อันนี้ก็อาจจะเกิดได้ครับ
Date :
2013-02-18 15:13:00
By :
Ex-[S]i[L]e[N]t
PHP - Basic MySQL Injection ===> นี้เป็นคลิปของ phpacademy หรือเปล่าครับ
Date :
2013-02-18 16:08:20
By :
popnakub
ใส่ user มั่วๆ
แล้ว ใส่ pass
' OR 1=1=1 AND TRUE <> '
ก็เข้าได้ ครับ ผมตกใจมาก
ไม่ต้องใส่ user ที่ถูกต้อง เลย เหอะๆ
Date :
2013-02-19 11:44:33
By :
theteza02
งั้นก็คงเหลือแต่ PDO แล้วสิครับ -.- โค๊ดยาวเกิ้ล -.-"
Date :
2013-02-19 14:05:47
By :
Ex-[S]i[L]e[N]t
แค่เคยได้ยินมาหนะครับ ถ้าผิดต้องขออภัย
Date :
2013-02-19 14:50:42
By :
Ex-[S]i[L]e[N]t
ขอบคุณครับ T T
Date :
2013-02-20 11:28:49
By :
cookiephp
บทความดี ๆ เดียวจะแชร์ให้ทุกบทความครับ
Date :
2013-02-27 10:16:50
By :
mr.win
Load balance : Server 02