สำหรับนักพัฒนาสาย PHP หรือ System Admin ที่ดูแลเว็บเซิร์ฟเวอร์ การตั้งค่าใน php.ini หลายร้อยบรรทัดมักเป็นสิ่งที่เราปล่อยให้เป็นค่าดีฟอลต์ (Default) เพราะคิดว่าระบบออกแบบมาอย่างปลอดภัยแล้ว แต่หนึ่งในฟีเจอร์ที่ถูกเปิดมาให้ตั้งแต่เกิดและมักกลายเป็น “บันไดขั้นสุดท้าย” ให้นักเจาะระบบยึดเซิร์ฟเวอร์ได้แบบไม่รู้ตัว คือฟีเจอร์ที่มีชื่อว่า session.upload_progress
บทความนี้จะพาไปเจาะลึกว่าฟีเจอร์นี้สร้างมาเพื่อแก้ปัญหาอะไร ทำงานอย่างไร และเหตุใดจึงมักถูกนำไปใช้เป็นอาวุธในการเปลี่ยนช่องโหว่อ่านไฟล์ธรรมดา (LFI) ให้กลายเป็นการยึดเซิร์ฟเวอร์เต็มรูปแบบ (RCE)
จุดเริ่มต้น: ทำไม PHP ต้องมี Session Upload Progress?
ย้อนกลับไปในยุคของ Web 1.0 และ 2.0 ยุคแรก (ช่วงก่อน HTML5 และก่อนที่เบราว์เซอร์จะมี Fetch/XHR Upload API สมบูรณ์) เวลาผู้ใช้งานอัปโหลดไฟล์ขนาดใหญ่ เช่น วิดีโอหรือไฟล์ Zip ขนาดหลายสิบเมกะไบต์ หน้าจอเว็บจะค้างนิ่งอยู่แบบนั้น ผู้ใช้ไม่มีทางรู้ได้เลยว่าไฟล์กำลังส่งอยู่ ส่งไปกี่เปอร์เซ็นต์ หรือเบราว์เซอร์ค้างไปแล้ว
ทีมพัฒนา PHP จึงเพิ่มฟีเจอร์ Session Upload Progress เข้ามาตั้งแต่ PHP 5.4 เพื่อเป็นทางลัดให้ Server ช่วยรายงานความคืบหน้า
- ฟอร์ม HTML ส่งไฟล์ขึ้นมาพร้อมระบุคีย์พิเศษ (ดีฟอลต์คือ
PHP_SESSION_UPLOAD_PROGRESS) - ขณะที่เว็บเซิร์ฟเวอร์กำลังสตรีมรับข้อมูลไบต์ของไฟล์ PHP จะแอบเขียนสถานะลงในตัวแปร
$_SESSIONของผู้ใช้เป็นระยะ - หน้าเว็บใช้ JavaScript ส่ง AJAX ยิงกลับมาถามเซิร์ฟเวอร์เรื่อยๆ เพื่อดึงข้อมูลไคลเอนต์นั้นไปวาดเป็นหลอด Progress Bar
ตัวอย่างโครงสร้างข้อมูลใน $_SESSION
เมื่อเปิดฟีเจอร์นี้ ข้อมูล Session จะถูกเขียนโครงสร้างหน้าตาประมาณนี้โดยอัตโนมัติ
PHP
$_SESSION["upload_progress_avatar_upload"] = [
"start_time" => 1716300000,
"content_length" => 15728640, // ขนาดไฟล์ทั้งหมด (Bytes)
"bytes_processed" => 7864320, // ส่งมาแล้วกี่ไบต์
"done" => false,
"files" => [
[
"field_name" => "file",
"name" => "backup.zip",
"tmp_name" => "/tmp/phpYzdqkD",
"error" => 0,
"done" => false,
"start_time" => 1716300000,
"bytes_processed" => 7864320,
]
]
];
ดาบสองคม: เบื้องหลังที่ทำให้เกิดช่องโหว่ความปลอดภัย
ความสะดวกในวันวานกลายมาเป็นความเสี่ยงระดับวิกฤตในปัจจุบัน ด้วยพฤติกรรมการทำงานเบื้องหลัง 3 ประการของ PHP
ประการที่ 1: บังคับสร้างไฟล์ Session แม้โค้ดจะไม่สั่ง session_start()
โดยปกติ PHP จะสร้างไฟล์ Session บน Disk (มักอยู่ที่ /tmp/sess_<session_id>) ก็ต่อเมื่อในโค้ดมีคำสั่ง session_start() เท่านั้น แต่เมื่อเปิดใช้งาน Upload Progress ทันทีที่มี Request แบบ multipart/form-data ส่งมาพร้อมชื่อฟิลด์ตามที่กำหนด PHP engine จะสร้างไฟล์ Session ขึ้นบนเครื่องทันที แม้โค้ดปลายทางจะเป็นแค่ไฟล์ PHP เปล่า ๆ ก็ตาม
ประการที่ 2: Attacker กำหนดชื่อไฟล์และเนื้อหาในไฟล์ได้ตามใจ
- กำหนดชื่อไฟล์
ผู้โจมตีสามารถส่ง HTTP HeaderCookie: PHPSESSID=hacked;ทำให้เซิร์ฟเวอร์สร้างไฟล์ชื่อตรง ๆ ว่า/tmp/sess_hacked - กำหนดเนื้อหา (Payload Injection)
ค่าของฟิลด์PHP_SESSION_UPLOAD_PROGRESSสามารถส่งเป็นโค้ด PHP ลงไปได้ตรง ๆ เช่น
HTMLPHP_SESSION_UPLOAD_PROGRESS = "<?php system($_GET['c']); ?>"
PHP จะนำสตริงนี้ไปบันทึกเป็นชื่อ Key ภายในไฟล์/tmp/sess_hacked
ประการที่ 3: ปัญหาการแย่งชิงจังหวะ (Race Condition)
แม้ PHP จะมีออปชัน session.upload_progress.cleanup = 1 ซึ่งทำหน้าที่ล้างข้อมูล Upload Progress ทันทีที่การอัปโหลดเสร็จสิ้น แต่ในช่วงเสี้ยววินาทีที่ไฟล์กำลังอัปโหลด (หรือกรณีที่แฮกเกอร์จงใจส่งไฟล์ขนาดยักษ์แบบหน่วงความเร็วเอาไว้) ไฟล์ Session ที่แฝงโค้ด PHP อันตรายจะยังคงค้างอยู่บนดิสก์
ท่าไม้ตายยอดฮิต: Local File Inclusion (LFI) to RCE
เมื่อระบบมีช่องโหว่ประเภท Local File Inclusion (LFI) เช่น โค้ดมีการเขียนแบบไม่รัดกุม
PHP
<?php
// vuln.php
$page = $_GET['page'];
include($page); // เช่น include("/var/log/nginx/access.log");
ในอดีต หากเซิร์ฟเวอร์ไม่มีจุดให้อัปโหลดไฟล์ และไม่มีการเปิด Log Injection ช่องโหว่ LFI อาจทำได้แค่อ่านไฟล์ Config ในเครื่อง (เช่น /etc/passwd หรือ wp-config.php) แต่ไม่สามารถรันคำสั่ง Shell ได้
แต่เมื่อมี Session Upload Progress เข้ามาช่วย
Plaintext
[Attacker]
│
├─► 1. ส่ง POST Request ขนาดใหญ่ค้างไว้
│ Header: Cookie: PHPSESSID=evil
│ Form: PHP_SESSION_UPLOAD_PROGRESS = "<?php system('id'); ?>"
│
[PHP Engine]
│
├─► 2. สร้างไฟล์ /tmp/sess_evil ซึ่งมีโค้ดอันตรายฝังอยู่ข้างในทันที
│
[Attacker] (ส่ง Request คู่ขนานทันที)
│
└─► 3. ส่ง GET Request: vuln.php?page=/tmp/sess_evil
PHP รันคำสั่ง system('id') สำเร็จ ──► ยึดเซิร์ฟเวอร์ (RCE)
ผู้โจมตีไม่จำเป็นต้องมีสิทธิ์อัปโหลดไฟล์บนเว็บ และไม่ต้องพึ่งพาไฟล์ใด ๆ บนระบบเลย เพียงแค่ยิงแพ็กเก็ตสองเส้นเข้าเซิร์ฟเวอร์พร้อมกัน ก็สามารถยึดสิทธิ์ระดับ Web User (เช่น www-data หรือ nobody) ได้ทันที
โลกยุคปัจจุบัน: ทำไมฟีเจอร์นี้ถึงหมดความจำเป็น?
ในสถาปัตยกรรมเว็บสมัยใหม่ เบราว์เซอร์ทุกตัวรองรับมาตรฐานการติดตามสถานะไฟล์ผ่านฝั่ง Client-side อย่างสมบูรณ์แบบแล้ว
- ใช้ Fetch API หรือ
XMLHttpRequest.upload.onprogressคำนวณขนาดไบต์ที่ส่งออกจากเบราว์เซอร์แบบ Real-time - ไม่กินทรัพยากร Disk I/O ของเซิร์ฟเวอร์ในการอ่าน/เขียนไฟล์ Session
- ไม่ต้องคอยยิง AJAX ซ้ำซ้อนเข้าหลังบ้านเพื่อขอเช็ค Progress ให้เปลือง Bandwidth
- สามารถอัปโหลดตรงไปยัง Object Storage (เช่น AWS S3, Cloudflare R2) ได้โดยไม่ต้องผ่านเว็บเซิร์ฟเวอร์ PHP ด้วยซ้ำ
ฟีเจอร์ session.upload_progress ใน PHP จึงกลายเป็นเสมือน Legacy Feature ที่แทบไม่มีการหยิบมาใช้อย่างถูกต้องอีกต่อไป แต่กลับเปิดอ้าไว้เป็นพื้นผิวการโจมตี (Attack Surface) ให้ระบบตกอยู่ในความเสี่ยง
วิธี Hardening ปิดฟีเจอร์นี้บนเซิร์ฟเวอร์
การปิดฟีเจอร์นี้ไม่ส่งผลกระทบต่อระบบส่วนใหญ่ (รวมถึง WordPress, Laravel, Symfony หรือ Custom CMS ทั่วไป)
วิธีที่ 1: ปิดผ่าน php.ini
เปิดไฟล์ php.ini ของระบบ (ทั้งใน CLI และ FPM) แล้วแก้ไขค่าดังนี้:
Ini, TOML
; ปิดการติดตาม upload progress
session.upload_progress.enabled = 0
วิธีที่ 2: ล็อกตายตัวผ่าน PHP-FPM Pool Config (แนะนำ)
เพื่อป้องกันไม่ให้สคริปต์ในระดับแอปพลิเคชันหรือปลั๊กอินแอบเปิดใช้งานผ่านคำสั่ง ini_set() ควรใช้คำสั่งระดับ Admin ในไฟล์คอนฟิก Pool ของ PHP-FPM (เช่น /etc/php/[version]/fpm/pool.d/www.conf):
Ini, TOML
php_admin_flag[session.upload_progress.enabled] = off
หลังจากแก้ไข อย่าลืมสั่งรีสตาร์ตเซอร์วิส PHP-FPM เพื่อให้คอนฟิกมีผลทันที:
Bash
sudo systemctl restart php*-fpm
และสามารถตรวจสอบความถูกต้องได้ด้วยคำสั่ง
Bash
php-fpm -i | grep session.upload_progress
สรุป
ช่องโหว่ความปลอดภัยร้ายแรงหลายครั้งไม่ได้เกิดจากข้อผิดพลาดของโค้ดที่ซับซ้อน แต่เกิดจาก “ฟังก์ชันอำนวยความสะดวกในอดีต” ที่หมดประโยชน์ไปแล้วแต่ยังคงเปิดทิ้งไว้ตามค่าเริ่มต้น การสละเวลาตรวจสอบและปิด session.upload_progress.enabled ในกระบวนการ Server Hardening จึงเป็นวิธีที่ง่าย ใช้เวลาไม่ถึง 2 นาที แต่ตัดช่องทางการโจมตี LFI-to-RCE ยอดนิยมของบรรดาแฮกเกอร์ได้อย่างมีประสิทธิภาพ