Insecure Deserialization คืออะไรและทำไมต้องระวัง
เวลาเราเขียนโปรแกรม เรามักต้องเก็บข้อมูลของวัตถุ (Object) ไว้ในฐานข้อมูลหรือไฟล์ เพื่อเอามาใช้งานต่อได้ในภายหลัง กระบวนการที่เปลี่ยนวัตถุให้กลายเป็นข้อความตัวอักษรยาวๆ เพื่อให้ง่ายต่อการเก็บหรือส่งต่อ เราเรียกว่า Serialization (การแปลงข้อมูลให้เป็นรูปแบบที่ส่งผ่านได้ง่าย)
ส่วนกระบวนการย้อนกลับ คือการเอาข้อความเหล่านั้นมาเปลี่ยนคืนเป็นวัตถุให้โปรแกรมใช้งานได้เหมือนเดิม เราเรียกว่า Deserialization (การแปลงข้อมูลกลับมาเป็นวัตถุ) มันเหมือนกับการแกะกล่องพัสดุที่ถูกห่อมาอย่างดี เพื่อเอาของข้างในออกมาใช้งานนั่นเอง
Insecure Deserialization (การแปลงข้อมูลกลับมาเป็นวัตถุแบบไม่ปลอดภัย) คือช่องโหว่ร้ายแรงที่เกิดขึ้นเมื่อเรายอมให้ผู้ใช้งานส่งข้อมูลแบบที่ว่านี้เข้ามาในระบบโดยไม่ตรวจสอบ หากเราเชื่อใจข้อมูลที่ผู้ใช้งานส่งมาให้มากเกินไป เขาอาจจะแอบใส่ "ของแถม" ที่เป็นอันตรายไว้ในกล่องพัสดุนั้น เพื่อหลอกให้โปรแกรมของเราทำงานบางอย่างที่เขาต้องการได้
รู้จักกับ Serialization ใน PHP
ในภาษา PHP เรามีฟังก์ชันหลักสองตัวที่ใช้จัดการเรื่องนี้ คือ serialize() สำหรับแปลงวัตถุเป็นข้อความ และ unserialize() สำหรับแปลงข้อความกลับเป็นวัตถุ ข้อมูลที่ได้จะเป็นรูปแบบพิเศษที่ระบุชื่อคลาส (Class - ต้นแบบของวัตถุ) และค่าตัวแปรต่างๆ ไว้ข้างใน
ลองนึกภาพว่าเรามีข้อมูลผู้ใช้งานที่ชื่อ "John" ระบบจะแปลงเป็นข้อความหน้าตาแบบนี้ O:4:"User":2:{s:4:"name";s:4:"John"...} เมื่อโปรแกรมเรียกใช้ unserialize() ระบบจะอ่านข้อความนี้ แล้วสร้างวัตถุจากคลาส User ขึ้นมาใหม่โดยอัตโนมัติพร้อมใส่ชื่อ John ให้ทันที
จุดที่อันตรายที่สุดคือขั้นตอนการสร้างวัตถุใหม่นี่แหละ เพราะถ้าคนร้ายสามารถแก้ไขข้อความนี้ได้ เขาจะไม่ได้แค่เปลี่ยนชื่อผู้ใช้เท่านั้น แต่อาจจะสั่งให้โปรแกรมสร้างวัตถุจากคลาสอื่นที่เขากำหนดเองได้ด้วย ซึ่งถ้าคลาสนั้นมีคำสั่งแอบแฝงอยู่ มันก็เหมือนเราเปิดประตูให้โจรเข้ามาในบ้านโดยไม่รู้ตัว
// ตัวอย่างการทำ Serialization
$user = new User();
$user->name = "John";
$data = serialize($user);
echo $data; // ผลลัพธ์: O:4:"User":1:{s:4:"name";s:4:"John";}
// ตัวอย่างการทำ Deserialization
$raw = 'O:4:"User":1:{s:4:"name";s:4:"John";}';
$obj = unserialize($raw);
echo $obj->name; // ผลลัพธ์: John
บรรทัดแรกเราสร้างวัตถุ User ขึ้นมา ส่วนบรรทัดที่สองแปลงเป็นข้อความเพื่อจัดเก็บ ต่อมาฟังก์ชัน unserialize() จะอ่านข้อความนั้นแล้วเปลี่ยนกลับเป็นวัตถุ User ให้เราใช้งานต่อได้ตามปกติ
Magic Methods: จุดเชื่อมต่อที่โจรชอบใช้
ใน PHP มีสิ่งที่เรียกว่า Magic Methods (ฟังก์ชันวิเศษที่ทำงานอัตโนมัติ) ซึ่งเป็นฟังก์ชันที่ถูกเรียกใช้โดยตัวภาษาเองในจังหวะต่างๆ เช่น ตอนที่วัตถุถูกสร้างขึ้น หรือตอนที่วัตถุถูกทำลายทิ้งไป ตัวอย่างเช่น __destruct() ที่จะทำงานทันทีเมื่อโปรแกรมจบการทำงาน
ถ้าคนร้ายสามารถควบคุมข้อมูลที่ถูก unserialize() ได้ เขาจะสามารถกำหนดค่าตัวแปรในวัตถุนั้นได้ และเมื่อ Magic Methods ทำงาน มันจะใช้ค่าตัวแปรที่คนร้ายตั้งไว้ไปทำคำสั่งต่างๆ ทันที นี่คือสิ่งที่เรียกว่าการ "ป้อนข้อมูลเพื่อควบคุมการทำงาน" ของโปรแกรม
ลองจินตนาการว่ามีคลาสที่เขียนไฟล์ลงเครื่องโดยอัตโนมัติในฟังก์ชัน __destruct() หากคนร้ายส่งข้อมูลที่ระบุชื่อไฟล์เป็น shell.php (ไฟล์ที่สั่งรันคำสั่งได้) เข้ามา โปรแกรมของเราก็จะเผลอสร้างไฟล์อันตรายนี้ขึ้นมาให้คนร้ายเองโดยไม่ตั้งใจ
class FileLogger {
public $logFile;
public function __destruct() {
// บรรทัดนี้จะทำงานเองเมื่อจบโปรแกรม
file_put_contents($this->logFile, "log data");
}
}
// หากคนร้ายส่ง $logFile = 'hacker.php' เข้ามา
// ระบบจะสร้างไฟล์ hacker.php ให้ทันที
โค้ดตัวอย่างนี้แสดงให้เห็นว่าฟังก์ชัน __destruct() จะทำงานเองอัตโนมัติ หากเรานำข้อมูลจากผู้ใช้มาใส่ใน $logFile โดยตรง คนร้ายจะสามารถเลือกเขียนไฟล์ไปที่ไหนก็ได้บนเซิร์ฟเวอร์ของเรา
POP Chains: การต่อจิ๊กซอว์เพื่อบุกรุก
ในโลกความเป็นจริง ช่องโหว่ไม่ได้เกิดขึ้นจากคลาสเดียวเสมอไป แต่มันคือการใช้ POP Chains (Property Oriented Programming Chains - การนำคลาสหลายตัวมาเชื่อมต่อกันเพื่อสร้างคำสั่งอันตราย) คนร้ายจะนำคลาสหลายๆ ตัวในระบบมาต่อกันเหมือนจิ๊กซอว์ เพื่อพาไปสู่เป้าหมายที่ใหญ่กว่า
คนร้ายจะเริ่มจากคลาสหนึ่งที่ดูเหมือนไม่มีพิษมีภัย แล้วตั้งค่าตัวแปรให้มันไปเรียกฟังก์ชันในอีกคลาสหนึ่งต่อกันไปเรื่อยๆ จนกระทั่งไปถึงจุดที่สามารถสั่งรันคำสั่งในระบบเซิร์ฟเวอร์ได้ นี่คือเหตุผลว่าทำไมเราถึงต้องระวังทุกคลาสในโปรเจกต์ ไม่ใช่แค่คลาสที่รับข้อมูลโดยตรง
การป้องกัน POP Chains ทำได้ยากเพราะมันอาศัยความสัมพันธ์ของคลาสต่างๆ ที่เราเขียนไว้เอง ดังนั้นวิธีที่ดีที่สุดคือการไม่ยอมให้ข้อมูลจากผู้ใช้เข้าไปสู่ฟังก์ชัน unserialize() ตั้งแต่แรก หากไม่จำเป็นจริงๆ ให้ใช้รูปแบบข้อมูลที่ปลอดภัยกว่าแทน เช่น JSON (รูปแบบข้อมูลมาตรฐานที่อ่านง่ายและปลอดภัยกว่า)
วิธีป้องกันตัวเองในฐานะโปรแกรมเมอร์
กฎเหล็กข้อแรกและข้อสำคัญที่สุดคือ ห้ามใช้ unserialize() กับข้อมูลที่มาจากผู้ใช้โดยตรง หากจำเป็นต้องเก็บข้อมูลหรือส่งข้อมูล ให้เปลี่ยนไปใช้ json_encode() และ json_decode() แทน เพราะ JSON ไม่มีการเรียกใช้ Magic Methods หรือการสร้างวัตถุโดยอัตโนมัติ
หากคุณจำเป็นต้องใช้ unserialize() จริงๆ ให้ทำการ Sign (ลงลายเซ็นดิจิทัล) ข้อมูลนั้นด้วยรหัสลับก่อนเก็บ เมื่ออ่านข้อมูลกลับมา ต้องตรวจสอบลายเซ็นก่อนเสมอว่าข้อมูลถูกแก้ไขไปหรือไม่ หากข้อมูลไม่ตรงกับลายเซ็น ให้ปฏิเสธการใช้งานทันที
สุดท้ายคือการอัปเดต PHP ให้เป็นเวอร์ชันล่าสุดอยู่เสมอ และคอยตรวจสอบ Library (ชุดคำสั่งเสริม) ที่เรานำมาใช้ในโปรเจกต์ เพราะบางครั้งช่องโหว่อาจจะไม่ได้มาจากโค้ดที่เราเขียนเอง แต่มาจากเครื่องมือภายนอกที่เราดึงเข้ามาใช้งานนั่นเอง
สรุป: เปลี่ยนนิสัยเพื่อความปลอดภัย
การเข้าใจเรื่อง Insecure Deserialization จะช่วยให้คุณมองเห็นว่าข้อมูลทุกอย่างที่ผู้ใช้ส่งเข้ามาคือความเสี่ยง คุณต้องคิดเสมอว่าข้อมูลเหล่านั้นอาจถูกแก้ไขและนำมาหลอกล่อให้โปรแกรมของคุณทำงานผิดพลาดได้เสมอ
วิธีนำไปใช้จริง: เมื่อต้องเก็บข้อมูลสถานะผู้ใช้ (Session) หรือค่าสถานะต่างๆ ให้เปลี่ยนมาใช้ JSON แทนการ Serialize วัตถุโดยตรง หากต้องทำโปรเจกต์ส่งอาจารย์หรือทำงานจริง ให้ตรวจเช็คทุกครั้งว่าเราได้ทำลายจุดเสี่ยงเหล่านี้ไปแล้วหรือยัง
จำไว้ว่าโปรแกรมเมอร์ที่เก่งไม่ได้วัดกันแค่เขียนโค้ดได้เร็ว แต่ต้องเขียนโค้ดที่ "ปลอดภัย" ด้วย การฝึกฝนตั้งคำถามกับโค้ดตัวเองว่า "ถ้าคนร้ายแก้ไขค่านี้ได้ จะเกิดอะไรขึ้น" คือก้าวสำคัญที่จะทำให้คุณเป็นนักพัฒนาที่มีคุณภาพและน่าเชื่อถือในสายงานนี้ครับ
ที่มา: Insecure Deserialization in PHP — When Your Cache Becomes an Attack Surface — DEV Community: laravel