PluginProbe
Patchstack – WordPress & Plugins Security / 2.2.13
Patchstack – WordPress & Plugins Security v2.2.13
2.3.7 trunk 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.17 2.1.18 2.1.19 2.1.2 2.1.20 2.1.21 2.1.22 2.1.23 2.1.24 2.1.25 2.1.3 2.1.4 2.1.5 2.1.6 All 49 releases
patchstack / includes / hardening.php

hardening.php in Patchstack – WordPress & Plugins Security 2.2.13, at includes/hardening.php

431 lines 13.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Do not allow the file to be called directly.
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * This class is used to provide several hardening options.
10 */
11 class P_Hardening extends P_Core {
12
13 /**
14 * Add the actions required for the hardening of the site.
15 *
16 * @param Patchstack $core
17 * @return void
18 */
19 public function __construct( $core ) {
20 parent::__construct( $core );
21
22 // Auto update plugins.
23 add_action( 'patchstack_update_plugins', [ $this, 'update_vulnerable_plugins' ] );
24
25 // The hardening features can only be used on an activated license.
26 if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
27 return;
28 }
29
30 // Disallowed modification of the theme files?
31 if ( ! defined( 'DISALLOW_FILE_EDIT' ) && $this->get_option( 'patchstack_pluginedit', true ) ) {
32 define( 'DISALLOW_FILE_EDIT', 1 );
33 }
34
35 // Set security headers
36 add_filter( 'wp_headers', [ $this, 'set_security_headers' ], 10, 1 );
37
38 // When country blocking is set.
39 if ( $this->get_option( 'patchstack_geo_block_enabled', false ) && ! empty( $this->get_option( 'patchstack_geo_block_countries', [] ) ) ) {
40 add_action( 'init', array( $this, 'geo_block_check' ), ~PHP_INT_MAX );
41 }
42
43 // Apply comment captcha?
44 if ( $this->get_option( 'patchstack_captcha_on_comments', 0 ) && ! is_user_logged_in() ) {
45 add_action( 'comment_form_after_fields', [ $this, 'captcha_display' ] );
46 add_filter( 'preprocess_comment', [ $this, 'verify_recaptcha' ] );
47 }
48
49 // Disable the application passwords feature?
50 if ( $this->get_option( 'patchstack_application_passwords_disabled', false ) == true ) {
51 add_filter( 'wp_is_application_passwords_available', '__return_false' );
52 }
53
54 // Block unauthorized XML-RPC requests?
55 if ( $this->get_option( 'patchstack_xmlrpc_is_disabled', false ) == true ) {
56 add_filter( 'xmlrpc_enabled', '__return_false' );
57 }
58
59 // Block unauthorized wp-json requests?
60 if ( $this->get_option( 'patchstack_json_is_disabled', false ) ) {
61 add_filter( 'rest_authentication_errors', [ $this, 'disable_wpjson' ] );
62 }
63
64 // Prevent user enumeration?
65 if ( $this->get_option( 'patchstack_userenum' ) ) {
66 add_action( 'init', [ $this, 'stop_user_enum' ], 1 );
67 }
68
69 // Attempt to hide the WordPress version?
70 if ( $this->get_option( 'patchstack_hidewpversion' ) ) {
71 remove_action( 'wp_head', 'wp_generator' );
72 add_filter( 'the_generator', [ $this, 'remove_generator' ] );
73 }
74
75 // Block email registration patterns?
76 if ( $this->get_option( 'patchstack_register_email_blacklist', '' ) != '' ) {
77 add_filter( 'registration_errors', [ $this, 'check_email_pattern' ], 1, 3 );
78 add_filter( 'wpmu_validate_user_signup', [ $this, 'check_email_pattern_wpmu' ], 1, 1 );
79 }
80
81 // Auto update software?
82 $update = get_site_option( 'patchstack_auto_update', [] );
83 if ( is_array( $update ) ) {
84 foreach ( $update as $type ) {
85 if ( $type != 'vulnerable' ) {
86 add_filter( 'auto_update_' . $type, '__return_true' );
87 }
88 }
89 }
90 }
91
92 /**
93 * Perform updates if the software upload call returns vulnerabilities.
94 * This is only executed when auto updates are enabled for vulnerable plugins.
95 *
96 * @param array $plugins
97 * @return void
98 */
99 public function update_vulnerable_plugins() {
100 // Is the auto update setting for vulnerable plugins enabled?
101 $update = get_site_option( 'patchstack_auto_update', [] );
102 if ( ! is_array( $update ) || ! in_array( 'vulnerable', $update ) ) {
103 return;
104 }
105
106 // Do we even have any vulnerable plugins to auto update?
107 $plugins = get_site_option( 'patchstack_vulnerable_plugins', [] );
108 if ( ! is_array( $plugins ) || count( $plugins ) == 0 ) {
109 return;
110 }
111
112 // Might not be necessary, but should prevent any hanging issues.
113 @set_time_limit( 180 );
114
115 // Require some files we need to execute the upgrade.
116 @include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
117 if ( file_exists( ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php' ) ) {
118 @include_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
119 }
120
121 @include_once ABSPATH . 'wp-admin/includes/plugin.php';
122 @include_once ABSPATH . 'wp-admin/includes/misc.php';
123 @include_once ABSPATH . 'wp-admin/includes/file.php';
124 @wp_update_plugins();
125 $all_plugins = get_plugins();
126
127 // New array with all available plugins and the ones we want to upgrade.
128 $upgrade = [];
129 foreach ( $all_plugins as $path => $data ) {
130 if ( in_array( $path, $plugins ) ) {
131 array_push( $upgrade, $path );
132 }
133 }
134
135 // Upgrade the plugins.
136 $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
137 $upgrader->bulk_upgrade( $upgrade );
138
139 // Reset the option that holds the vulnerable plugins.
140 update_site_option( 'patchstack_vulnerable_plugins', [] );
141
142 // Resend the sofware data to the API.
143 do_action( 'patchstack_send_software_data' );
144 }
145
146 /**
147 * Determine the country of the user and if we should block the user.
148 *
149 * @return void
150 */
151 public function geo_block_check() {
152 $countries = $this->get_option( 'patchstack_geo_block_countries', [] );
153 $ip = $this->get_ip();
154
155 // Don't block Patchstack.
156 if ( in_array( $ip, $this->ips ) || ( isset( $_POST['webarx_secret'] ) && $this->plugin->listener->verifyToken( $_POST['webarx_secret'] ) ) || isset( $_POST['patchstack_ott_action'] )) {
157
158 // OTT action.
159 if ( isset( $_POST['patchstack_ott_action'] ) ) {
160 $ott = get_option( 'patchstack_ott_action', '' );
161 if ( ! empty( $ott ) && hash_equals( $ott, $_POST['patchstack_ott_action'] ) ) {
162 return;
163 }
164 } else {
165 return;
166 }
167 }
168
169 // Load the required libraries.
170 try {
171 require_once __DIR__ . '/../lib/geoip2-php/autoload.php';
172 $reader = new GeoIp2\Database\Reader( __DIR__ . '/../lib/GeoLite2-Country.mmdb' );
173 $record = $reader->country( $ip );
174
175 // Determine if we want to do an inverse check or not.
176 $match = in_array( $record->country->isoCode, $countries );
177 $match = $this->get_option( 'patchstack_geo_block_inverse', false ) ? ! $match : $match;
178
179 // Check if there's a match.
180 if ( $match ) {
181 $this->plugin->firewall_base->display_error_page( 23 );
182 }
183 } catch ( \Exception $e ) {
184 }
185 }
186
187 /**
188 * Prevent unauthorized users from accessing wp-json.
189 *
190 * @return void|WP_Error
191 */
192 public function disable_wpjson() {
193 // Some default exceptions.
194 $path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
195 $whitelists = [ '/wp-json/contact-form-7/' ];
196 foreach ( $whitelists as $whitelist ) {
197 if ( stripos( $path, $whitelist ) !== false ) {
198 return;
199 }
200 }
201
202 // Block unauthorized users.
203 if ( ! is_user_logged_in() ) {
204 $msg = apply_filters( 'disable_wp_rest_api_error', esc_attr__( 'The WP REST API cannot be accessed by unauthorized users.', 'disable-wp-rest-api' ) );
205 return new WP_Error( 'rest_authorization_required', $msg, [ 'status' => rest_authorization_required_code() ] );
206 }
207 }
208
209 /**
210 * Set security headers if the option is enabled.
211 *
212 * @param array $headers
213 * @return void|array
214 */
215 public function set_security_headers( $headers ) {
216 if ( get_option( 'patchstack_add_security_headers' ) ) {
217 $headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
218 $headers['X-Frame-Options'] = 'SAMEORIGIN';
219 $headers['X-XSS-Protection'] = '1; mode=block';
220 $headers['X-Content-Type-Options'] = 'nosniff';
221 $headers['X-Powered-By'] = null;
222 $headers['Server'] = null;
223 $headers['Strict-Transport-Security'] = 'max-age=31536000';
224 }
225
226 return $headers;
227 }
228
229 /**
230 * Determine if the reCAPTCHA is valid upon comment submission.
231 *
232 * @param array $comment_data
233 * @return void|array
234 */
235 public function verify_recaptcha( $comment_data ) {
236 $result = $this->captcha_check();
237 if ( ! $result['response'] && ( $result['reason'] === 'VERIFICATION_FAILED' || $result['reason'] === 'RECAPTCHA_EMPTY_RESPONSE' ) ) {
238 wp_clear_auth_cookie();
239 wp_die( 'reCaptcha was not solved or response was empty', 'Error' );
240 }
241
242 return $comment_data;
243 }
244
245 /**
246 * Add the captcha to the comments form.
247 *
248 * @return void
249 */
250 public function captcha_display() {
251 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
252 case 'v2':
253 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
254 require_once dirname( __FILE__ ) . '/views/captcha_v2.php';
255 break;
256 case 'invisible':
257 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
258 require_once dirname( __FILE__ ) . '/views/captcha_invisible.php';
259 break;
260 case 'v3':
261 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
262 require_once dirname( __FILE__ ) . '/views/captcha_v3.php';
263 break;
264 case 'turnstile':
265 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_turnstile' ) );
266 require_once dirname( __FILE__ ) . '/views/captcha_turnstile.php';
267 break;
268 }
269 }
270
271 /**
272 * Check if the submitted reCAPTCHA is valid.
273 *
274 * @return array
275 */
276 public function captcha_check() {
277 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
278 case 'v2':
279 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key' ) );
280 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
281 break;
282 case 'invisible':
283 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3' ) );
284 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
285 break;
286 case 'v3':
287 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3_new' ) );
288 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
289 break;
290 case 'turnstile':
291 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_turnstile' ) );
292 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_turnstile' ) );
293 break;
294 }
295
296 if ( ! $secret_key || ! $site_key ) {
297 return [
298 'response' => false,
299 'reason' => 'ERROR_NO_KEYS',
300 ];
301 }
302
303 if ( ! isset( $_POST['g-recaptcha-response'] ) || empty( $_POST['g-recaptcha-response'] ) ) {
304 return [
305 'response' => false,
306 'reason' => 'RECAPTCHA_EMPTY_RESPONSE',
307 ];
308 }
309
310 $response = $this->get_captcha_response( $secret_key, $this->get_option( 'patchstack_captcha_type' ) );
311 if ( isset( $response['success'] ) && ! empty( $response['success'] ) ) {
312 return [
313 'response' => true,
314 'reason' => '',
315 ];
316 }
317
318 return [
319 'response' => false,
320 'reason' => 'VERIFICATION_FAILED',
321 ];
322 }
323
324 /**
325 * Query Google for reAPTCHA validation and response.
326 *
327 * @param string $privatekey
328 * @param string $type
329 * @return array
330 */
331 public function get_captcha_response( $privatekey, $type ) {
332 $args = [
333 'body' => [
334 'secret' => $privatekey,
335 'response' => $_POST['g-recaptcha-response'],
336 ],
337 'sslverify' => false,
338 ];
339
340 if ($type != 'turnstile') {
341 $resp = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', $args );
342 } else {
343 $resp = wp_remote_post( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', $args );
344 }
345
346 return json_decode( wp_remote_retrieve_body( $resp ), true );
347 }
348
349 /**
350 * Delete the readme.html file.
351 *
352 * @return void
353 */
354 public function delete_readme() {
355 if ( get_site_option( 'patchstack_rm_readme', false ) != true || ! file_exists( ABSPATH . 'readme.html' ) ) {
356 return;
357 }
358
359 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
360 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
361 $fs = new WP_Filesystem_Direct( '' );
362 $fs->delete( ABSPATH . 'readme.html' );
363 }
364
365 /**
366 * Disable user enumeration with ?author= and the REST endpoint.
367 *
368 * @return void
369 */
370 public function stop_user_enum() {
371 if ( isset( $_GET['author'] ) && ! is_user_logged_in() && ! is_admin() ) {
372 die( wp_safe_redirect( get_site_url() ) );
373 }
374
375 if ( stripos( $_SERVER['REQUEST_URI'], 'v2/users' ) !== false || ( isset( $_REQUEST['rest_route'] ) && stripos( $_REQUEST['rest_route'], 'v2/users' ) !== false ) ) {
376 if ( ! is_user_logged_in() ) {
377 die( wp_safe_redirect( get_site_url() ) );
378 }
379 }
380 }
381
382 /**
383 * Hide the WordPress generator version in response.
384 *
385 * @return string
386 */
387 public function remove_generator() {
388 return '';
389 }
390
391 /**
392 * Determine if the email address of a new registration matches the defined patterns.
393 * This filter is called on regular sites.
394 *
395 * @param object $errors
396 * @param string $sanitized_user_login
397 * @param string $user_email
398 * @return object
399 */
400 public function check_email_pattern( $errors, $sanitized_user_login, $user_email ) {
401 $patterns = explode( ',', $this->get_option( 'patchstack_register_email_blacklist' ) );
402 foreach ( $patterns as $pattern ) {
403 if ( stripos( $user_email, $pattern ) !== false ) {
404 $errors->add( 'user_email', esc_attr__( 'An invalid email address has been supplied.', 'patchstack' ) );
405 }
406 }
407
408 return $errors;
409 }
410
411 /**
412 * Determine if the email address of a new registration matches the defined patterns.
413 * This filter is called on network sites.
414 *
415 * @param array $result
416 * @return array
417 */
418 public function check_email_pattern_wpmu( $result ) {
419 if ( isset( $result['user_email'] ) ) {
420 $patterns = explode( ',', $this->get_option( 'patchstack_register_email_blacklist' ) );
421 foreach ( $patterns as $pattern ) {
422 if ( stripos( $result['user_email'], $pattern ) !== false ) {
423 $result['errors']->add( 'user_email', esc_attr__( 'An invalid email address has been supplied.', 'patchstack' ) );
424 }
425 }
426 }
427
428 return $result;
429 }
430 }
431