disable_file_editing(); } // Remove WordPress version. if ( WP_Security_Pack::get_setting( 'remove_wp_version', true ) ) { add_filter( 'the_generator', '__return_empty_string' ); remove_action( 'wp_head', 'wp_generator' ); add_filter( 'style_loader_src', array( $this, 'remove_version_strings' ), 10, 2 ); add_filter( 'script_loader_src', array( $this, 'remove_version_strings' ), 10, 2 ); } // Add security headers. if ( WP_Security_Pack::get_setting( 'add_security_headers', true ) ) { add_action( 'send_headers', array( $this, 'add_security_headers' ) ); } // Restrict REST API. if ( WP_Security_Pack::get_setting( 'restrict_rest_api', true ) ) { add_filter( 'rest_authentication_errors', array( $this, 'restrict_rest_api' ) ); } // Disable application passwords for non-admins. if ( WP_Security_Pack::get_setting( 'disable_application_passwords', false ) ) { add_filter( 'wp_is_application_passwords_available', '__return_false' ); } // Remove unnecessary headers. add_action( 'init', array( $this, 'remove_unnecessary_headers' ) ); // Disable user enumeration. if ( WP_Security_Pack::get_setting( 'disable_user_enumeration', true ) ) { add_action( 'init', array( $this, 'block_author_scanning' ) ); add_filter( 'rest_endpoints', array( $this, 'restrict_users_endpoint' ) ); add_filter( 'oembed_response_data', array( $this, 'remove_author_from_oembed' ) ); } // Disable pingbacks/trackbacks. if ( WP_Security_Pack::get_setting( 'disable_pingbacks', true ) ) { add_filter( 'xmlrpc_methods', array( $this, 'disable_pingback_methods' ) ); add_filter( 'wp_headers', array( $this, 'remove_pingback_header' ) ); add_filter( 'pings_open', '__return_false', 20, 2 ); } } /** * Remove XML-RPC related headers. * * @param array $headers HTTP headers. * @return array */ public function remove_xmlrpc_headers( $headers ) { unset( $headers['X-Pingback'] ); return $headers; } /** * Block direct XML-RPC requests. */ public function block_xmlrpc_requests() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; if ( strpos( $request_uri, 'xmlrpc.php' ) !== false ) { status_header( 403 ); exit( 'XML-RPC is disabled.' ); } } /** * Disable file editing in dashboard. */ private function disable_file_editing() { if ( ! defined( 'DISALLOW_FILE_EDIT' ) ) { define( 'DISALLOW_FILE_EDIT', true ); } } /** * Remove version strings from scripts and styles. * * @param string $src Source URL. * @param string $handle Handle name. * @return string */ public function remove_version_strings( $src, $handle ) { if ( strpos( $src, 'ver=' ) !== false ) { $src = remove_query_arg( 'ver', $src ); } return $src; } /** * Add security headers. */ public function add_security_headers() { // Don't add headers for admin pages if user is logged in. if ( is_admin() && is_user_logged_in() ) { // Still add some basic headers. header( 'X-Content-Type-Options: nosniff' ); return; } // Get header settings. $headers = WP_Security_Pack::get_setting( 'security_headers', $this->get_default_headers() ); // X-Content-Type-Options. if ( ! empty( $headers['x_content_type_options'] ) ) { header( 'X-Content-Type-Options: ' . $headers['x_content_type_options'] ); } // X-Frame-Options. if ( ! empty( $headers['x_frame_options'] ) ) { header( 'X-Frame-Options: ' . $headers['x_frame_options'] ); } // X-XSS-Protection. if ( ! empty( $headers['x_xss_protection'] ) ) { header( 'X-XSS-Protection: ' . $headers['x_xss_protection'] ); } // Referrer-Policy. if ( ! empty( $headers['referrer_policy'] ) ) { header( 'Referrer-Policy: ' . $headers['referrer_policy'] ); } // Permissions-Policy. if ( ! empty( $headers['permissions_policy'] ) ) { header( 'Permissions-Policy: ' . $headers['permissions_policy'] ); } // Content-Security-Policy. if ( ! empty( $headers['content_security_policy'] ) ) { header( 'Content-Security-Policy: ' . $headers['content_security_policy'] ); } // Strict-Transport-Security (HSTS). if ( ! empty( $headers['strict_transport_security'] ) && is_ssl() ) { header( 'Strict-Transport-Security: ' . $headers['strict_transport_security'] ); } } /** * Get default security headers. * * @return array */ public function get_default_headers() { return array( 'x_content_type_options' => 'nosniff', 'x_frame_options' => 'SAMEORIGIN', 'x_xss_protection' => '1; mode=block', 'referrer_policy' => 'strict-origin-when-cross-origin', 'permissions_policy' => 'geolocation=(), microphone=(), camera=()', 'content_security_policy' => '', 'strict_transport_security' => 'max-age=31536000; includeSubDomains', ); } /** * Restrict REST API to authenticated users. * * @param WP_Error|null|bool $result Authentication result. * @return WP_Error|null|bool */ public function restrict_rest_api( $result ) { // If there's already an error, return it. if ( is_wp_error( $result ) ) { return $result; } // Allow if user is logged in. if ( is_user_logged_in() ) { return $result; } // Get allowed REST routes. $allowed_routes = WP_Security_Pack::get_setting( 'rest_api_allowed_routes', array() ); // Always allow some essential routes. $essential_routes = array( '/wp/v2/oembed', '/wp-site-health', ); $allowed_routes = array_merge( $allowed_routes, $essential_routes ); // Get current route. $current_route = $GLOBALS['wp']->query_vars['rest_route'] ?? ''; // Check if current route is allowed. foreach ( $allowed_routes as $route ) { if ( strpos( $current_route, $route ) === 0 ) { return $result; } } // Block unauthenticated access. return new WP_Error( 'rest_not_logged_in', __( 'You must be authenticated to access this endpoint.', 'wp-security-pack' ), array( 'status' => 401 ) ); } /** * Remove unnecessary headers. */ public function remove_unnecessary_headers() { // Remove Really Simple Discovery link. remove_action( 'wp_head', 'rsd_link' ); // Remove Windows Live Writer manifest link. remove_action( 'wp_head', 'wlwmanifest_link' ); // Remove shortlink. remove_action( 'wp_head', 'wp_shortlink_wp_head' ); // Remove feed links. if ( WP_Security_Pack::get_setting( 'remove_feed_links', false ) ) { remove_action( 'wp_head', 'feed_links', 2 ); remove_action( 'wp_head', 'feed_links_extra', 3 ); } } /** * Block author scanning via ?author=N URLs. */ public function block_author_scanning() { if ( is_admin() ) { return; } // Block ?author=N requests for non-logged-in users. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! is_user_logged_in() && isset( $_GET['author'] ) ) { wp_safe_redirect( home_url(), 301 ); exit; } } /** * Restrict REST API users endpoint to authenticated users. * * @param array $endpoints REST API endpoints. * @return array */ public function restrict_users_endpoint( $endpoints ) { if ( is_user_logged_in() ) { return $endpoints; } // Remove users endpoint for unauthenticated requests. if ( isset( $endpoints['/wp/v2/users'] ) ) { unset( $endpoints['/wp/v2/users'] ); } if ( isset( $endpoints['/wp/v2/users/(?P[\d]+)'] ) ) { unset( $endpoints['/wp/v2/users/(?P[\d]+)'] ); } return $endpoints; } /** * Remove author information from oEmbed responses. * * @param array $data oEmbed response data. * @return array */ public function remove_author_from_oembed( $data ) { if ( isset( $data['author_name'] ) ) { unset( $data['author_name'] ); } if ( isset( $data['author_url'] ) ) { unset( $data['author_url'] ); } return $data; } /** * Disable pingback XML-RPC methods. * * @param array $methods XML-RPC methods. * @return array */ public function disable_pingback_methods( $methods ) { unset( $methods['pingback.ping'] ); unset( $methods['pingback.extensions.getPingbacks'] ); return $methods; } /** * Remove X-Pingback header. * * @param array $headers HTTP headers. * @return array */ public function remove_pingback_header( $headers ) { unset( $headers['X-Pingback'] ); return $headers; } }