| 1 |
<?php |
| 2 |
namespace WPEXtra\Modules\Common; |
| 3 |
|
| 4 |
if ( ! defined( 'ABSPATH' ) ) { |
| 5 |
exit; |
| 6 |
} |
| 7 |
|
| 8 |
use WPEXtra\Settings; |
| 9 |
use WPEXtra\Helper; |
| 10 |
use WPEXtra\Base; |
| 11 |
|
| 12 |
class Security extends Base { |
| 13 |
|
| 14 |
public function __construct() { |
| 15 |
parent::__construct(); |
| 16 |
} |
| 17 |
|
| 18 |
protected $features = [ |
| 19 |
'disable_embeds', |
| 20 |
'disable_xmlrpc', |
| 21 |
'remove_jquery_migrate', |
| 22 |
'remove_wp_version', |
| 23 |
'clean_head_links', |
| 24 |
'remove_wlwmanifest_link', |
| 25 |
'remove_rsd_link', |
| 26 |
'remove_shortlink', |
| 27 |
'disable_rss_feeds', |
| 28 |
'disable_self_pingbacks', |
| 29 |
'block_user_enumeration', |
| 30 |
'security_headers', |
| 31 |
'themeplugin_edits', |
| 32 |
'core_updates', |
| 33 |
'http_request', |
| 34 |
'disable_rest_api', |
| 35 |
'remove_rest_api_links', |
| 36 |
'disable_heartbeat', |
| 37 |
'heartbeat_frequency', |
| 38 |
'remove_blocks', |
| 39 |
]; |
| 40 |
|
| 41 |
public function clean_head_links() { |
| 42 |
$this->remove_wlwmanifest_link(); |
| 43 |
$this->remove_rsd_link(); |
| 44 |
$this->remove_shortlink(); |
| 45 |
$this->disable_self_pingbacks(); |
| 46 |
} |
| 47 |
|
| 48 |
public function disable_embeds() { |
| 49 |
add_action('init', [$this, 'disable_embed'], 9999); |
| 50 |
} |
| 51 |
|
| 52 |
public function disable_embed() { |
| 53 |
global $wp; |
| 54 |
if (isset($wp->public_query_vars) && is_array($wp->public_query_vars)) { |
| 55 |
$wp->public_query_vars = array_diff($wp->public_query_vars, ['embed']); |
| 56 |
} |
| 57 |
add_filter('embed_oembed_discover', '__return_false'); |
| 58 |
remove_action('wp_head', 'wp_oembed_add_discovery_links'); |
| 59 |
remove_action('wp_head', 'wp_oembed_add_host_js'); |
| 60 |
remove_filter('oembed_dataparse', 'wp_filter_oembed_result', 10); |
| 61 |
remove_filter('pre_oembed_result', 'wp_filter_pre_oembed_result', 10); |
| 62 |
add_filter('tiny_mce_plugins', [$this, 'disableEmbedsTinyMCE']); |
| 63 |
add_filter('rewrite_rules_array', [$this, 'disableEmbedsRewrites']); |
| 64 |
} |
| 65 |
|
| 66 |
public function disableEmbedsTinyMCE($plugins) { |
| 67 |
return is_array($plugins) ? array_diff($plugins, ['wpembed']) : $plugins; |
| 68 |
} |
| 69 |
|
| 70 |
public function disableEmbedsRewrites($rules) { |
| 71 |
if (is_array($rules)) { |
| 72 |
foreach ($rules as $rule => $rewrite) { |
| 73 |
if (false !== strpos($rewrite, 'embed=true')) { |
| 74 |
unset($rules[$rule]); |
| 75 |
} |
| 76 |
} |
| 77 |
} |
| 78 |
return $rules; |
| 79 |
} |
| 80 |
|
| 81 |
public function disable_xmlrpc() { |
| 82 |
add_filter('xmlrpc_enabled', '__return_false'); |
| 83 |
add_filter('pings_open', '__return_false', 9999); |
| 84 |
add_filter('pre_update_option_enable_xmlrpc', '__return_false'); |
| 85 |
add_filter('pre_option_enable_xmlrpc', '__return_zero'); |
| 86 |
add_filter('wp_headers', [$this, 'remove_xpingback']); |
| 87 |
add_action('init', [$this, 'intercept_xmlrpc_header']); |
| 88 |
} |
| 89 |
|
| 90 |
public function remove_xpingback($headers) { |
| 91 |
unset($headers['X-Pingback'], $headers['x-pingback']); |
| 92 |
return $headers; |
| 93 |
} |
| 94 |
|
| 95 |
public function intercept_xmlrpc_header() { |
| 96 |
if (!isset($_SERVER['SCRIPT_FILENAME'])) { |
| 97 |
return; |
| 98 |
} |
| 99 |
if ('xmlrpc.php' !== basename($_SERVER['SCRIPT_FILENAME'])) { |
| 100 |
return; |
| 101 |
} |
| 102 |
$header = 'HTTP/1.1 403 Forbidden'; |
| 103 |
header($header); |
| 104 |
echo esc_html($header); |
| 105 |
exit; |
| 106 |
} |
| 107 |
|
| 108 |
public function remove_jquery_migrate() { |
| 109 |
add_filter('wp_default_scripts', [$this, 'jquery_migrate']); |
| 110 |
} |
| 111 |
|
| 112 |
public function jquery_migrate(&$scripts) { |
| 113 |
if (!is_admin()) { |
| 114 |
$scripts->remove('jquery'); |
| 115 |
$scripts->add('jquery', false, ['jquery-core']); |
| 116 |
} |
| 117 |
} |
| 118 |
|
| 119 |
public function remove_wp_version() { |
| 120 |
remove_action('wp_head', 'wp_generator'); |
| 121 |
add_filter('the_generator', '__return_empty_string'); |
| 122 |
} |
| 123 |
|
| 124 |
public function remove_wlwmanifest_link() { |
| 125 |
remove_action('wp_head', 'wlwmanifest_link'); |
| 126 |
} |
| 127 |
|
| 128 |
public function remove_rsd_link() { |
| 129 |
remove_action('wp_head', 'rsd_link'); |
| 130 |
} |
| 131 |
|
| 132 |
public function remove_shortlink() { |
| 133 |
remove_action('wp_head', 'wp_shortlink_wp_head'); |
| 134 |
remove_action('template_redirect', 'wp_shortlink_header', 11, 0); |
| 135 |
} |
| 136 |
|
| 137 |
public function disable_rss_feeds() { |
| 138 |
add_action('template_redirect', [$this, 'rss_feed'], 1); |
| 139 |
remove_action('wp_head', 'feed_links', 2); |
| 140 |
remove_action('wp_head', 'feed_links_extra', 3); |
| 141 |
} |
| 142 |
|
| 143 |
public function rss_feed() { |
| 144 |
if (!is_feed() || is_404()) { |
| 145 |
return; |
| 146 |
} |
| 147 |
if (isset($_GET['feed'])) { |
| 148 |
wp_safe_redirect(esc_url_raw(remove_query_arg('feed')), 301); |
| 149 |
exit; |
| 150 |
} |
| 151 |
if (get_query_var('feed') !== 'old') { |
| 152 |
set_query_var('feed', ''); |
| 153 |
} |
| 154 |
redirect_canonical(); |
| 155 |
wp_die(sprintf(esc_html__("No feed available, please visit the <a href='%s'>homepage</a>!", 'wp-extra'), esc_url(home_url('/')))); |
| 156 |
} |
| 157 |
|
| 158 |
public function disable_self_pingbacks() { |
| 159 |
add_action('pre_ping', [$this, 'self_pingbacks']); |
| 160 |
} |
| 161 |
|
| 162 |
public function self_pingbacks(&$links) { |
| 163 |
$home = get_option('home'); |
| 164 |
foreach ($links as $l => $link) { |
| 165 |
if (strpos($link, $home) === 0) { |
| 166 |
unset($links[$l]); |
| 167 |
} |
| 168 |
} |
| 169 |
} |
| 170 |
|
| 171 |
public function block_user_enumeration() { |
| 172 |
if (!is_admin()) { |
| 173 |
// Block query string ?author=N for non-logged-in visitors |
| 174 |
if (isset($_REQUEST['author']) && (is_numeric($_REQUEST['author']) || '' !== $_REQUEST['author'])) { |
| 175 |
wp_safe_redirect(home_url(), 301); |
| 176 |
exit; |
| 177 |
} |
| 178 |
// Hide REST API user list from unauthorized visitors |
| 179 |
add_filter('rest_endpoints', [$this, 'filter_rest_user_endpoints']); |
| 180 |
} |
| 181 |
} |
| 182 |
|
| 183 |
public function filter_rest_user_endpoints($endpoints) { |
| 184 |
if (!is_user_logged_in()) { |
| 185 |
if (isset($endpoints['/wp/v2/users'])) { |
| 186 |
unset($endpoints['/wp/v2/users']); |
| 187 |
} |
| 188 |
if (isset($endpoints['/wp/v2/users/(?P<id>[\d]+)'])) { |
| 189 |
unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']); |
| 190 |
} |
| 191 |
} |
| 192 |
return $endpoints; |
| 193 |
} |
| 194 |
|
| 195 |
public function security_headers() { |
| 196 |
add_filter('wp_headers', [$this, 'send_security_headers']); |
| 197 |
} |
| 198 |
|
| 199 |
public function send_security_headers($headers) { |
| 200 |
if (!is_admin()) { |
| 201 |
$headers['X-Frame-Options'] = 'SAMEORIGIN'; |
| 202 |
$headers['X-Content-Type-Options'] = 'nosniff'; |
| 203 |
$headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'; |
| 204 |
$headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=()'; |
| 205 |
} |
| 206 |
return $headers; |
| 207 |
} |
| 208 |
|
| 209 |
|
| 210 |
public function themeplugin_edits() { |
| 211 |
add_filter('map_meta_cap', [$this, 'disable_file_editor_caps'], 10, 2); |
| 212 |
if (!defined('DISALLOW_FILE_EDIT')) { |
| 213 |
define('DISALLOW_FILE_EDIT', true); |
| 214 |
} |
| 215 |
} |
| 216 |
|
| 217 |
public function disable_file_editor_caps($caps, $cap) { |
| 218 |
if (in_array($cap, ['edit_themes', 'edit_plugins', 'edit_files'], true)) { |
| 219 |
$caps[] = 'do_not_allow'; |
| 220 |
} |
| 221 |
return $caps; |
| 222 |
} |
| 223 |
|
| 224 |
public function core_updates() { |
| 225 |
add_filter('auto_update_core', '__return_false'); |
| 226 |
add_filter('automatic_updater_disabled', '__return_true'); |
| 227 |
} |
| 228 |
|
| 229 |
public function http_request() { |
| 230 |
add_filter('pre_http_request', [$this, 'pass_reject_request'], 10, 3); |
| 231 |
} |
| 232 |
|
| 233 |
public function pass_reject_request($preempt, $parsed_args, $url) { |
| 234 |
$value = Helper::get_option('http_request', ''); |
| 235 |
$blocked_domains = array_filter(array_map('trim', explode("\n", $value))); |
| 236 |
foreach ($blocked_domains as $domain) { |
| 237 |
if (strpos($url, $domain) !== false) { |
| 238 |
return new \WP_Error('http_request_block', esc_html__('Blocked by WP EXtra', 'wp-extra')); |
| 239 |
} |
| 240 |
} |
| 241 |
return $preempt; |
| 242 |
} |
| 243 |
|
| 244 |
public function disable_rest_api() { |
| 245 |
add_filter('rest_authentication_errors', [$this, 'restAuthenticationErrors'], 20); |
| 246 |
} |
| 247 |
|
| 248 |
public function restAuthenticationErrors($result) { |
| 249 |
if (!empty($result)) { |
| 250 |
return $result; |
| 251 |
} |
| 252 |
|
| 253 |
$rest_route = isset($GLOBALS['wp']->query_vars['rest_route']) ? $GLOBALS['wp']->query_vars['rest_route'] : ''; |
| 254 |
$exceptions = apply_filters('wpex_rest_api_exceptions', [ |
| 255 |
'contact-form-7', |
| 256 |
'wordfence', |
| 257 |
'elementor', |
| 258 |
'woocommerce', |
| 259 |
'fluentform', |
| 260 |
'wpforms' |
| 261 |
]); |
| 262 |
|
| 263 |
foreach ($exceptions as $exception) { |
| 264 |
if (!empty($rest_route) && strpos($rest_route, $exception) !== false) { |
| 265 |
return $result; |
| 266 |
} |
| 267 |
} |
| 268 |
|
| 269 |
$disabled = false; |
| 270 |
$disableOption = Helper::get_option('disable_rest_api', ''); |
| 271 |
$disableOptions = is_array($disableOption) ? $disableOption : [$disableOption]; |
| 272 |
|
| 273 |
if (in_array('non_admins', $disableOptions, true) && !current_user_can('manage_options')) { |
| 274 |
$disabled = true; |
| 275 |
} elseif (in_array('logged_out', $disableOptions, true) && !is_user_logged_in()) { |
| 276 |
$disabled = true; |
| 277 |
} |
| 278 |
|
| 279 |
if ($disabled) { |
| 280 |
return new \WP_Error('rest_authentication_error', __('Sorry, you do not have permission to make REST API requests.', 'wp-extra'), ['status' => 401]); |
| 281 |
} |
| 282 |
return $result; |
| 283 |
} |
| 284 |
|
| 285 |
public function remove_rest_api_links() { |
| 286 |
remove_action('xmlrpc_rsd_apis', 'rest_output_rsd'); |
| 287 |
remove_action('wp_head', 'rest_output_link_wp_head'); |
| 288 |
remove_action('template_redirect', 'rest_output_link_header', 11, 0); |
| 289 |
} |
| 290 |
|
| 291 |
public function disable_heartbeat() { |
| 292 |
add_action('init', [$this, 'disableHeartbeat'], 1); |
| 293 |
} |
| 294 |
|
| 295 |
public function disableHeartbeat() { |
| 296 |
if (is_admin()) { |
| 297 |
global $pagenow; |
| 298 |
if (!empty($pagenow)) { |
| 299 |
if ($pagenow === 'admin.php' && !empty($_GET['page'])) { |
| 300 |
$exceptions = [ |
| 301 |
'gf_edit_forms', |
| 302 |
'gf_entries', |
| 303 |
'gf_settings' |
| 304 |
]; |
| 305 |
if (in_array($_GET['page'], $exceptions, true)) { |
| 306 |
return; |
| 307 |
} |
| 308 |
} |
| 309 |
if ($pagenow === 'site-health.php') { |
| 310 |
return; |
| 311 |
} |
| 312 |
} |
| 313 |
} |
| 314 |
$setting = Helper::get_option('disable_heartbeat'); |
| 315 |
if ($setting) { |
| 316 |
if ($setting === 'everywhere') { |
| 317 |
$this->replaceHearbeat(); |
| 318 |
} elseif ($setting === 'allow_posts') { |
| 319 |
global $pagenow; |
| 320 |
if ($pagenow !== 'post.php' && $pagenow !== 'post-new.php') { |
| 321 |
$this->replaceHearbeat(); |
| 322 |
} |
| 323 |
} |
| 324 |
} |
| 325 |
} |
| 326 |
|
| 327 |
private function replaceHearbeat() { |
| 328 |
wp_deregister_script('heartbeat'); |
| 329 |
if (is_admin() && Helper::get_option('disable_heartbeat')) { |
| 330 |
wp_register_script('heartbeat', plugins_url('/assets/js/heartbeat.min.js', WPEX_FILE)); |
| 331 |
wp_enqueue_script('heartbeat', plugins_url('/assets/js/heartbeat.min.js', WPEX_FILE)); |
| 332 |
} |
| 333 |
} |
| 334 |
|
| 335 |
public function heartbeat_frequency() { |
| 336 |
add_filter('heartbeat_settings', [$this, 'heartbeatFrequency']); |
| 337 |
} |
| 338 |
|
| 339 |
public function heartbeatFrequency($settings) { |
| 340 |
$freq = Helper::get_option('heartbeat_frequency'); |
| 341 |
if ($freq) { |
| 342 |
$settings['interval'] = intval($freq); |
| 343 |
} |
| 344 |
return $settings; |
| 345 |
} |
| 346 |
|
| 347 |
public function remove_blocks() { |
| 348 |
add_filter('allowed_block_types_all', [$this, 'remove_default_blocks']); |
| 349 |
add_filter('allowed_block_types', [$this, 'remove_default_blocks']); |
| 350 |
} |
| 351 |
|
| 352 |
public function remove_default_blocks($allowed_blocks) { |
| 353 |
if (!class_exists('\WP_Block_Type_Registry')) { |
| 354 |
return $allowed_blocks; |
| 355 |
} |
| 356 |
$registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered(); |
| 357 |
$filtered_blocks = []; |
| 358 |
|
| 359 |
foreach ($registered_blocks as $block) { |
| 360 |
if (strpos($block->name, 'core/') === false) { |
| 361 |
if (!class_exists('WooCommerce') || strpos($block->name, 'woocommerce/') === false) { |
| 362 |
$filtered_blocks[] = $block->name; |
| 363 |
} |
| 364 |
} |
| 365 |
} |
| 366 |
return $filtered_blocks; |
| 367 |
} |
| 368 |
|
| 369 |
} |