2026年11月12日

2026年11月12日

ซ่อนเวอร์ชัน WordPress: generator tag, ?ver= และ readme.html

บทความนี้เป็นฉบับปรับให้เหมาะกับท้องถิ่นจากต้นฉบับภาษาญี่ปุ่น อ่านต้นฉบับภาษาญี่ปุ่น

โดยค่าเริ่มต้น WordPress จะเขียนหมายเลขเวอร์ชันของตัวเองลงใน HTML ของหน้าเว็บและในฟีด ผู้โจมตีอาจใช้ข้อมูลนี้เป็นเบาะแสว่าเป็นเวอร์ชันที่มีช่องโหว่ที่รู้จักแล้วหรือไม่ บทความนี้อธิบายวิธีตรวจว่าเวอร์ชันแสดงอยู่ที่ใดบ้าง และขั้นตอนการซ่อน เนื้อหาเป็นของเดือนตุลาคม 2026

สิ่งที่อยากให้รู้ก่อนคือ การซ่อนเวอร์ชันเป็นเพียงมาตรการเสริม เพราะยังมีวิธีเดาเวอร์ชันจากเนื้อหาของไฟล์ CSS หรือ JavaScript ได้ จึงไม่ได้หมายความว่าซ่อนแล้วจะป้องกันการระบุเวอร์ชันได้เสมอไป สิ่งที่ได้ผลที่สุดคือการอัปเดตแกนหลัก ปลั๊กอิน และธีมของ WordPress ให้เป็นเวอร์ชันล่าสุดอยู่เสมอ

จุดที่แสดงเวอร์ชัน

ตำแหน่ง สิ่งที่แสดง วิธีจัดการ
<head> ของ HTML แท็กอย่าง <meta name="generator" content="WordPress 6.9.4"> ฟิลเตอร์ the_generator
ฟีด RSS และ Atom บรรทัดอย่าง <generator>https://wordpress.org/?v=6.9.4</generator> ใช้ฟิลเตอร์ the_generator เช่นเดียวกัน
URL ของ CSS และ JavaScript ?ver= อย่าง style.min.css?ver=6.9.4 ฟิลเตอร์ style_loader_src และ script_loader_src
เฮดเดอร์ HTTP X-Powered-By: PHP/8.3.x (เป็นเวอร์ชันของ PHP ไม่ใช่ของ WordPress) expose_php ใน php.ini
readme.html บอกว่าใช้ WordPress อยู่ (readme.html ในปัจจุบันไม่มีหมายเลขเวอร์ชัน) ปฏิเสธการเข้าถึง

ข้อมูลไซต์ที่ /wp-json/ ของ REST API ส่งกลับมา (ชื่อ คำอธิบาย URL เขตเวลา namespace เป็นต้น) ไม่มีเวอร์ชันของ WordPress รวมอยู่ จึงไม่จำเป็นต้องตัดผลลัพธ์ของ REST API เพื่อจุดประสงค์ในการซ่อนเวอร์ชัน

ขั้นตอนที่ 1: ตรวจสถานะการแสดงผลปัจจุบัน

ให้แทนที่ด้วย URL ของเว็บไซต์ตนเอง แล้วรันบนคอมพิวเตอร์หรือเซิร์ฟเวอร์ของตนเอง หากใช้ปลั๊กอินแคชหรือ CDN ให้ล้างแคชก่อนหลังเปลี่ยนแปลงแล้วจึงตรวจสอบ

# generator tag ใน HTML
curl -s https://example.com/ | grep -i 'name="generator"'

# generator ในฟีด
curl -s https://example.com/feed/ | grep -i '<generator>'

# รายการค่า ver= ที่ติดอยู่กับ CSS และ JS
curl -s https://example.com/ | grep -oE 'ver=[0-9][0-9.]*' | sort | uniq -c

# X-Powered-By ในเฮดเดอร์ HTTP
curl -sI https://example.com/ | grep -i 'x-powered-by'

# ตรวจว่าอ่าน readme.html ได้หรือไม่ (ถ้าได้ 200 แสดงว่าอ่านได้)
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/readme.html

หากในรายการ ver= มีค่าเดียวกับเวอร์ชันของ WordPress ที่แสดงใน "แดชบอร์ด (Dashboard) → อัปเดต (Updates)" ของหน้าผู้ดูแล แสดงว่าสามารถรู้เวอร์ชันของ WordPress ได้จาก CSS หรือ JS นั้น เมื่อแกนหลักของ WordPress หรือธีมโหลด CSS และ JS โดยไม่ได้ระบุเวอร์ชัน เวอร์ชันของ WordPress จะถูกใส่ลงใน ver=

ขั้นตอนที่ 2: เลือกที่วางโค้ด

โค้ดในบทความนี้ให้วางไว้ที่ใดที่หนึ่งต่อไปนี้

  • mu-plugin (แนะนำ): หากวางไฟล์ PHP ไว้ในโฟลเดอร์ wp-content/mu-plugins/ (สร้างขึ้นหากยังไม่มี) ไฟล์จะถูกโหลดโดยไม่ต้องกดเปิดใช้งาน และผลยังคงอยู่แม้เปลี่ยนธีม
  • functions.php ของ child theme: หากเขียนใน functions.php ของธีมหลัก โค้ดจะหายเมื่อธีมอัปเดต

หากทำเป็น mu-plugin ให้รวมโค้ดของขั้นตอนถัดไปไว้ในไฟล์เดียว แล้วบันทึกด้วยชื่ออย่าง wp-content/mu-plugins/hide-wp-version.php

ขั้นตอนที่ 3: ซ่อนเวอร์ชันใน generator tag และ ver=

<?php
/**
 * Plugin Name: Hide WordPress Version
 * Description: ซ่อนเวอร์ชันของ WordPress ที่แสดงใน generator tag และใน ver= ของ CSS และ JS
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// ทำให้ <meta name="generator"> ใน HTML และ <generator> ในฟีดว่างเปล่า
add_filter( 'the_generator', '__return_empty_string' );

/**
 * เปลี่ยนเป็นค่าอื่นเฉพาะเมื่อ ver= ของ CSS และ JS ตรงกับเวอร์ชันของ WordPress
 * เพราะเป็นการเปลี่ยนค่าแทนการลบ ver= แคชของเบราว์เซอร์จึงอัปเดตแม้หลังอัปเดต WordPress
 */
function wpdr_replace_wp_version_query( $src ) {
	if ( ! is_string( $src ) || '' === $src ) {
		return $src;
	}

	$query = wp_parse_url( $src, PHP_URL_QUERY );
	if ( empty( $query ) ) {
		return $src;
	}

	parse_str( $query, $args );
	$wp_version = get_bloginfo( 'version' );

	if ( isset( $args['ver'] ) && $args['ver'] === $wp_version ) {
		$src = add_query_arg( 'ver', substr( wp_hash( $wp_version ), 0, 10 ), $src );
	}

	return $src;
}
add_filter( 'style_loader_src', 'wpdr_replace_wp_version_query', 20 );
add_filter( 'script_loader_src', 'wpdr_replace_wp_version_query', 20 );

ประเด็นสำคัญมีดังนี้

  • ฟิลเตอร์ the_generator มีผลทั้งกับ generator tag ใน HTML และ generator ในฟีด ส่วน remove_action( 'wp_head', 'wp_generator' ); ลบเฉพาะแท็กใน HTML จึงไม่มีผลกับฟีด
  • หากลบ ?ver= ทั้งหมด เมื่ออัปเดตปลั๊กอินหรือธีม เบราว์เซอร์อาจใช้ CSS และ JS เก่าต่อไป โค้ดข้างต้นจะเปลี่ยนเป็นสตริงอื่นที่สร้างจากเวอร์ชันเฉพาะเมื่อค่าตรงกับเวอร์ชันของ WordPress เมื่ออัปเดต WordPress ค่าก็เปลี่ยนตาม แคชจึงถูกอัปเดต
  • ver= ที่ปลั๊กอินหรือธีมระบุหมายเลขเวอร์ชันของตัวเองไว้ จะไม่ถูกเปลี่ยน

เมื่อบันทึกแล้ว ให้ใช้คำสั่งของขั้นตอนที่ 1 ตรวจว่า generator ไม่แสดงออกมา และในรายการ ver= ไม่เหลือเวอร์ชันของ WordPress หากใช้ปลั๊กอินแคช ให้ล้างแคชก่อนแล้วจึงตรวจสอบ

ขั้นตอนที่ 4: หยุดเฮดเดอร์ X-Powered-By

X-Powered-By เป็นเฮดเดอร์ที่ PHP เพิ่มเอง ไม่ใช่ WordPress และทำให้รู้เวอร์ชันของ PHP หากต้องการหยุด ให้ตั้ง expose_php เป็น Off ในไฟล์ตั้งค่า PHP (php.ini) ค่านี้เปลี่ยนได้ใน php.ini เท่านั้น

expose_php = Off

ตัวอย่างสำหรับ VPS ที่ใช้ Ubuntu และ PHP-FPM (ให้ปรับ 8.3 ให้ตรงกับเวอร์ชัน PHP ที่ใช้)

# ตรวจการตั้งค่าปัจจุบัน
grep -n '^expose_php' /etc/php/8.3/fpm/php.ini

# เมื่อแก้ไขแล้วให้รีสตาร์ต PHP-FPM
sudo systemctl restart php8.3-fpm

php.ini ของแพ็กเกจ Ubuntu ตั้งค่าเริ่มต้นเป็น expose_php = Off อยู่แล้ว หากเฮดเดอร์ยังแสดงอยู่ ให้ตรวจว่ามีการใช้ php.ini ไฟล์อื่นหรือไม่ บนเซิร์ฟเวอร์เช่า บางแห่งเปลี่ยนได้จากการตั้งค่า PHP ในหน้าจัดการ แต่บางแห่งเปลี่ยนไม่ได้

หากเปลี่ยน php.ini ไม่ได้ ก็มีวิธีถอดเฮดเดอร์ด้วยโค้ดของ WordPress อย่างไรก็ตาม send_headers เป็นฮุกที่ทำงานเมื่อ WordPress แสดงหน้าฝั่งหน้าบ้าน จึงไม่มีผลกับหน้าผู้ดูแลและหน้าล็อกอิน

add_action( 'send_headers', function () {
	header_remove( 'X-Powered-By' );
} );

ขั้นตอนที่ 5: ปฏิเสธการเข้าถึง readme.html และ license.txt

readme.html และ license.txt เป็นไฟล์ของแกนหลัก WordPress readme.html ในปัจจุบันไม่มีหมายเลขเวอร์ชัน แต่ก็รู้ได้ว่าใช้ WordPress อยู่

ต่อให้ลบไป เมื่ออัปเดตแกนหลัก WordPress ไฟล์ก็จะกลับมาอีก การปฏิเสธการเข้าถึงที่เว็บเซิร์ฟเวอร์จึงทำได้ง่ายกว่าการลบ

กรณี Apache (.htaccess)

เพิ่มต่อท้ายใน .htaccess ที่อยู่ในโฟลเดอร์ของ WordPress ส่วนระหว่าง # BEGIN WordPress และ # END WordPress WordPress จะเขียนทับเอง จึงให้เขียนไว้นอกส่วนนั้น เป็นรูปแบบของ Apache 2.4 (Order และ Deny from all เป็นรูปแบบเก่าของ Apache 2.2)

<FilesMatch "^(readme\.html|license\.txt)$">
    Require all denied
</FilesMatch>

กรณี Nginx

เพิ่มลงในบล็อก server ของไซต์ ตรวจสอบการตั้งค่า แล้วโหลดใหม่

location ~* ^/(readme\.html|license\.txt)$ {
    deny all;
}
sudo nginx -t && sudo systemctl reload nginx

หลังเพิ่มแล้ว ให้รัน curl -s -o /dev/null -w '%{http_code}\n' https://example.com/readme.html และตรวจว่าได้ 403

สิ่งที่ซ่อนแล้วก็ยังป้องกันไม่ได้

  • เนื้อหาของไฟล์ CSS และ JS ของ WordPress ต่างกันเล็กน้อยในแต่ละเวอร์ชัน จึงเดาเวอร์ชันได้หากดึงไฟล์มาเปรียบเทียบ
  • บางครั้งรู้เวอร์ชันของปลั๊กอินได้จาก readme.txt หรือ ver= ของปลั๊กอินและธีม สิ่งที่ถูกเล็งง่ายกว่าคือช่องโหว่ของปลั๊กอิน มากกว่าแกนหลัก
  • ผู้ที่ล็อกอินหน้าผู้ดูแลได้ยังเห็นเวอร์ชันเหมือนเดิม

ด้วยเหตุนี้ นอกจากการซ่อนเวอร์ชัน ควรทำควบคู่กับการอัปเดตแกนหลัก ปลั๊กอิน และธีม การลบปลั๊กอินที่ไม่ได้ใช้ และการปิดการแก้ไขไฟล์ในหน้าผู้ดูแล

สรุป

  • จุดหลักที่แสดงเวอร์ชันคือ generator ใน HTML และฟีด และ ?ver= ของ CSS และ JS ส่วน /wp-json/ ของ REST API ไม่มีเวอร์ชันของ WordPress รวมอยู่
  • add_filter( 'the_generator', '__return_empty_string' ); ลบ generator ได้ทั้งใน HTML และฟีด
  • อย่าลบ ?ver= แต่ให้เปลี่ยนเฉพาะค่าที่ตรงกับเวอร์ชันของ WordPress จะไม่ทำให้การอัปเดตแคชหยุดชะงัก
  • X-Powered-By หยุดได้ด้วย expose_php = Off ใน php.ini
  • ต่อให้ลบ readme.html ก็จะกลับมาเมื่ออัปเดต จึงให้ปฏิเสธการเข้าถึงที่เว็บเซิร์ฟเวอร์
  • การซ่อนเป็นเพียงมาตรการเสริม สิ่งสำคัญที่สุดคืออัปเดตต่อไปเรื่อย ๆ

บทความที่เกี่ยวข้อง:

มีคำถามเกี่ยวกับ WordPress ไหม ผู้เผยแพร่เว็บไซต์นี้อยู่ที่ญี่ปุ่นและตอบเป็นภาษาญี่ปุ่น ติดต่อ (ภาษาญี่ปุ่น)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲