กฎความปลอดภัย .htaccess ของ WordPress ที่ช่วยได้จริง
สิ่งที่ถูกขายในนามความปลอดภัย .htaccess ของ WordPress ส่วนใหญ่เป็นแค่ของประดับ กฎที่เปลี่ยนพื้นผิวการโจมตีของคุณได้จริงนั้นสั้น บทความนี้จะบอกว่าอันไหนเป็นอันไหน
เผยแพร่แล้ว
สิ่งที่ถูกขายในนาม “ความปลอดภัย .htaccess ของ WordPress” ส่วนใหญ่เป็นแค่ของประดับ กฎที่เปลี่ยนพื้นผิวการโจมตีของคุณได้จริงนั้นสั้น คือ บล็อกการรัน PHP ภายใน wp-content/uploads, ปฏิเสธการเข้าถึง wp-config.php โดยตรง และปิดการทำ directory indexing ส่วนกฎยอดฮิต เช่น การบล็อก xmlrpc.php “เพื่อหยุด brute force”, การซ่อนเวอร์ชัน WordPress, การก๊อปวาง user-agent list ของบอทเลว 200 บรรทัด นั้นมีตั้งแต่ช่วยได้เล็กน้อยไปจนถึงเป็นละครล้วนๆ ด้านล่างนี้คือว่าอันไหนเป็นอันไหน และเพราะอะไร
มีเรื่องหนึ่งที่ต้องเคลียร์ก่อน .htaccess ทำงานได้เฉพาะบน Apache เท่านั้น (และ LiteSpeed ซึ่งอ่านมันด้วย) บน nginx มันจะ ถูกละเลยทั้งหมด ไฟล์นั้นแค่วางอยู่เฉยๆ ในขณะที่คุณเชื่อว่าตัวเองได้รับการปกป้อง ถ้าโฮสต์ของคุณรัน nginx เรื่องพวกนี้ใช้ไม่ได้เลย และคุณต้องใช้บล็อก server/location แทน ตรวจสอบด้วย curl -I https://yoursite.com แล้วดูที่ header Server: ก่อนที่คุณจะเสียเวลาชั่วโมงหนึ่งไปกับการแก้ไฟล์ที่เซิร์ฟเวอร์ไม่เคยอ่าน
กฎที่สำคัญจริง: ห้าม PHP ใน /uploads
นี่คือกฎที่คุ้มค่าที่จะทำ wp-content/uploads ถูกออกแบบมาให้เขียนได้จากทุกที่ (world-writable) ทุกการอัพโหลดสื่อ ทุกปลั๊กอิน wordpress ที่บันทึกไฟล์ จะเขียนลงที่นั่น ถ้าผู้โจมตีเอาไฟล์ .php เข้าไปในไดเรกทอรีนั้นได้ (ผ่าน upload handler ที่มีช่องโหว่, ฟิลด์รูปภาพที่ไม่ตรวจสอบ MIME type, หรือปลั๊กอิน wordpress ที่ถูกเจาะ) ความต่างระหว่างแค่ความรำคาญกับการถูกยึดเครื่องแบบ remote-code-execution เต็มรูปแบบ อยู่ที่ว่าเซิร์ฟเวอร์จะ รัน ไฟล์นั้นเมื่อมีการร้องขอหรือไม่ ปฏิเสธการรันเสีย แล้ว payload ที่ถูกอัพโหลดก็เป็นแค่ไฟล์เฉื่อยๆ ที่นั่งอยู่บนดิสก์
วางโค้ดนี้ลงใน wp-content/uploads/.htaccess (สร้างไฟล์ขึ้นมาถ้ายังไม่มี)
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
นั่นคือ syntax ของ Apache 2.4 บนเวอร์ชันเก่า 2.2 ตัวที่เทียบเท่ากันคือ Order Deny,Allow / Deny from all การผสมสอง dialect นี้ในไฟล์เดียวคือสาเหตุที่พบบ่อยที่สุดของ 500 Internal Server Error ที่จู่ๆ ก็เกิดขึ้นหลังจาก “hardening” ถ้าทั้งเว็บล่มทันทีที่คุณเซฟ นั่นแทบจะเป็นเหตุผลนั้นเสมอ ตรวจสอบเวอร์ชันของคุณด้วย apachectl -v
ปกป้อง wp-config.php
wp-config.php เก็บ credential ของฐานข้อมูลและ auth salt ของคุณ ในขณะที่ PHP กำลังทำงาน การร้องขอไฟล์นี้โดยตรงจะได้หน้าเปล่ากลับมา เพราะ PHP รันไฟล์แทนที่จะพิมพ์มันออกมา ความเสี่ยงอยู่ที่กรณีที่มันพัง ถ้า PHP ล่ม, ถูกตั้งค่าผิดระหว่างการย้ายเซิร์ฟเวอร์ (migration), หรือ handler ถูกปิดใช้งาน Apache จะเสิร์ฟไฟล์นั้นเป็น plaintext และเทรหัสผ่านฐานข้อมูลของคุณให้ใครก็ตามที่ถาม การปฏิเสธการเข้าถึงคือประกันราคาถูกกันช่วงห้านาทีที่โชคร้าย
<Files wp-config.php>
Require all denied
</Files>
ปิด directory indexing
ถ้ามีใครเข้ามาที่โฟลเดอร์ที่ไม่มี index.php และ Apache เปิด Options +Indexes อยู่ มันจะแสดงรายการเนื้อหาข้างในออกมา ทั้งไฟล์ backup ทุกไฟล์ ทั้ง SQL dump ที่หลงเหลือที่คุณลืมไปแล้ว ปิดมันทั้งเว็บเลย
Options -Indexes
ความรุนแรงต่ำ แต่มีจริง และไม่ต้องแลกอะไรเลย
นั่นคือลิสต์แกนหลักตามความจริง คุณสามารถประกอบสิ่งเหล่านี้เข้าด้วยกัน พร้อม syntax ที่ถูกต้องระหว่าง 2.4 กับ 2.2 เพื่อไม่ให้เว็บของคุณ 500 ได้ด้วย ตัวสร้าง .htaccess ของ WordPress แทนที่จะก๊อปวางจากโพสต์ในฟอรัมที่เขียนไว้สำหรับ Apache คนละเวอร์ชัน
สิ่งที่ไม่ควรทำ
การบล็อก xmlrpc.php “เพื่อป้องกัน brute-force” นี่คืออันใหญ่ที่ทุกคนพูดต่อๆ กัน และมันผิดอย่างที่พูด ใช่ ในอดีตเมธอด system.multicall ของ XML-RPC เคยเปิดทางให้ผู้โจมตีมัดการเดารหัสผ่านหลายครั้งรวมเป็น request เดียวได้ ซึ่งเป็นการขยายผลจริง แต่ช่องทาง brute-force ที่พบบ่อยที่สุดอย่างท่วมท้นคือ POST request ธรรมดาๆ ไปยัง wp-login.php และการบล็อก xmlrpc.php ไม่ช่วยอะไรเลย กับเรื่องนั้น Brute force ถูกเอาชนะด้วย rate limiting, รหัสผ่านที่แข็งแรง และ 2FA ไม่ใช่ด้วยการฆ่า endpoint หนึ่งตัว มี เหตุผลที่ชอบธรรมในการปิด XML-RPC อยู่จริง คือฟีเจอร์ pingback ของมันสามารถถูกนำไปใช้ในทางที่ผิดเพื่อทำ DDoS reflection ได้ ดังนั้นถ้าคุณไม่ได้ใช้ Jetpack, แอปมือถือ, หรือ pingback การปิดมันจะช่วยลดพื้นผิวการโจมตี แค่อย่าหลอกตัวเองว่ามันคือเกราะป้องกัน brute-force ของคุณ เพราะมันไม่ใช่
การซ่อนเวอร์ชัน WordPress / เอา generator tag ออก การถอด readme.html และ tag <meta name="generator"> ออกให้ความรู้สึกเหมือนได้ hardening ผู้โจมตีสามารถ fingerprint เวอร์ชันของคุณได้จาก query string ของ asset ที่ถูก enqueue, markup ของ block-editor และร่องรอยอีกนับสิบภายในไม่กี่วินาที คุณไม่ได้ซ่อนอะไรเลย คุณแค่ทำให้ตัวเองรู้สึกว่ายุ่ง
บล็อกลิสต์ user-agent และ referrer ของบอทเลวยักษ์ใหญ่ User agent เป็น HTTP header ตัวเดียวที่ปลอมได้ ลิสต์พวกนี้ล้าสมัยตั้งแต่วันที่คุณก๊อปวาง มันบล็อกอะไรที่มีฝีมือจริงไม่ได้เลย และ Apache จะประมวลผลทุก regex ใน ทุก request คุณกำลังจ่ายภาษีด้านประสิทธิภาพจริงๆ เพื่อความปลอดภัยที่เป็นศูนย์ ข้ามมันไปเถอะ
การล็อก IP ให้ wp-login.php ดีมาก จนกระทั่ง ISP ของคุณหมุนเปลี่ยน address แล้วคุณก็ล็อกตัวเองออกจาก admin ของตัวเอง ใช้ได้เฉพาะเมื่อมี static IP จริงๆ เท่านั้น
การ redirect กัน author-enumeration (?author=1) โค้ด rewrite ใน .htaccess ที่คนก๊อปวางมานั้นไม่สมบูรณ์ในตัวเอง เพราะ REST endpoint /wp-json/wp/v2/users ยังคงแสดง username อยู่ การบล็อกทางหนึ่งในขณะที่อีกทางเปิดอยู่คือละคร
ยังติดอยู่ใช่ไหม?
ถ้ากฎทำให้เว็บ 500 นั่นคือปัญหา syntax ให้ดึงบล็อกสุดท้ายที่คุณเพิ่งเพิ่มเข้าไปออกแล้ว reload นั่นจะแยกปัญหาออกมาได้ทันที ถ้ากฎดูเหมือนไม่ทำอะไรเลย ให้ยืนยันว่าคุณอยู่บน Apache จริงๆ และ AllowOverride ถูกเปิดใช้งานสำหรับไดเรกทอรีนั้น (โฮสต์แบบ managed จำนวนมากจำกัดมันไว้) สร้างไฟล์จากเทมเพลตที่รู้ว่าใช้ได้ด้วย ตัวสร้าง .htaccess เก็บสามกฎที่สำคัญไว้ แล้วทิ้งที่เหลือ
FAQ
คำถาม
จะหยุดไม่ให้ไฟล์ PHP รันในโฟลเดอร์ uploads ของ WordPress ได้อย่างไร?
สร้างไฟล์ .htaccess ไว้ใน wp-content/uploads พร้อมบล็อก FilesMatch ที่จับไฟล์ .php แล้วใส่ Require all denied เพราะ uploads ถูกออกแบบมาให้เขียนได้จากทุกที่ การปฏิเสธการรันจึงทำให้ไฟล์ .php ที่ถูกแอบใส่เข้ามากลายเป็นแค่ไฟล์เฉื่อยบนดิสก์ แทนที่จะกลายเป็นการยึดเครื่องแบบ remote code execution
จำเป็นต้องบล็อกการเข้าถึง wp-config.php โดยตรงไหม?
จำเป็น และแทบไม่มีต้นทุนอะไร ให้เพิ่มบล็อก Files สำหรับ wp-config.php พร้อม Require all denied ตอนที่ PHP ทำงานอยู่ คำขอตรง ๆ จะได้แค่หน้าเปล่า แต่ถ้า PHP ล่ม ถูกตั้งค่าผิดระหว่างย้ายเว็บ หรือ handler ถูกปิด Apache จะเสิร์ฟไฟล์นั้นเป็นข้อความล้วนและเทข้อมูลรับรองฐานข้อมูลของคุณออกมาทั้งหมด
ทำไมเว็บขึ้น 500 Internal Server Error ทันทีหลังแก้ .htaccess?
เกือบทุกครั้งเป็นเพราะผสม syntax ของ Apache คนละเวอร์ชัน Require all denied คือ Apache 2.4 ส่วน Order Deny,Allow คู่กับ Deny from all คือ 2.2 การรวมสอง dialect ไว้ในไฟล์เดียวจะทำให้เว็บพังทันทีที่คุณเซฟ ให้ตรวจเวอร์ชันด้วย apachectl -v หรือลบบล็อกล่าสุดที่เพิ่งเพิ่มเข้าไปแล้วรีโหลด
.htaccess ใช้งานได้บน nginx ไหม?
ไม่ได้ .htaccess ทำงานเฉพาะบน Apache เท่านั้น และบน LiteSpeed ซึ่งอ่านไฟล์นี้ด้วย ส่วนบน nginx ไฟล์จะถูกละเลยทั้งหมด มันจึงวางอยู่เฉย ๆ ในขณะที่คุณเชื่อว่าได้รับการปกป้อง ให้รัน curl -I กับเว็บของคุณแล้วดู header Server: ก่อน จากนั้นค่อยใช้บล็อก server หรือ location แทน
การบล็อก xmlrpc.php หยุดการโจมตีแบบ brute force ใน WordPress ได้ไหม?
ไม่ได้ ช่องทาง brute force ที่พบบ่อยที่สุดคือคำขอ POST ธรรมดาไปที่ wp-login.php ซึ่งการบล็อก xmlrpc.php ไม่ได้ช่วยอะไรเลย brute force ถูกหยุดด้วยการจำกัดอัตราคำขอ รหัสผ่านที่แข็งแรง และ 2FA ส่วนการปิด XML-RPC ยังคุ้มอยู่ถ้าคุณไม่ได้ใช้ Jetpack แอปมือถือ หรือ pingback เพราะ pingback ถูกนำไปใช้สะท้อนการโจมตี DDoS ได้