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 ) );