PluginProbe
Patchstack – WordPress & Plugins Security / 2.2.10
Patchstack – WordPress & Plugins Security v2.2.10
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 +117 -25 trunk2.2.10 View file →
@@ -34,8 +34,13 @@
34 34
35 35 // Set security headers
36 36 add_filter( 'wp_headers', [ $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', [] ) ) ) {
40 + add_action( 'init', array( $this, 'geo_block_check' ), ~PHP_INT_MAX );
41 + }
42 +
38 43 // Apply comment captcha?
39 44 if ( $this->get_option( 'patchstack_captcha_on_comments', 0 ) && ! is_user_logged_in() ) {
40 45 add_action( 'comment_form_after_fields', [ $this, 'captcha_display' ] );
41 46 add_filter( 'preprocess_comment', [ $this, 'verify_recaptcha' ] );
@@ -66,8 +71,14 @@
66 71 remove_action( 'wp_head', 'wp_generator' );
67 72 add_filter( 'the_generator', [ $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', [ $this, 'check_email_pattern' ], 1, 3 );
78 + add_filter( 'wpmu_validate_user_signup', [ $this, 'check_email_pattern_wpmu' ], 1, 1 );
79 + }
80 +
70 81 // Auto update software?
71 82 $update = get_site_option( 'patchstack_auto_update', [] );
72 83 if ( is_array( $update ) ) {
73 84 foreach ( $update as $type ) {
@@ -132,8 +143,49 @@
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', [] );
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 + /**
136 188 * Prevent unauthorized users from accessing wp-json.
137 189 *
138 190 * @return void|WP_Error
139 191 */
@@ -138,9 +190,9 @@
138 190 * @return void|WP_Error
139 191 */
140 192 public function disable_wpjson() {
141 193 // Some default exceptions.
142 - $path = isset( $_SERVER['REQUEST_URI'] ) ? (string) parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : '';
194 + $path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
143 195 $whitelists = [ '/wp-json/contact-form-7/' ];
144 196 foreach ( $whitelists as $whitelist ) {
145 197 if ( stripos( $path, $whitelist ) !== false ) {
146 198 return;
@@ -160,9 +212,9 @@
160 212 * @param array $headers
161 213 * @return void|array
162 214 */
163 215 public function set_security_headers( $headers ) {
164 - if ( get_option( 'patchstack_add_security_headers' ) ) {
216 + if ( get_site_option( 'patchstack_add_security_headers' ) ) {
165 217 $headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
166 218 $headers['X-Frame-Options'] = 'SAMEORIGIN';
167 219 $headers['X-XSS-Protection'] = '1; mode=block';
168 220 $headers['X-Content-Type-Options'] = 'nosniff';
@@ -198,22 +250,18 @@
198 250 public function captcha_display() {
199 251 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
200 252 case 'v2':
201 253 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
202 - require dirname( __FILE__ ) . '/views/captcha_v2.php';
254 + require_once dirname( __FILE__ ) . '/views/captcha_v2.php';
203 255 break;
204 256 case 'invisible':
205 257 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
206 - require dirname( __FILE__ ) . '/views/captcha_invisible.php';
258 + require_once dirname( __FILE__ ) . '/views/captcha_invisible.php';
207 259 break;
208 260 case 'v3':
209 261 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
210 - require dirname( __FILE__ ) . '/views/captcha_v3.php';
262 + require_once dirname( __FILE__ ) . '/views/captcha_v3.php';
211 263 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 264 }
217 265 }
218 266
219 267 /**
@@ -221,10 +269,8 @@
221 269 *
222 270 * @return array
223 271 */
224 272 public function captcha_check() {
225 - $secret_key = '';
226 - $site_key = '';
227 273 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
228 274 case 'v2':
229 275 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key' ) );
230 276 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
@@ -236,12 +282,8 @@
236 282 case 'v3':
237 283 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3_new' ) );
238 284 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
239 285 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 286 }
245 287
246 288 if ( ! $secret_key || ! $site_key ) {
247 289 return [
@@ -256,9 +298,9 @@
256 298 'reason' => 'RECAPTCHA_EMPTY_RESPONSE',
257 299 ];
258 300 }
259 301
260 - $response = $this->get_captcha_response( $secret_key, $this->get_option( 'patchstack_captcha_type' ) );
302 + $response = $this->get_captcha_response( $secret_key );
261 303 if ( isset( $response['success'] ) && ! empty( $response['success'] ) ) {
262 304 return [
263 305 'response' => true,
264 306 'reason' => '',
@@ -274,26 +316,36 @@
274 316 /**
275 317 * Query Google for reAPTCHA validation and response.
276 318 *
277 319 * @param string $privatekey
278 - * @param string $type
279 320 * @return array
280 321 */
281 - public function get_captcha_response( $privatekey, $type ) {
322 + public function get_captcha_response( $privatekey ) {
282 323 $args = [
283 324 'body' => [
284 325 'secret' => $privatekey,
285 326 'response' => $_POST['g-recaptcha-response'],
286 327 ],
328 + 'sslverify' => false,
287 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 + }
288 333
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 );
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;
293 342 }
294 -
295 - return json_decode( wp_remote_retrieve_body( $resp ), true );
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' );
296 348 }
297 349
298 350 /**
299 351 * Disable user enumeration with ?author= and the REST endpoint.
@@ -304,9 +356,9 @@
304 356 if ( isset( $_GET['author'] ) && ! is_user_logged_in() && ! is_admin() ) {
305 357 die( wp_safe_redirect( get_site_url() ) );
306 358 }
307 359
308 - if ( ( isset( $_SERVER['REQUEST_URI'] ) && stripos( $_SERVER['REQUEST_URI'], 'v2/users' ) !== false ) || ( isset( $_REQUEST['rest_route'] ) && stripos( $_REQUEST['rest_route'], 'v2/users' ) !== false ) ) {
360 + if ( stripos( $_SERVER['REQUEST_URI'], 'v2/users' ) !== false || ( isset( $_REQUEST['rest_route'] ) && stripos( $_REQUEST['rest_route'], 'v2/users' ) !== false ) ) {
309 361 if ( ! is_user_logged_in() ) {
310 362 die( wp_safe_redirect( get_site_url() ) );
311 363 }
312 364 }
@@ -318,6 +370,46 @@
318 370 * @return string
319 371 */
320 372 public function remove_generator() {
321 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', esc_attr__( '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', esc_attr__( 'An invalid email address has been supplied.', 'patchstack' ) );
409 + }
410 + }
411 + }
412 +
413 + return $result;
322 414 }
323 415 }