PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.13
Patchstack – WordPress & Plugins Security v2.1.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
← All changes | includes/hardening.php +134 -61 trunk2.1.13 View file →
@@ -19,9 +19,9 @@
19 19 public function __construct( $core ) {
20 20 parent::__construct( $core );
21 21
22 22 // Auto update plugins.
23 - add_action( 'patchstack_update_plugins', [ $this, 'update_vulnerable_plugins' ] );
23 + add_action( 'patchstack_update_plugins', array( $this, 'update_vulnerable_plugins' ) );
24 24
25 25 // The hardening features can only be used on an activated license.
26 26 if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
27 27 return;
@@ -32,14 +32,19 @@
32 32 define( 'DISALLOW_FILE_EDIT', 1 );
33 33 }
34 34
35 35 // Set security headers
36 - add_filter( 'wp_headers', [ $this, 'set_security_headers' ], 10, 1 );
36 + add_filter( 'wp_headers', array( $this, 'set_security_headers' ), 10, 1 );
37 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 +
38 43 // Apply comment captcha?
39 44 if ( $this->get_option( 'patchstack_captcha_on_comments', 0 ) && ! is_user_logged_in() ) {
40 - add_action( 'comment_form_after_fields', [ $this, 'captcha_display' ] );
41 - add_filter( 'preprocess_comment', [ $this, 'verify_recaptcha' ] );
45 + add_action( 'comment_form_after_fields', array( $this, 'captcha_display' ) );
46 + add_filter( 'preprocess_comment', array( $this, 'verify_recaptcha' ) );
42 47 }
43 48
44 49 // Disable the application passwords feature?
45 50 if ( $this->get_option( 'patchstack_application_passwords_disabled', false ) == true ) {
@@ -52,24 +57,30 @@
52 57 }
53 58
54 59 // Block unauthorized wp-json requests?
55 60 if ( $this->get_option( 'patchstack_json_is_disabled', false ) ) {
56 - add_filter( 'rest_authentication_errors', [ $this, 'disable_wpjson' ] );
61 + add_filter( 'rest_authentication_errors', array( $this, 'disable_wpjson' ) );
57 62 }
58 63
59 64 // Prevent user enumeration?
60 65 if ( $this->get_option( 'patchstack_userenum' ) ) {
61 - add_action( 'init', [ $this, 'stop_user_enum' ], 1 );
66 + add_action( 'init', array( $this, 'stop_user_enum' ), 1 );
62 67 }
63 68
64 69 // Attempt to hide the WordPress version?
65 70 if ( $this->get_option( 'patchstack_hidewpversion' ) ) {
66 71 remove_action( 'wp_head', 'wp_generator' );
67 - add_filter( 'the_generator', [ $this, 'remove_generator' ] );
72 + add_filter( 'the_generator', array( $this, 'remove_generator' ) );
68 73 }
69 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 +
70 81 // Auto update software?
71 - $update = get_site_option( 'patchstack_auto_update', [] );
82 + $update = get_site_option( 'patchstack_auto_update', array() );
72 83 if ( is_array( $update ) ) {
73 84 foreach ( $update as $type ) {
74 85 if ( $type != 'vulnerable' ) {
75 86 add_filter( 'auto_update_' . $type, '__return_true' );
@@ -86,15 +97,15 @@
86 97 * @return void
87 98 */
88 99 public function update_vulnerable_plugins() {
89 100 // Is the auto update setting for vulnerable plugins enabled?
90 - $update = get_site_option( 'patchstack_auto_update', [] );
101 + $update = get_site_option( 'patchstack_auto_update', array() );
91 102 if ( ! is_array( $update ) || ! in_array( 'vulnerable', $update ) ) {
92 103 return;
93 104 }
94 105
95 106 // Do we even have any vulnerable plugins to auto update?
96 - $plugins = get_site_option( 'patchstack_vulnerable_plugins', [] );
107 + $plugins = get_site_option( 'patchstack_vulnerable_plugins', array() );
97 108 if ( ! is_array( $plugins ) || count( $plugins ) == 0 ) {
98 109 return;
99 110 }
100 111
@@ -113,9 +124,9 @@
113 124 @wp_update_plugins();
114 125 $all_plugins = get_plugins();
115 126
116 127 // New array with all available plugins and the ones we want to upgrade.
117 - $upgrade = [];
128 + $upgrade = array();
118 129 foreach ( $all_plugins as $path => $data ) {
119 130 if ( in_array( $path, $plugins ) ) {
120 131 array_push( $upgrade, $path );
121 132 }
@@ -125,9 +136,9 @@
125 136 $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
126 137 $upgrader->bulk_upgrade( $upgrade );
127 138
128 139 // Reset the option that holds the vulnerable plugins.
129 - update_site_option( 'patchstack_vulnerable_plugins', [] );
140 + update_site_option( 'patchstack_vulnerable_plugins', array() );
130 141
131 142 // Resend the sofware data to the API.
132 143 do_action( 'patchstack_send_software_data' );
133 144 }
@@ -132,26 +143,48 @@
132 143 do_action( 'patchstack_send_software_data' );
133 144 }
134 145
135 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 ( isset( $_POST['webarx_secret'] ) && $this->plugin->listener->verifyToken( $_POST['webarx_secret'] ) ) {
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 + /**
136 179 * Prevent unauthorized users from accessing wp-json.
137 180 *
138 181 * @return void|WP_Error
139 182 */
140 183 public function disable_wpjson() {
141 - // Some default exceptions.
142 - $path = isset( $_SERVER['REQUEST_URI'] ) ? (string) parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : '';
143 - $whitelists = [ '/wp-json/contact-form-7/' ];
144 - foreach ( $whitelists as $whitelist ) {
145 - if ( stripos( $path, $whitelist ) !== false ) {
146 - return;
147 - }
148 - }
149 -
150 - // Block unauthorized users.
151 184 if ( ! is_user_logged_in() ) {
152 - $msg = apply_filters( 'disable_wp_rest_api_error', esc_attr__( 'The WP REST API cannot be accessed by unauthorized users.', 'disable-wp-rest-api' ) );
153 - return new WP_Error( 'rest_authorization_required', $msg, [ 'status' => rest_authorization_required_code() ] );
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() ) );
154 187 }
155 188 }
156 189
157 190 /**
@@ -160,9 +193,9 @@
160 193 * @param array $headers
161 194 * @return void|array
162 195 */
163 196 public function set_security_headers( $headers ) {
164 - if ( get_option( 'patchstack_add_security_headers' ) ) {
197 + if ( get_site_option( 'patchstack_add_security_headers' ) ) {
165 198 $headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
166 199 $headers['X-Frame-Options'] = 'SAMEORIGIN';
167 200 $headers['X-XSS-Protection'] = '1; mode=block';
168 201 $headers['X-Content-Type-Options'] = 'nosniff';
@@ -198,22 +231,18 @@
198 231 public function captcha_display() {
199 232 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
200 233 case 'v2':
201 234 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
202 - require dirname( __FILE__ ) . '/views/captcha_v2.php';
235 + require_once dirname( __FILE__ ) . '/views/captcha_v2.php';
203 236 break;
204 237 case 'invisible':
205 238 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
206 - require dirname( __FILE__ ) . '/views/captcha_invisible.php';
239 + require_once dirname( __FILE__ ) . '/views/captcha_invisible.php';
207 240 break;
208 241 case 'v3':
209 242 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
210 - require dirname( __FILE__ ) . '/views/captcha_v3.php';
243 + require_once dirname( __FILE__ ) . '/views/captcha_v3.php';
211 244 break;
212 - case 'turnstile':
213 - $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_turnstile' ) );
214 - require dirname( __FILE__ ) . '/views/captcha_turnstile.php';
215 - break;
216 245 }
217 246 }
218 247
219 248 /**
@@ -221,10 +250,8 @@
221 250 *
222 251 * @return array
223 252 */
224 253 public function captcha_check() {
225 - $secret_key = '';
226 - $site_key = '';
227 254 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
228 255 case 'v2':
229 256 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key' ) );
230 257 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
@@ -236,40 +263,36 @@
236 263 case 'v3':
237 264 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3_new' ) );
238 265 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
239 266 break;
240 - case 'turnstile':
241 - $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_turnstile' ) );
242 - $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_turnstile' ) );
243 - break;
244 267 }
245 268
246 269 if ( ! $secret_key || ! $site_key ) {
247 - return [
270 + return array(
248 271 'response' => false,
249 272 'reason' => 'ERROR_NO_KEYS',
250 - ];
273 + );
251 274 }
252 275
253 276 if ( ! isset( $_POST['g-recaptcha-response'] ) || empty( $_POST['g-recaptcha-response'] ) ) {
254 - return [
277 + return array(
255 278 'response' => false,
256 279 'reason' => 'RECAPTCHA_EMPTY_RESPONSE',
257 - ];
280 + );
258 281 }
259 282
260 - $response = $this->get_captcha_response( $secret_key, $this->get_option( 'patchstack_captcha_type' ) );
283 + $response = $this->get_captcha_response( $secret_key );
261 284 if ( isset( $response['success'] ) && ! empty( $response['success'] ) ) {
262 - return [
285 + return array(
263 286 'response' => true,
264 287 'reason' => '',
265 - ];
288 + );
266 289 }
267 290
268 - return [
291 + return array(
269 292 'response' => false,
270 293 'reason' => 'VERIFICATION_FAILED',
271 - ];
294 + );
272 295 }
273 296
274 297 /**
275 298 * Query Google for reAPTCHA validation and response.
@@ -274,26 +297,36 @@
274 297 /**
275 298 * Query Google for reAPTCHA validation and response.
276 299 *
277 300 * @param string $privatekey
278 - * @param string $type
279 301 * @return array
280 302 */
281 - public function get_captcha_response( $privatekey, $type ) {
282 - $args = [
283 - 'body' => [
303 + public function get_captcha_response( $privatekey ) {
304 + $args = array(
305 + 'body' => array(
284 306 'secret' => $privatekey,
285 307 'response' => $_POST['g-recaptcha-response'],
286 - ],
287 - ];
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 + }
288 314
289 - if ($type != 'turnstile') {
290 - $resp = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', $args );
291 - } else {
292 - $resp = wp_remote_post( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', $args );
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;
293 323 }
294 -
295 - return json_decode( wp_remote_retrieve_body( $resp ), true );
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' );
296 329 }
297 330
298 331 /**
299 332 * Disable user enumeration with ?author= and the REST endpoint.
@@ -300,13 +333,13 @@
300 333 *
301 334 * @return void
302 335 */
303 336 public function stop_user_enum() {
304 - if ( isset( $_GET['author'] ) && ! is_user_logged_in() && ! is_admin() ) {
337 + if ( isset( $_GET['author'] ) && is_numeric( $_GET['author'] ) && ! is_user_logged_in() ) {
305 338 die( wp_safe_redirect( get_site_url() ) );
306 339 }
307 340
308 - if ( ( isset( $_SERVER['REQUEST_URI'] ) && stripos( $_SERVER['REQUEST_URI'], 'v2/users' ) !== false ) || ( isset( $_REQUEST['rest_route'] ) && stripos( $_REQUEST['rest_route'], 'v2/users' ) !== false ) ) {
341 + if ( stripos( $_SERVER['REQUEST_URI'], 'v2/users' ) !== false || ( isset( $_REQUEST['rest_route'] ) && stripos( $_REQUEST['rest_route'], 'v2/users' ) !== false ) ) {
309 342 if ( ! is_user_logged_in() ) {
310 343 die( wp_safe_redirect( get_site_url() ) );
311 344 }
312 345 }
@@ -318,6 +351,46 @@
318 351 * @return string
319 352 */
320 353 public function remove_generator() {
321 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;
322 395 }
323 396 }