mirror of
https://gitlab.com/ArkHost/WP-Security-Pack.git
synced 2026-09-19 17:37:30 +02:00
332 lines
9.0 KiB
PHP
332 lines
9.0 KiB
PHP
<?php
|
|
/**
|
|
* Security hardening for Security Pack.
|
|
*
|
|
* @package ArkHost_Security_Pack
|
|
*/
|
|
|
|
// Prevent direct access.
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Hardening class for security enhancements.
|
|
*/
|
|
class ARKSP_Hardening {
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct() {
|
|
// Disable XML-RPC.
|
|
if ( ARKSP_Plugin::get_setting( 'disable_xmlrpc', true ) ) {
|
|
add_filter( 'xmlrpc_enabled', '__return_false' );
|
|
add_filter( 'wp_headers', array( $this, 'remove_xmlrpc_headers' ) );
|
|
add_action( 'wp', array( $this, 'block_xmlrpc_requests' ), 1 );
|
|
}
|
|
|
|
// Disable file editing.
|
|
if ( ARKSP_Plugin::get_setting( 'disable_file_editing', true ) ) {
|
|
$this->disable_file_editing();
|
|
}
|
|
|
|
// Remove WordPress version.
|
|
if ( ARKSP_Plugin::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 ( ARKSP_Plugin::get_setting( 'add_security_headers', true ) ) {
|
|
add_action( 'send_headers', array( $this, 'add_security_headers' ) );
|
|
}
|
|
|
|
// Restrict REST API.
|
|
if ( ARKSP_Plugin::get_setting( 'restrict_rest_api', true ) ) {
|
|
add_filter( 'rest_authentication_errors', array( $this, 'restrict_rest_api' ) );
|
|
}
|
|
|
|
// Disable application passwords for non-admins.
|
|
if ( ARKSP_Plugin::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 ( ARKSP_Plugin::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 ( ARKSP_Plugin::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 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress core constant.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 = ARKSP_Plugin::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 = ARKSP_Plugin::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.', 'arkhost-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 ( ARKSP_Plugin::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<id>[\d]+)'] ) ) {
|
|
unset( $endpoints['/wp/v2/users/(?P<id>[\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;
|
|
}
|
|
}
|