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

397 lines 12.6 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', array( $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', array( $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', array() ) ) ) {
40 add_action( 'init', array( $this, 'geo_block_check' ), 10 );
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', array( $this, 'captcha_display' ) );
46 add_filter( 'preprocess_comment', array( $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', array( $this, 'disable_wpjson' ) );
62 }
63
64 // Prevent user enumeration?
65 if ( $this->get_option( 'patchstack_userenum' ) ) {
66 add_action( 'init', array( $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', array( $this, 'remove_generator' ) );
73 }
74
75 // Block email registration patterns?
76 if ( $this->get_option( 'patchstack_register_email_blacklist', '' ) != '' ) {
77 add_filter( 'registration_errors', array( $this, 'check_email_pattern' ), 1, 3 );
78 add_filter( 'wpmu_validate_user_signup', array( $this, 'check_email_pattern_wpmu' ), 1, 1 );
79 }
80
81 // Auto update software?
82 $update = get_site_option( 'patchstack_auto_update', array() );
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', array() );
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', array() );
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 = array();
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', array() );
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', array() );
153 $ip = $this->get_ip();
154
155 // Don't block Patchstack.
156 if ( in_array( $ip, $this->plugin->ips ) ) {
157 return;
158 }
159
160 // Load the required libraries.
161 try {
162 require_once __DIR__ . '/../lib/geoip2-php/autoload.php';
163 $reader = new GeoIp2\Database\Reader( __DIR__ . '/../lib/GeoLite2-Country.mmdb' );
164 $record = $reader->country( $ip );
165
166 // Determine if we want to do an inverse check or not.
167 $match = in_array( $record->country->isoCode, $countries );
168 $match = $this->get_option( 'patchstack_geo_block_inverse', false ) ? ! $match : $match;
169
170 // Check if there's a match.
171 if ( $match ) {
172 $this->plugin->firewall_base->display_error_page( 23 );
173 }
174 } catch ( \Exception $e ) {
175 }
176 }
177
178 /**
179 * Prevent unauthorized users from accessing wp-json.
180 *
181 * @return void|WP_Error
182 */
183 public function disable_wpjson() {
184 if ( ! is_user_logged_in() ) {
185 $msg = apply_filters( 'disable_wp_rest_api_error', __( 'The WP REST API cannot be accessed by unauthorized users.', 'disable-wp-rest-api' ) );
186 return new WP_Error( 'rest_authorization_required', $msg, array( 'status' => rest_authorization_required_code() ) );
187 }
188 }
189
190 /**
191 * Set security headers if the option is enabled.
192 *
193 * @param array $headers
194 * @return void|array
195 */
196 public function set_security_headers( $headers ) {
197 if ( get_site_option( 'patchstack_add_security_headers' ) ) {
198 $headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
199 $headers['X-Frame-Options'] = 'SAMEORIGIN';
200 $headers['X-XSS-Protection'] = '1; mode=block';
201 $headers['X-Content-Type-Options'] = 'nosniff';
202 $headers['X-Powered-By'] = null;
203 $headers['Server'] = null;
204 $headers['Strict-Transport-Security'] = 'max-age=31536000';
205 }
206
207 return $headers;
208 }
209
210 /**
211 * Determine if the reCAPTCHA is valid upon comment submission.
212 *
213 * @param array $comment_data
214 * @return void|array
215 */
216 public function verify_recaptcha( $comment_data ) {
217 $result = $this->captcha_check();
218 if ( ! $result['response'] && ( $result['reason'] === 'VERIFICATION_FAILED' || $result['reason'] === 'RECAPTCHA_EMPTY_RESPONSE' ) ) {
219 wp_clear_auth_cookie();
220 wp_die( 'reCaptcha was not solved or response was empty', 'Error' );
221 }
222
223 return $comment_data;
224 }
225
226 /**
227 * Add the captcha to the comments form.
228 *
229 * @return void
230 */
231 public function captcha_display() {
232 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
233 case 'v2':
234 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
235 require_once dirname( __FILE__ ) . '/views/captcha_v2.php';
236 break;
237 case 'invisible':
238 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
239 require_once dirname( __FILE__ ) . '/views/captcha_invisible.php';
240 break;
241 case 'v3':
242 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
243 require_once dirname( __FILE__ ) . '/views/captcha_v3.php';
244 break;
245 }
246 }
247
248 /**
249 * Check if the submitted reCAPTCHA is valid.
250 *
251 * @return array
252 */
253 public function captcha_check() {
254 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
255 case 'v2':
256 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key' ) );
257 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
258 break;
259 case 'invisible':
260 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3' ) );
261 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
262 break;
263 case 'v3':
264 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3_new' ) );
265 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
266 break;
267 }
268
269 if ( ! $secret_key || ! $site_key ) {
270 return array(
271 'response' => false,
272 'reason' => 'ERROR_NO_KEYS',
273 );
274 }
275
276 if ( ! isset( $_POST['g-recaptcha-response'] ) || empty( $_POST['g-recaptcha-response'] ) ) {
277 return array(
278 'response' => false,
279 'reason' => 'RECAPTCHA_EMPTY_RESPONSE',
280 );
281 }
282
283 $response = $this->plugin->hardening->get_captcha_response( $secret_key );
284 if ( isset( $response['success'] ) && ! empty( $response['success'] ) ) {
285 return array(
286 'response' => true,
287 'reason' => '',
288 );
289 }
290
291 return array(
292 'response' => false,
293 'reason' => 'VERIFICATION_FAILED',
294 );
295 }
296
297 /**
298 * Query Google for reAPTCHA validation and response.
299 *
300 * @param string $privatekey
301 * @return array
302 */
303 public function get_captcha_response( $privatekey ) {
304 $args = array(
305 'body' => array(
306 'secret' => $privatekey,
307 'response' => $_POST['g-recaptcha-response'],
308 ),
309 'sslverify' => false,
310 );
311 $resp = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', $args );
312 return json_decode( wp_remote_retrieve_body( $resp ), true );
313 }
314
315 /**
316 * Delete the readme.html file.
317 *
318 * @return void
319 */
320 public function delete_readme() {
321 if ( get_site_option( 'patchstack_rm_readme', false ) != true || ! file_exists( ABSPATH . 'readme.html' ) ) {
322 return;
323 }
324
325 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
326 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
327 $fs = new WP_Filesystem_Direct( '' );
328 $fs->delete( ABSPATH . 'readme.html' );
329 }
330
331 /**
332 * Disable user enumeration with ?author= and the REST endpoint.
333 *
334 * @return void
335 */
336 public function stop_user_enum() {
337 if ( isset( $_GET['author'] ) && is_numeric( $_GET['author'] ) && ! is_user_logged_in() ) {
338 die( wp_safe_redirect( get_site_url() ) );
339 }
340
341 if ( stripos( $_SERVER['REQUEST_URI'], 'v2/users' ) !== false || ( isset( $_REQUEST['rest_route'] ) && stripos( $_REQUEST['rest_route'], 'v2/users' ) !== false ) ) {
342 if ( ! is_user_logged_in() ) {
343 die( wp_safe_redirect( get_site_url() ) );
344 }
345 }
346 }
347
348 /**
349 * Hide the WordPress generator version in response.
350 *
351 * @return string
352 */
353 public function remove_generator() {
354 return '';
355 }
356
357 /**
358 * Determine if the email address of a new registration matches the defined patterns.
359 * This filter is called on regular sites.
360 *
361 * @param object $errors
362 * @param string $sanitized_user_login
363 * @param string $user_email
364 * @return object
365 */
366 public function check_email_pattern( $errors, $sanitized_user_login, $user_email ) {
367 $patterns = explode( ',', $this->get_option( 'patchstack_register_email_blacklist' ) );
368 foreach ( $patterns as $pattern ) {
369 if ( stripos( $user_email, $pattern ) !== false ) {
370 $errors->add( 'user_email', __( 'An invalid email address has been supplied.', 'patchstack' ) );
371 }
372 }
373
374 return $errors;
375 }
376
377 /**
378 * Determine if the email address of a new registration matches the defined patterns.
379 * This filter is called on network sites.
380 *
381 * @param array $result
382 * @return array
383 */
384 public function check_email_pattern_wpmu( $result ) {
385 if ( isset( $result['user_email'] ) ) {
386 $patterns = explode( ',', $this->get_option( 'patchstack_register_email_blacklist' ) );
387 foreach ( $patterns as $pattern ) {
388 if ( stripos( $result['user_email'], $pattern ) !== false ) {
389 $result['errors']->add( 'user_email', __( 'An invalid email address has been supplied.', 'patchstack' ) );
390 }
391 }
392 }
393
394 return $result;
395 }
396 }
397