PluginProbe
Patchstack – WordPress & Plugins Security / 2.2.2
Patchstack – WordPress & Plugins Security v2.2.2
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.2, at includes/hardening.php

416 lines 13.2 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 || $this->is_community() ) {
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', __( '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_site_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 }
265 }
266
267 /**
268 * Check if the submitted reCAPTCHA is valid.
269 *
270 * @return array
271 */
272 public function captcha_check() {
273 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
274 case 'v2':
275 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key' ) );
276 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
277 break;
278 case 'invisible':
279 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3' ) );
280 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
281 break;
282 case 'v3':
283 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3_new' ) );
284 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
285 break;
286 }
287
288 if ( ! $secret_key || ! $site_key ) {
289 return [
290 'response' => false,
291 'reason' => 'ERROR_NO_KEYS',
292 ];
293 }
294
295 if ( ! isset( $_POST['g-recaptcha-response'] ) || empty( $_POST['g-recaptcha-response'] ) ) {
296 return [
297 'response' => false,
298 'reason' => 'RECAPTCHA_EMPTY_RESPONSE',
299 ];
300 }
301
302 $response = $this->get_captcha_response( $secret_key );
303 if ( isset( $response['success'] ) && ! empty( $response['success'] ) ) {
304 return [
305 'response' => true,
306 'reason' => '',
307 ];
308 }
309
310 return [
311 'response' => false,
312 'reason' => 'VERIFICATION_FAILED',
313 ];
314 }
315
316 /**
317 * Query Google for reAPTCHA validation and response.
318 *
319 * @param string $privatekey
320 * @return array
321 */
322 public function get_captcha_response( $privatekey ) {
323 $args = [
324 'body' => [
325 'secret' => $privatekey,
326 'response' => $_POST['g-recaptcha-response'],
327 ],
328 'sslverify' => false,
329 ];
330 $resp = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', $args );
331 return json_decode( wp_remote_retrieve_body( $resp ), true );
332 }
333
334 /**
335 * Delete the readme.html file.
336 *
337 * @return void
338 */
339 public function delete_readme() {
340 if ( get_site_option( 'patchstack_rm_readme', false ) != true || ! file_exists( ABSPATH . 'readme.html' ) ) {
341 return;
342 }
343
344 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
345 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
346 $fs = new WP_Filesystem_Direct( '' );
347 $fs->delete( ABSPATH . 'readme.html' );
348 }
349
350 /**
351 * Disable user enumeration with ?author= and the REST endpoint.
352 *
353 * @return void
354 */
355 public function stop_user_enum() {
356 if ( isset( $_GET['author'] ) && ! is_user_logged_in() && ! is_admin() ) {
357 die( wp_safe_redirect( get_site_url() ) );
358 }
359
360 if ( stripos( $_SERVER['REQUEST_URI'], 'v2/users' ) !== false || ( isset( $_REQUEST['rest_route'] ) && stripos( $_REQUEST['rest_route'], 'v2/users' ) !== false ) ) {
361 if ( ! is_user_logged_in() ) {
362 die( wp_safe_redirect( get_site_url() ) );
363 }
364 }
365 }
366
367 /**
368 * Hide the WordPress generator version in response.
369 *
370 * @return string
371 */
372 public function remove_generator() {
373 return '';
374 }
375
376 /**
377 * Determine if the email address of a new registration matches the defined patterns.
378 * This filter is called on regular sites.
379 *
380 * @param object $errors
381 * @param string $sanitized_user_login
382 * @param string $user_email
383 * @return object
384 */
385 public function check_email_pattern( $errors, $sanitized_user_login, $user_email ) {
386 $patterns = explode( ',', $this->get_option( 'patchstack_register_email_blacklist' ) );
387 foreach ( $patterns as $pattern ) {
388 if ( stripos( $user_email, $pattern ) !== false ) {
389 $errors->add( 'user_email', __( 'An invalid email address has been supplied.', 'patchstack' ) );
390 }
391 }
392
393 return $errors;
394 }
395
396 /**
397 * Determine if the email address of a new registration matches the defined patterns.
398 * This filter is called on network sites.
399 *
400 * @param array $result
401 * @return array
402 */
403 public function check_email_pattern_wpmu( $result ) {
404 if ( isset( $result['user_email'] ) ) {
405 $patterns = explode( ',', $this->get_option( 'patchstack_register_email_blacklist' ) );
406 foreach ( $patterns as $pattern ) {
407 if ( stripos( $result['user_email'], $pattern ) !== false ) {
408 $result['errors']->add( 'user_email', __( 'An invalid email address has been supplied.', 'patchstack' ) );
409 }
410 }
411 }
412
413 return $result;
414 }
415 }
416