Sunday, August 1, 2010

PHP magic_quotes_gpc and SQL injection

เรื่อง magic_quotes_gpc ของ PHP อาจจะดูเก่าไปหน่อย เพราะ feature นี้ใน PHP 5.3 ก็ deprecated แล้ว และก็จะไม่มีใน PHP 6 แต่ผมก็ยังเชื่อว่าหลายๆ คนยังไม่เข้าใจว่า magic_quotes_gpc มันช่วยและไม่ช่วยป้องกัน SQL injection ยังไง

ตาม document ของ PHP ถ้า magic_quotes_gpc ได้ถูกเปิดใช้งาน ตัวอักษร ' (single-quote), " (double-quote), \ (backslash) และ NULL ในข้อมูล GET, POST, COOKIE จะถูก escape ด้วย backslash อัตโนมัติ

กรณีที่ 1 SQL injection กับ MySQL
ผมขอเริ่มด้วยกรณีที่ magic_quotes_gpc ช่วยป้องกัน เรื่อง SQL injection ตัวอย่างนี้  PHP programmer ทุกๆ คนน่าจะเคยเห็นแล้ว
นั่นคือ code สำหรับการทำ authentication โดยข้อมูลอยู่ใน MySQL database (สมมติว่าเก็บ password เป็น plaintext)
// ... initialization and establishing database connection ...
$result = mysql_query("SELECT * FROM account WHERE username='".$_GET['username']."' AND password='".$_GET['password']."'");
if (mysql_num_rows($result) > 0) {
  // authenticated
}
else {
  // login failed
}
ทดสอบด้วย classic SQL injection คือ username เป็น
admin 
และ password เป็น
' or 1=1# 
ถ้าปิด magic_quotes_gpc จะได้ SQL command เป็น
SELECT * FROM account WHERE username='admin' AND password='' or 1=1#'
หลังเครื่องหมาย # ใน MySQL คือ comment จะไม่รวมใน SQL command และโดยทั่วไป (รวมถึง MySQL) operator precedence ของ AND จะสูงกว่า OR นั่นคือ AND จะถูกคิดก่อน OR ถ้าใส่วงเล็บให้ดูง่ายก็จะเป็น
SELECT * FROM account WHERE (username='admin' AND password='') or 1=1
เนื่องจาก 1=1 เป็นจริงเสมอ ก็ทำให้ SQL command ของข้างบนเท่ากับ
SELECT * FROM account
ซึ่งก็จะ login ผ่าน :)

ในทางกลับกัน ถ้าเปิด magic_quotes_gpc จะได้ SQL command เป็น
SELECT * FROM account WHERE username='admin' AND password='\' or 1=1'
ก็จะ login ไม่ผ่าน :(

จะเห็นว่า ถ้าจะทำ SQL injection ในกรณีนี้ต้องมีการใช้ single-quote เข้าไปใน username หรือ password แต่ magic_quotes_gpc จะ escape single-quote ทำให้ไม่สามารถทำ SQL injection หรือพูดได้ว่า
magic_quotes_gpc ป้องกัน SQL injection ได้ ถ้า SQL injection ต้องใช้ตัวอักษร ' (single-quote), " (double-quote), \ (backslash) หรือ NULL

กรณีที่ 2 SQL injection กับ database อื่นๆ
ผมขอใช้ code เดิมจากกรณีที่ 1 แต่เปลี่ยน database เป็นตัวอื่นที่ escape single-quote ตาม SQL standard (เช่น Oracle, MSSQL) คือใช้ single-quote สองตัว เช่น 'O''hh' แต่ครั้งนี้ password ต้องเปลี่ยนเป็น
' or 1=1-- 
(-- คือ comment ตาม SQL standard)
คราวนี้ขอแสดงเฉพาะกรณีเปิด magic_quotes_gpc ก็จะได้ SQL command เป็น
SELECT * FROM account WHERE username='admin' AND password='\' or 1=1--'
สำหรับ database ที่ escape single-quote ตาม SQL standard จะมองว่า password คือ \ (backslash) ซึ่งก็จะ login ผ่านอยู่ดี :)

หลายคนอาจจะบอกว่า PHP กับ MSSQL ไม่น่าจะมีคนใช้ ส่วน Oracle ก็คงมีใช้น้อยมาก อันนี้ผมเห็นด้วย แล้ว PostgreSQL ละ
PostgreSQL ตอนแรกก็ใช้ backslash เพื่อที่จะ escape single-quote แต่ใน version 8 ก็จะมี option standard_conforming_strings ให้เลือกใช้แบบเดิม (สำหรับ backward compatibility) หรือแบบ SQL standard โดยที่ทาง PostgreSQL เองก็แนะนำให้ใช้ตาม standard

Note: PHP มี option magic_quotes_sybase เพื่อที่จะ escape single-quote สำหรับ database ที่ทำตาม standard

กรณีที่ 3 SQL injection กับข้อมูลที่เป็นตัวเลข
ตัวอย่างที่พบบ่อยๆ ของกรณีนี้ คือการแสดงข้อมูลตาม id ที่อยู่ใน database โดยที่ PHP code ที่ทำ SQL query เป็นดังนี้
mysql_query("SELECT col1, col2, col3 FROM data WHERE id=".$_GET['id']);
ทดสอบด้วยการใส่ค่า id เป็น
0 UNION SELECT username,password,1 FROM account 
จะได้ SQL command เป็น
SELECT col1, col2, col3 FROM data WHERE id=0 UNION SELECT username,password,1 FROM account
สมมติว่าใน database ไม่มี id ที่เป็น 0 SQL command ก็จะเป็น
SELECT username,password,1 FROM account
ทำให้แทนที่จะดึงข้อมูลจาก data table ก็กลายเป็นดึงข้อมูลมาจาก account table มาแสดงผล

จะเห็นว่า id เป็นตัวเลข และโดยส่วนมาก programmer ไม่ได้ใส่ single-quote สำหรับตัวเลข ดังนั้น SQL injection ไม่ต้องมี single-quote ก่อนที่จะเพิ่ม SQL command และ magic_quotes_gpc ก็ไม่สามารถช่วยป้องกันในกรณีได้ เพราะไม่มีตัวอักษรที่ต้อง escape

ตัวอย่างอื่นๆ ของ SQL injection กับข้อมูลที่เป็นตัวเลข คือเวลาแสดงข้อมูลเป็นหลายๆ หน้า ตัวเว็บจะมีการรับหน้า และจำนวนข้อมูลต่อหน้า

กรณีที่ 4 SQL injection กับ SQL operator
กรณีนี้ส่วนมากจะพบในหน้า Advanced Search ของเว็บที่ให้ผู้ใช้สามารถใส่หลายๆ เงื่อนไขในการค้นหา และสามารถเลือกได้ว่าจะนำมา AND หรือ OR โดยที่ทางเว็บส่ง AND หรือ OR ตรงๆ เช่น
HTML code snippet:
col1: <input name="col1" type="text" /><br/>
<input checked="checked" name="oper" type="radio" value="AND" /> และ
<input name="oper" type="radio" value="OR" /> หรือ<br/>
col2: <input name="col2" type="text" />
PHP code snippet:
$result = mysql_query("SELECT col1, col2, col3 FROM data WHERE col1='%".$_POST['col1']."%' ".$_POST['oper']." col2='%".$_POST['col2']."%'");
จากตัวอย่างผู้ใช้สามารถเงื่อนไขของ col1 กับ col2 และมี radio box เพื่อเลือก AND หรือ OR โดยตัวอย่าง SQL injection ก็คือแก้ไขค่า oper เป็น
UNION SELECT username,password,1 FROM account#
ทำให้ SQL command เป็น
SELECT col1, col2, col3 FROM data WHERE col1='%cond1%' UNION SELECT username,password,1 FROM account# col2='%cond2%'
ซึ่งก็คือค้นหาข้อมูลใน data table ตาม cond1 และข้อมูลใน account table ทั้งหมด

Note: ถ้า SQL command ใน PHP code ซับซ้อนกว่านี้ การทำ SQL injection ก็ยังเป็นไปได้ แค่อาจต้องเพิ่มบางส่วน เช่น วงเล็บ เพื่อให้ SQL syntax พร้อมกับทำ injection

จะเห็นว่ากรณีนี้ ก็เป็นอีกกรณีหนึ่งที่ magic_quotes_gpc ไม่สามารถป้องกัน SQL injection ได้

จากตัวอย่างทั้งหมดที่กล่าวมา คงทำให้เห็นกันแล้วว่า magic_quotes_gpc ช่วยและไม่ช่วยป้องกัน SQL injection ยังไง (จริงๆ คือนึกไม่ออกแล้ว)
ถ้ามีการเปิด magic_quotes_gpc ก็ยังสามารถทำ SQL injection ถ้าไม่จำเป็นต้องใช้ตัวอักษรที่ถูก escape
และก็จะจบก็คงต้องเขียนวิธีป้องกัน ที่มีอยู่หลักเดียวง่ายๆ คือตรวจสอบ หรือ filter input ที่มาจากผู้ใช้ทั้งหมด ก่อนนำมาประมวลผล

วิธีการป้องกันตามตัวอย่างที่ได้ยกมา (สมมติว่าไม่มีการใช้ magic_quotes_gpc)
1. สำหรับข้อมูลที่เป็นตัวอักษร ใช้
mysql_real_escape_string()
ก่อนที่ใช้นำไปใส่ใน SQL command
2. สำหรับข้อมูลตัวเลข ให้ทำการ cast เป็นตัวเลขก่อนด้วยการใช้
(int)
หรือ
(integer)
หรือใช้
intval()
function
3. สำหรับข้อมูลที่เป็นส่วนหนึ่งของ SQL syntax ก็ให้ตรวจสอบว่าเป็นที่เราอนุญาตหรือไม่ เช่นตัวอย่างในกรณีที่ 4 ก็ตรวจสอบว่าค่า oper เป็น AND หรือ OR เท่านั้น

สุดท้ายการที่ทาง PHP จะเอา magic_quotes_* ออกนั้น น่าจะเป็นเพราะว่า (อันนี้ความคิดผม) มันทำให้ programmer เข้าใจผิด และอาจทำให้สับสน รวมทั้งข้อมูลที่รับมาอาจจะไม่ได้เอามาใช้กับ database เช่น save ลงไฟล์ ทำให้ programmer ต้องมาแก้ไขข้อมูลให้ถูกต้องอีกด้วย
stripslashes()

2 comments:

  1. เนื้อหาดีมากครับ

    ReplyDelete
  2. เนื้อหาดีครับ

    ReplyDelete