hardening.php

I’ll build this as a full inc/security/hardening.php module for your rx-theme, with safe WordPress-native hardening only, so it avoids breaking admin, REST, AJAX, cron, login, and block editor features.

Yes, my dear friend. Create this file:

rx-theme/inc/security/hardening.php

Then load it from functions.php.

<?php
/**
 * RX Theme Security Hardening
 *
 * File: inc/security/hardening.php
 *
 * Purpose:
 * - Add WordPress-native theme-level security hardening.
 * - Reduce exposed information.
 * - Disable risky endpoints/features where safe.
 * - Add common browser security headers.
 * - Protect login, REST API, XML-RPC, author enumeration, uploads, and embeds.
 *
 * Important:
 * This file is defensive hardening only. It does not replace:
 * - Server firewall
 * - Cloudflare/WAF
 * - Security plugin
 * - Regular updates
 * - Strong passwords
 * - Backups
 *
 * @package RX_Theme
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'RX_Theme_Security_Hardening' ) ) :

final class RX_Theme_Security_Hardening {

	/**
	 * Boot all security features.
	 */
	public static function init(): void {

		/*
		 * Basic WordPress hardening.
		 */
		add_action( 'init', array( __CLASS__, 'disable_file_editor' ), 1 );
		add_action( 'init', array( __CLASS__, 'remove_unwanted_head_links' ), 20 );

		/*
		 * Security headers.
		 */
		add_action( 'send_headers', array( __CLASS__, 'send_security_headers' ), 20 );

		/*
		 * WordPress version and generator cleanup.
		 */
		add_filter( 'the_generator', '__return_empty_string' );
		add_filter( 'style_loader_src', array( __CLASS__, 'remove_wp_version_from_assets' ), 9999 );
		add_filter( 'script_loader_src', array( __CLASS__, 'remove_wp_version_from_assets' ), 9999 );

		/*
		 * XML-RPC hardening.
		 */
		add_filter( 'xmlrpc_enabled', '__return_false' );
		add_filter( 'xmlrpc_methods', array( __CLASS__, 'disable_xmlrpc_methods' ) );
		add_filter( 'wp_headers', array( __CLASS__, 'remove_x_pingback_header' ) );

		/*
		 * REST API hardening.
		 */
		add_filter( 'rest_authentication_errors', array( __CLASS__, 'restrict_sensitive_rest_routes' ) );
		add_filter( 'rest_endpoints', array( __CLASS__, 'remove_public_user_rest_routes' ) );

		/*
		 * Login hardening.
		 */
		add_filter( 'login_errors', array( __CLASS__, 'generic_login_error' ) );
		add_filter( 'login_headerurl', array( __CLASS__, 'login_logo_url' ) );
		add_filter( 'login_headertext', array( __CLASS__, 'login_logo_title' ) );

		/*
		 * Author enumeration protection.
		 */
		add_action( 'template_redirect', array( __CLASS__, 'block_author_enumeration' ), 1 );
		add_filter( 'redirect_canonical', array( __CLASS__, 'stop_author_query_redirect' ), 10, 2 );

		/*
		 * Upload hardening.
		 */
		add_filter( 'upload_mimes', array( __CLASS__, 'restrict_upload_mimes' ), 999 );
		add_filter( 'wp_handle_upload_prefilter', array( __CLASS__, 'block_dangerous_upload_files' ) );
		add_filter( 'sanitize_file_name', array( __CLASS__, 'extra_sanitize_file_name' ), 10, 1 );

		/*
		 * Comment and pingback hardening.
		 */
		add_filter( 'pre_ping', array( __CLASS__, 'disable_self_pingbacks' ) );
		add_filter( 'pings_open', '__return_false', 20, 2 );

		/*
		 * oEmbed and discovery cleanup.
		 */
		add_action( 'init', array( __CLASS__, 'disable_unneeded_embeds' ), 20 );

		/*
		 * Admin and dashboard hardening.
		 */
		add_action( 'admin_init', array( __CLASS__, 'admin_security_checks' ), 1 );
		add_filter( 'admin_footer_text', array( __CLASS__, 'clean_admin_footer_text' ) );
		add_filter( 'update_footer', array( __CLASS__, 'hide_admin_version_footer' ), 999 );

		/*
		 * Feed hardening.
		 */
		add_action( 'do_feed', array( __CLASS__, 'protect_feeds' ), 1 );
		add_action( 'do_feed_rdf', array( __CLASS__, 'protect_feeds' ), 1 );
		add_action( 'do_feed_rss', array( __CLASS__, 'protect_feeds' ), 1 );
		add_action( 'do_feed_rss2', array( __CLASS__, 'protect_feeds' ), 1 );
		add_action( 'do_feed_atom', array( __CLASS__, 'protect_feeds' ), 1 );

		/*
		 * User and profile hardening.
		 */
		add_action( 'user_profile_update_errors', array( __CLASS__, 'prevent_admin_username_change_to_weak_names' ), 10, 3 );

		/*
		 * Query hardening.
		 */
		add_action( 'pre_get_posts', array( __CLASS__, 'harden_public_queries' ) );
	}

	/**
	 * Define file editor lock.
	 *
	 * Best place is wp-config.php, but defining here still helps in many cases.
	 */
	public static function disable_file_editor(): void {
		if ( ! defined( 'DISALLOW_FILE_EDIT' ) ) {
			define( 'DISALLOW_FILE_EDIT', true );
		}
	}

	/**
	 * Remove unnecessary WordPress discovery links from head.
	 */
	public static function remove_unwanted_head_links(): void {
		remove_action( 'wp_head', 'wp_generator' );
		remove_action( 'wp_head', 'rsd_link' );
		remove_action( 'wp_head', 'wlwmanifest_link' );
		remove_action( 'wp_head', 'wp_shortlink_wp_head' );
		remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' );
		remove_action( 'wp_head', 'rest_output_link_wp_head' );
		remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
		remove_action( 'template_redirect', 'rest_output_link_header', 11 );
	}

	/**
	 * Send browser security headers.
	 */
	public static function send_security_headers(): void {
		if ( headers_sent() ) {
			return;
		}

		$is_ssl = is_ssl();

		header( 'X-Content-Type-Options: nosniff' );
		header( 'X-Frame-Options: SAMEORIGIN' );
		header( 'Referrer-Policy: strict-origin-when-cross-origin' );
		header( 'X-XSS-Protection: 0' );

		header( 'Permissions-Policy: accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(self), publickey-credentials-get=(self), sync-xhr=(), usb=(), xr-spatial-tracking=()' );

		/*
		 * Strict-Transport-Security should only be sent on HTTPS.
		 * Avoid enabling it on local/dev environments.
		 */
		if ( $is_ssl && function_exists( 'wp_get_environment_type' ) && 'production' === wp_get_environment_type() ) {
			header( 'Strict-Transport-Security: max-age=31536000; includeSubDomains; preload' );
		}

		/*
		 * Conservative Content Security Policy.
		 * This is intentionally soft and compatible with WordPress themes/plugins.
		 * Strong CSP often needs per-site tuning.
		 */
		$csp = array(
			"default-src 'self'",
			"base-uri 'self'",
			"form-action 'self'",
			"frame-ancestors 'self'",
			"object-src 'none'",
			"upgrade-insecure-requests",
		);

		header( 'Content-Security-Policy: ' . implode( '; ', $csp ) );
	}

	/**
	 * Remove wp version query from theme/plugin assets.
	 */
	public static function remove_wp_version_from_assets( string $src ): string {
		if ( strpos( $src, 'ver=' . get_bloginfo( 'version' ) ) !== false ) {
			$src = remove_query_arg( 'ver', $src );
		}

		return $src;
	}

	/**
	 * Disable XML-RPC methods.
	 */
	public static function disable_xmlrpc_methods( array $methods ): array {
		$blocked = array(
			'pingback.ping',
			'pingback.extensions.getPingbacks',
			'wp.getUsersBlogs',
			'wp.getUsers',
			'wp.getProfile',
			'wp.editProfile',
			'wp.getAuthors',
			'wp.getTags',
			'wp.getCategories',
			'metaWeblog.getUsersBlogs',
			'blogger.getUsersBlogs',
		);

		foreach ( $blocked as $method ) {
			if ( isset( $methods[ $method ] ) ) {
				unset( $methods[ $method ] );
			}
		}

		return $methods;
	}

	/**
	 * Remove X-Pingback header.
	 */
	public static function remove_x_pingback_header( array $headers ): array {
		if ( isset( $headers['X-Pingback'] ) ) {
			unset( $headers['X-Pingback'] );
		}

		return $headers;
	}

	/**
	 * Restrict sensitive REST API routes for logged-out users.
	 */
	public static function restrict_sensitive_rest_routes( $result ) {
		if ( ! empty( $result ) ) {
			return $result;
		}

		if ( is_user_logged_in() ) {
			return $result;
		}

		$request_uri = isset( $_SERVER['REQUEST_URI'] )
			? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
			: '';

		$blocked_patterns = array(
			'/wp/v2/users',
			'/wp/v2/settings',
			'/wp/v2/plugins',
			'/wp/v2/themes',
			'/wp/v2/search',
		);

		foreach ( $blocked_patterns as $pattern ) {
			if ( false !== strpos( $request_uri, $pattern ) ) {
				return new WP_Error(
					'rx_rest_forbidden',
					__( 'REST API access to this route is restricted.', 'rx-theme' ),
					array( 'status' => 401 )
				);
			}
		}

		return $result;
	}

	/**
	 * Remove public user REST routes.
	 */
	public static function remove_public_user_rest_routes( array $endpoints ): array {
		if ( ! is_user_logged_in() ) {
			if ( isset( $endpoints['/wp/v2/users'] ) ) {
				unset( $endpoints['/wp/v2/users'] );
			}

			if ( isset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] ) ) {
				unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
			}
		}

		return $endpoints;
	}

	/**
	 * Replace detailed login errors.
	 */
	public static function generic_login_error(): string {
		return __( 'Invalid login details. Please try again.', 'rx-theme' );
	}

	/**
	 * Login logo URL.
	 */
	public static function login_logo_url(): string {
		return home_url( '/' );
	}

	/**
	 * Login logo title.
	 */
	public static function login_logo_title(): string {
		return get_bloginfo( 'name' );
	}

	/**
	 * Block author enumeration like /?author=1.
	 */
	public static function block_author_enumeration(): void {
		if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}

		if ( isset( $_GET['author'] ) ) {
			wp_safe_redirect( home_url( '/' ), 301 );
			exit;
		}
	}

	/**
	 * Stop canonical redirects that expose author archive URLs.
	 */
	public static function stop_author_query_redirect( $redirect_url, $requested_url ) {
		if ( isset( $_GET['author'] ) ) {
			return false;
		}

		return $redirect_url;
	}

	/**
	 * Restrict risky upload MIME types.
	 */
	public static function restrict_upload_mimes( array $mimes ): array {
		$blocked = array(
			'exe',
			'dll',
			'bat',
			'cmd',
			'com',
			'cpl',
			'msi',
			'scr',
			'vbs',
			'js',
			'jar',
			'php',
			'php3',
			'php4',
			'php5',
			'phtml',
			'phar',
			'pl',
			'py',
			'rb',
			'sh',
			'cgi',
			'htaccess',
			'html',
			'htm',
			'svg',
			'svgz',
		);

		foreach ( $blocked as $ext ) {
			if ( isset( $mimes[ $ext ] ) ) {
				unset( $mimes[ $ext ] );
			}
		}

		return $mimes;
	}

	/**
	 * Block dangerous uploads by extension and filename.
	 */
	public static function block_dangerous_upload_files( array $file ): array {
		if ( empty( $file['name'] ) ) {
			return $file;
		}

		$filename = strtolower( sanitize_file_name( $file['name'] ) );
		$ext      = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );

		$dangerous_extensions = array(
			'php',
			'php3',
			'php4',
			'php5',
			'phtml',
			'phar',
			'exe',
			'dll',
			'bat',
			'cmd',
			'com',
			'cpl',
			'msi',
			'scr',
			'vbs',
			'js',
			'jar',
			'pl',
			'py',
			'rb',
			'sh',
			'cgi',
			'htaccess',
			'html',
			'htm',
			'svg',
			'svgz',
		);

		if ( in_array( $ext, $dangerous_extensions, true ) ) {
			$file['error'] = __( 'This file type is not allowed for security reasons.', 'rx-theme' );
			return $file;
		}

		/*
		 * Block double-extension tricks like image.jpg.php.
		 */
		foreach ( $dangerous_extensions as $dangerous_ext ) {
			if ( preg_match( '/\.' . preg_quote( $dangerous_ext, '/' ) . '\./i', $filename ) ) {
				$file['error'] = __( 'Suspicious file name blocked for security reasons.', 'rx-theme' );
				return $file;
			}
		}

		return $file;
	}

	/**
	 * Extra filename sanitization.
	 */
	public static function extra_sanitize_file_name( string $filename ): string {
		$filename = remove_accents( $filename );
		$filename = preg_replace( '/[^A-Za-z0-9\.\-\_]/', '-', $filename );
		$filename = preg_replace( '/-+/', '-', $filename );
		$filename = trim( $filename, '.-_' );

		return strtolower( $filename );
	}

	/**
	 * Disable self-pingbacks.
	 */
	public static function disable_self_pingbacks( array &$links ): void {
		$home = home_url();

		foreach ( $links as $key => $link ) {
			if ( 0 === strpos( $link, $home ) ) {
				unset( $links[ $key ] );
			}
		}
	}

	/**
	 * Disable unneeded oEmbed discovery and scripts.
	 */
	public static function disable_unneeded_embeds(): void {
		remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
		remove_action( 'wp_head', 'wp_oembed_add_host_js' );
		remove_action( 'rest_api_init', 'wp_oembed_register_route' );

		add_filter( 'embed_oembed_discover', '__return_false' );
		add_filter( 'tiny_mce_plugins', array( __CLASS__, 'disable_embed_tinymce_plugin' ) );
		add_filter( 'rewrite_rules_array', array( __CLASS__, 'disable_embed_rewrite_rules' ) );
	}

	/**
	 * Remove wpembed TinyMCE plugin.
	 */
	public static function disable_embed_tinymce_plugin( array $plugins ): array {
		return array_diff( $plugins, array( 'wpembed' ) );
	}

	/**
	 * Remove embed rewrite rules.
	 */
	public static function disable_embed_rewrite_rules( array $rules ): array {
		foreach ( $rules as $rule => $rewrite ) {
			if ( false !== strpos( $rewrite, 'embed=true' ) ) {
				unset( $rules[ $rule ] );
			}
		}

		return $rules;
	}

	/**
	 * Admin security checks.
	 */
	public static function admin_security_checks(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		/*
		 * Optional place for admin-only notices/checks.
		 * Kept silent by default to avoid dashboard noise.
		 */
	}

	/**
	 * Clean admin footer text.
	 */
	public static function clean_admin_footer_text(): string {
		return esc_html__( 'RX Theme secured and optimized.', 'rx-theme' );
	}

	/**
	 * Hide exact WordPress version from admin footer.
	 */
	public static function hide_admin_version_footer(): string {
		return '';
	}

	/**
	 * Protect feeds from exposing generator/version.
	 *
	 * This does not disable feeds fully because many SEO/news sites need feeds.
	 */
	public static function protect_feeds(): void {
		add_filter( 'the_generator', '__return_empty_string' );
	}

	/**
	 * Prevent weak administrator usernames during profile update.
	 */
	public static function prevent_admin_username_change_to_weak_names( WP_Error $errors, bool $update, WP_User $user ): void {
		if ( empty( $user->user_login ) ) {
			return;
		}

		$weak_names = array(
			'admin',
			'administrator',
			'root',
			'test',
			'user',
			'manager',
			'owner',
			'webmaster',
		);

		if ( in_array( strtolower( $user->user_login ), $weak_names, true ) ) {
			$errors->add(
				'rx_weak_username',
				__( 'For security, please avoid weak administrator usernames such as admin, administrator, root, or test.', 'rx-theme' )
			);
		}
	}

	/**
	 * Harden public queries.
	 */
	public static function harden_public_queries( WP_Query $query ): void {
		if ( is_admin() || ! $query->is_main_query() ) {
			return;
		}

		/*
		 * Prevent public author query enumeration.
		 */
		if ( $query->is_author() && isset( $_GET['author'] ) ) {
			$query->set_404();
			status_header( 404 );
		}
	}
}

RX_Theme_Security_Hardening::init();

endif;

Now add this in your functions.php:

/**
 * Security hardening.
 */
$rx_security_hardening = get_template_directory() . '/inc/security/hardening.php';

if ( file_exists( $rx_security_hardening ) ) {
	require_once $rx_security_hardening;
}

Recommended folder location:

rx-theme/
├── functions.php
└── inc/
    └── security/
        └── hardening.php

Important note: the Content-Security-Policy line may block some external scripts if your site uses many CDN scripts, ads, analytics, or third-party widgets. If anything breaks, replace this line:

header( 'Content-Security-Policy: ' . implode( '; ', $csp ) );

with this safer testing version:

header( 'Content-Security-Policy-Report-Only: ' . implode( '; ', $csp ) );

Leave a Reply

Your email address will not be published. Required fields are marked *