PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 11.8.4
Jetpack – WP Security, Backup, Speed, & Growth v11.8.4
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / modules / protect.php

protect.php in Jetpack – WP Security, Backup, Speed, & Growth 11.8.4, at modules/protect.php

1,007 lines 28.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Module Name: Brute force protection
4 * Module Description: Enabling brute force protection will prevent bots and hackers from attempting to log in to your website with common username and password combinations.
5 * Sort Order: 1
6 * Recommendation Order: 4
7 * First Introduced: 3.4
8 * Requires Connection: Yes
9 * Requires User Connection: Yes
10 * Auto Activate: Yes
11 * Module Tags: Recommended
12 * Feature: Security
13 * Additional Search Queries: security, jetpack protect, secure, protection, botnet, brute force, protect, login, bot, password, passwords, strong passwords, strong password, wp-login.php, protect admin
14 */
15
16 use Automattic\Jetpack\Constants;
17
18 require_once JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php';
19
20 /**
21 * Jetpack project module class.
22 */
23 class Jetpack_Protect_Module {
24
25 /**
26 * Instance of the class.
27 *
28 * @var Jetpack_Protect_Module()
29 */
30 private static $instance = null;
31
32 /**
33 * API Key.
34 *
35 * @var string
36 */
37 public $api_key;
38
39 /**
40 * API Key error.
41 *
42 * @var string
43 */
44 public $api_key_error;
45
46 /**
47 * Whitelisted ips
48 *
49 * @var array
50 */
51 public $whitelist;
52
53 /**
54 * Whitelist error.
55 *
56 * @var string
57 */
58 public $whitelist_error;
59
60 /**
61 * Whitelist saved
62 *
63 * @todo find out if this is even used.
64 *
65 * @var array
66 */
67 public $whitelist_saved;
68
69 /**
70 * The URI.
71 *
72 * @var string
73 */
74 private $local_host;
75
76 /**
77 * Last request.
78 *
79 * @todo find out if this is even used.
80 *
81 * @var string
82 */
83 public $last_request;
84
85 /**
86 * Response fetched from wp_remote_post()
87 *
88 * @var array
89 */
90 public $last_response_raw;
91
92 /**
93 * Last response.
94 *
95 * @todo find out if this is used.
96 * @var array
97 */
98 public $last_response;
99
100 /**
101 * Block login with math, default is 1.
102 *
103 * @var int
104 */
105 private $block_login_with_math;
106
107 /**
108 * Singleton implementation
109 *
110 * @return object
111 */
112 public static function instance() {
113 if ( ! is_a( self::$instance, 'Jetpack_Protect_Module' ) ) {
114 self::$instance = new Jetpack_Protect_Module();
115 }
116
117 return self::$instance;
118 }
119
120 /**
121 * Registers actions
122 */
123 private function __construct() {
124 add_action( 'jetpack_activate_module_protect', array( $this, 'on_activation' ) );
125 add_action( 'jetpack_deactivate_module_protect', array( $this, 'on_deactivation' ) );
126 add_action( 'jetpack_modules_loaded', array( $this, 'modules_loaded' ) );
127 add_action( 'login_form', array( $this, 'check_use_math' ), 0 );
128 add_filter( 'authenticate', array( $this, 'check_preauth' ), 10, 3 );
129 add_action( 'wp_login', array( $this, 'log_successful_login' ), 10, 2 );
130 add_action( 'wp_login_failed', array( $this, 'log_failed_attempt' ) );
131 add_action( 'admin_init', array( $this, 'maybe_update_headers' ) );
132 add_action( 'admin_init', array( $this, 'maybe_display_security_warning' ) );
133
134 // This is a backup in case $pagenow fails for some reason.
135 add_action( 'login_form', array( $this, 'check_login_ability' ), 1 );
136
137 // Load math fallback after math page form submission.
138 if ( isset( $_POST['jetpack_protect_process_math_form'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- POST request just determines if we need to use Math for Authentication.
139 include_once __DIR__ . '/protect/math-fallback.php';
140 new Jetpack_Protect_Math_Authenticate();
141 }
142
143 // Runs a script every day to clean up expired transients so they don't
144 // clog up our users' databases.
145 require_once JETPACK__PLUGIN_DIR . '/modules/protect/transient-cleanup.php';
146 }
147
148 /**
149 * On module activation, try to get an api key
150 */
151 public function on_activation() {
152 if ( is_multisite() && is_main_site() && get_site_option( 'jetpack_protect_active', 0 ) == 0 ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
153 update_site_option( 'jetpack_protect_active', 1 );
154 }
155
156 update_site_option( 'jetpack_protect_activating', 'activating' );
157
158 // Get BruteProtect's counter number.
159 self::protect_call( 'check_key' );
160 }
161
162 /**
163 * On module deactivation, unset protect_active
164 */
165 public function on_deactivation() {
166 if ( is_multisite() && is_main_site() ) {
167 update_site_option( 'jetpack_protect_active', 0 );
168 }
169 }
170
171 /**
172 * Get the protect key,
173 */
174 public function maybe_get_protect_key() {
175 if ( get_site_option( 'jetpack_protect_activating', false ) && ! get_site_option( 'jetpack_protect_key', false ) ) {
176 $key = $this->get_protect_key();
177 delete_site_option( 'jetpack_protect_activating' );
178 return $key;
179 }
180
181 return get_site_option( 'jetpack_protect_key' );
182 }
183
184 /**
185 * Sends a "check_key" API call once a day. This call allows us to track IP-related
186 * headers for this server via the Protect API, in order to better identify the source
187 * IP for login attempts
188 *
189 * @param bool $force - if we're forcing the request.
190 */
191 public function maybe_update_headers( $force = false ) {
192 $updated_recently = $this->get_transient( 'jpp_headers_updated_recently' );
193
194 if ( ! $force ) {
195 if ( isset( $_GET['protect_update_headers'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this doesn't change anything, just forces the once-a-day check to run via force if set.
196 $force = true;
197 }
198 }
199
200 // check that current user is admin so we prevent a lower level user from adding
201 // a trusted header, allowing them to brute force an admin account.
202 if ( ( $updated_recently && ! $force ) || ! current_user_can( 'update_plugins' ) ) {
203 return;
204 }
205
206 $response = self::protect_call( 'check_key' );
207 $this->set_transient( 'jpp_headers_updated_recently', 1, DAY_IN_SECONDS );
208
209 if ( isset( $response['msg'] ) && $response['msg'] ) {
210 update_site_option( 'trusted_ip_header', json_decode( $response['msg'] ) );
211 }
212 }
213
214 /**
215 * Handle discplaying a security warning.
216 */
217 public function maybe_display_security_warning() {
218 if ( is_multisite() && current_user_can( 'manage_network' ) ) {
219 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
220 require_once ABSPATH . '/wp-admin/includes/plugin.php';
221 }
222
223 if ( ! is_plugin_active_for_network( plugin_basename( JETPACK__PLUGIN_FILE ) ) ) {
224 add_action( 'load-index.php', array( $this, 'prepare_jetpack_protect_multisite_notice' ) );
225 add_action( 'wp_ajax_jetpack-protect-dismiss-multisite-banner', array( $this, 'ajax_dismiss_handler' ) );
226 }
227 }
228 }
229
230 /**
231 * Handles preparing the multisite notice.
232 */
233 public function prepare_jetpack_protect_multisite_notice() {
234 $dismissed = get_site_option( 'jetpack_dismissed_protect_multisite_banner' );
235 if ( $dismissed ) {
236 return;
237 }
238
239 add_action( 'admin_notices', array( $this, 'admin_jetpack_manage_notice' ) );
240 }
241
242 /**
243 * Handle dismissing the multisite banner.
244 */
245 public function ajax_dismiss_handler() {
246 check_ajax_referer( 'jetpack_protect_multisite_banner_opt_out' );
247
248 if ( ! current_user_can( 'manage_network' ) ) {
249 wp_send_json_error( new WP_Error( 'insufficient_permissions' ) );
250 }
251
252 update_site_option( 'jetpack_dismissed_protect_multisite_banner', true );
253
254 wp_send_json_success();
255 }
256
257 /**
258 * Displays a warning about Jetpack Protect's network activation requirement.
259 * Attaches some custom JS to Core's `is-dismissible` UI to save the dismissed state.
260 */
261 public function admin_jetpack_manage_notice() {
262 ?>
263 <div class="jetpack-protect-warning notice notice-warning is-dismissible" data-dismiss-nonce="<?php echo esc_attr( wp_create_nonce( 'jetpack_protect_multisite_banner_opt_out' ) ); ?>">
264 <h2><?php esc_html_e( 'Jetpack Brute Force Attack Prevention cannot keep your site secure', 'jetpack' ); ?></h2>
265
266 <p><?php esc_html_e( "Thanks for activating Jetpack's brute force attack prevention feature! To start protecting your whole WordPress Multisite Network, please network activate the Jetpack plugin. Due to the way logins are handled on WordPress Multisite Networks, Jetpack must be network activated in order for the brute force attack prevention feature to work properly.", 'jetpack' ); ?></p>
267
268 <p>
269 <a class="button-primary" href="<?php echo esc_url( network_admin_url( 'plugins.php' ) ); ?>">
270 <?php esc_html_e( 'View Network Admin', 'jetpack' ); ?>
271 </a>
272 <a class="button" href="<?php echo esc_url( __( 'https://jetpack.com/support/multisite-protect', 'jetpack' ) ); ?>" target="_blank">
273 <?php esc_html_e( 'Learn More', 'jetpack' ); ?>
274 </a>
275 </p>
276 </div>
277 <script>
278 jQuery( function( $ ) {
279 $( '.jetpack-protect-warning' ).on( 'click', 'button.notice-dismiss', function( event ) {
280 event.preventDefault();
281
282 wp.ajax.post(
283 'jetpack-protect-dismiss-multisite-banner',
284 {
285 _wpnonce: $( event.delegateTarget ).data( 'dismiss-nonce' ),
286 }
287 ).fail( function( error ) {
288 <?php
289 // A failure here is really strange, and there's not really anything a site owner can do to fix one.
290 // Just log the error for now to help debugging.
291 ?>
292
293 if ( 'function' === typeof error.done && '-1' === error.responseText ) {
294 console.error( 'Notice dismissal failed: check_ajax_referer' );
295 } else {
296 console.error( 'Notice dismissal failed: ' + JSON.stringify( error ) );
297 }
298 } )
299 } );
300 } );
301 </script>
302 <?php
303 }
304
305 /**
306 * Request an api key from wordpress.com
307 *
308 * @return bool | string
309 */
310 public function get_protect_key() {
311
312 $protect_blog_id = self::get_main_blog_jetpack_id();
313
314 // If we can't find the the blog id, that means we are on multisite, and the main site never connected
315 // the protect api key is linked to the main blog id - instruct the user to connect their main blog.
316 if ( ! $protect_blog_id ) {
317 $this->api_key_error = __( 'Your main blog is not connected to WordPress.com. Please connect to get an API key.', 'jetpack' );
318
319 return false;
320 }
321
322 $request = array(
323 'jetpack_blog_id' => $protect_blog_id,
324 'bruteprotect_api_key' => get_site_option( 'bruteprotect_api_key' ),
325 'multisite' => '0',
326 );
327
328 // Send the number of blogs on the network if we are on multisite.
329 if ( is_multisite() ) {
330 $request['multisite'] = get_blog_count();
331 if ( ! $request['multisite'] ) {
332 global $wpdb;
333 $request['multisite'] = $wpdb->get_var( "SELECT COUNT(blog_id) as c FROM $wpdb->blogs WHERE spam = '0' AND deleted = '0' and archived = '0'" );
334 }
335 }
336
337 // Request the key.
338 $xml = new Jetpack_IXR_Client();
339 $xml->query( 'jetpack.protect.requestKey', $request );
340
341 // Hmm, can't talk to wordpress.com.
342 if ( $xml->isError() ) {
343 $code = $xml->getErrorCode();
344 $message = $xml->getErrorMessage();
345 // Translators: The xml error code, and the xml error message.
346 $this->api_key_error = sprintf( __( 'Error connecting to WordPress.com. Code: %1$s, %2$s', 'jetpack' ), $code, $message );
347
348 return false;
349 }
350
351 $response = $xml->getResponse();
352
353 // Hmm, can't talk to the protect servers ( api.bruteprotect.com ).
354 if ( ! isset( $response['data'] ) ) {
355 $this->api_key_error = __( 'No reply from Jetpack servers', 'jetpack' );
356
357 return false;
358 }
359
360 // There was an issue generating the key.
361 if ( empty( $response['success'] ) ) {
362 $this->api_key_error = $response['data'];
363
364 return false;
365 }
366
367 // Key generation successful!
368 $active_plugins = Jetpack::get_active_plugins();
369
370 // We only want to deactivate BruteProtect if we successfully get a key.
371 if ( in_array( 'bruteprotect/bruteprotect.php', $active_plugins, true ) ) {
372 Jetpack_Client_Server::deactivate_plugin( 'bruteprotect/bruteprotect.php', 'BruteProtect' );
373 }
374
375 $key = $response['data'];
376 update_site_option( 'jetpack_protect_key', $key );
377
378 return $key;
379 }
380
381 /**
382 * Called via WP action wp_login_failed to log failed attempt with the api
383 *
384 * Fires custom, plugable action jpp_log_failed_attempt with the IP
385 *
386 * @param string $login_user - the user attempting to log in.
387 * @return void
388 */
389 public function log_failed_attempt( $login_user = null ) {
390
391 /**
392 * Fires before every failed login attempt.
393 *
394 * @module protect
395 *
396 * @since 3.4.0
397 *
398 * @param array Information about failed login attempt
399 * [
400 * 'login' => (string) Username or email used in failed login attempt
401 * ]
402 */
403 do_action( 'jpp_log_failed_attempt', array( 'login' => $login_user ) );
404
405 if ( isset( $_COOKIE['jpp_math_pass'] ) ) {
406
407 $transient = $this->get_transient( 'jpp_math_pass_' . sanitize_key( $_COOKIE['jpp_math_pass'] ) );
408 --$transient;
409
410 if ( ! $transient || $transient < 1 ) {
411 $this->delete_transient( 'jpp_math_pass_' . sanitize_key( $_COOKIE['jpp_math_pass'] ) );
412 setcookie( 'jpp_math_pass', 0, time() - DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
413 } else {
414 $this->set_transient( 'jpp_math_pass_' . sanitize_key( $_COOKIE['jpp_math_pass'] ), $transient, DAY_IN_SECONDS );
415 }
416 }
417 $this->protect_call( 'failed_attempt' );
418 }
419
420 /**
421 * Set up the Protect configuration page
422 */
423 public function modules_loaded() {
424 Jetpack::enable_module_configurable( __FILE__ );
425 }
426
427 /**
428 * Logs a successful login back to our servers, this allows us to make sure we're not blocking
429 * a busy IP that has a lot of good logins along with some forgotten passwords. Also saves current user's ip
430 * to the ip address whitelist
431 *
432 * @param string $user_login - the user loggign in.
433 * @param string $user - the user.
434 */
435 public function log_successful_login( $user_login, $user = null ) {
436 if ( ! $user ) { // For do_action( 'wp_login' ) calls that lacked passing the 2nd arg.
437 $user = get_user_by( 'login', $user_login );
438 }
439
440 $this->protect_call( 'successful_login', array( 'roles' => $user->roles ) );
441 }
442
443 /**
444 * Checks for loginability BEFORE authentication so that bots don't get to go around the log in form.
445 *
446 * If we are using our math fallback, authenticate via math-fallback.php
447 *
448 * @param string $user - the user.
449 * @param string $username - the username.
450 * @param string $password - the password.
451 *
452 * @return string $user
453 */
454 public function check_preauth( $user = 'Not Used By Protect', $username = 'Not Used By Protect', $password = 'Not Used By Protect' ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
455 $allow_login = $this->check_login_ability( true );
456 $use_math = $this->get_transient( 'brute_use_math' );
457
458 if ( ! $allow_login ) {
459 $this->block_with_math();
460 }
461
462 if ( ( 1 == $use_math || 1 == $this->block_login_with_math ) && isset( $_POST['log'] ) ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual, WordPress.Security.NonceVerification.Missing -- POST request just determines if we use math authentication.
463 include_once __DIR__ . '/protect/math-fallback.php';
464 Jetpack_Protect_Math_Authenticate::math_authenticate();
465 }
466
467 return $user;
468 }
469
470 /**
471 * Get all IP headers so that we can process on our server...
472 *
473 * @return array
474 */
475 public function get_headers() {
476 $output = array();
477 $ip_related_headers = array(
478 'GD_PHP_HANDLER',
479 'HTTP_AKAMAI_ORIGIN_HOP',
480 'HTTP_CF_CONNECTING_IP',
481 'HTTP_CLIENT_IP',
482 'HTTP_FASTLY_CLIENT_IP',
483 'HTTP_FORWARDED',
484 'HTTP_FORWARDED_FOR',
485 'HTTP_INCAP_CLIENT_IP',
486 'HTTP_TRUE_CLIENT_IP',
487 'HTTP_X_CLIENTIP',
488 'HTTP_X_CLUSTER_CLIENT_IP',
489 'HTTP_X_FORWARDED',
490 'HTTP_X_FORWARDED_FOR',
491 'HTTP_X_IP_TRAIL',
492 'HTTP_X_REAL_IP',
493 'HTTP_X_VARNISH',
494 'REMOTE_ADDR',
495 );
496
497 foreach ( $ip_related_headers as $header ) {
498 if ( ! empty( $_SERVER[ $header ] ) ) {
499 $output[ $header ] = wp_unslash( $_SERVER[ $header ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
500 }
501 }
502
503 return $output;
504 }
505
506 /**
507 * Checks if the IP address has been whitelisted
508 *
509 * @param string $ip - the IP address.
510 *
511 * @return bool
512 */
513 public function ip_is_whitelisted( $ip ) {
514 // If we found an exact match in wp-config.
515 if ( defined( 'JETPACK_IP_ADDRESS_OK' ) && JETPACK_IP_ADDRESS_OK === $ip ) {
516 return true;
517 }
518
519 $whitelist = jetpack_protect_get_local_whitelist();
520
521 if ( is_multisite() ) {
522 $whitelist = array_merge( $whitelist, get_site_option( 'jetpack_protect_global_whitelist', array() ) );
523 }
524
525 if ( ! empty( $whitelist ) ) :
526 foreach ( $whitelist as $item ) :
527 // If the IPs are an exact match.
528 if ( ! $item->range && isset( $item->ip_address ) && $item->ip_address === $ip ) {
529 return true;
530 }
531
532 if ( $item->range && isset( $item->range_low ) && isset( $item->range_high ) ) {
533 if ( jetpack_protect_ip_address_is_in_range( $ip, $item->range_low, $item->range_high ) ) {
534 return true;
535 }
536 }
537 endforeach;
538 endif;
539
540 return false;
541 }
542
543 /**
544 * Checks the status for a given IP. API results are cached as transients
545 *
546 * @param bool $preauth - Whether or not we are checking prior to authorization.
547 *
548 * @return bool Either returns true, fires $this->kill_login, or includes a math fallback and returns false
549 */
550 public function check_login_ability( $preauth = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
551
552 /**
553 * JETPACK_ALWAYS_PROTECT_LOGIN will always disable the login page, and use a page provided by Jetpack.
554 */
555 if ( Constants::is_true( 'JETPACK_ALWAYS_PROTECT_LOGIN' ) ) {
556 $this->kill_login();
557 }
558
559 if ( $this->is_current_ip_whitelisted() ) {
560 return true;
561 }
562
563 $status = $this->get_cached_status();
564
565 if ( empty( $status ) ) {
566 // If we've reached this point, this means that the IP isn't cached.
567 // Now we check with the Protect API to see if we should allow login.
568 $response = $this->protect_call( $action = 'check_ip' ); // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found
569
570 if ( isset( $response['math'] ) && ! function_exists( 'brute_math_authenticate' ) ) {
571 include_once __DIR__ . '/protect/math-fallback.php';
572 new Jetpack_Protect_Math_Authenticate();
573
574 return false;
575 }
576
577 $status = $response['status'];
578 }
579
580 if ( 'blocked' === $status ) {
581 $this->block_with_math();
582 }
583
584 if ( 'blocked-hard' === $status ) {
585 $this->kill_login();
586 }
587
588 return true;
589 }
590
591 /**
592 * Check if IP is whitelisted.
593 */
594 public function is_current_ip_whitelisted() {
595 $ip = jetpack_protect_get_ip();
596
597 // Server is misconfigured and we can't get an IP.
598 if ( ! $ip && class_exists( 'Jetpack' ) ) {
599 Jetpack::deactivate_module( 'protect' );
600 ob_start();
601 Jetpack::state( 'message', 'protect_misconfigured_ip' );
602 ob_end_clean();
603 return true;
604 }
605
606 /**
607 * Short-circuit check_login_ability.
608 *
609 * If there is an alternate way to validate the current IP such as
610 * a hard-coded list of IP addresses, we can short-circuit the rest
611 * of the login ability checks and return true here.
612 *
613 * @module protect
614 *
615 * @since 4.4.0
616 *
617 * @param bool false Should we allow all logins for the current ip? Default: false
618 */
619 if ( apply_filters( 'jpp_allow_login', false, $ip ) ) {
620 return true;
621 }
622
623 if ( jetpack_protect_ip_is_private( $ip ) ) {
624 return true;
625 }
626
627 if ( $this->ip_is_whitelisted( $ip ) ) {
628 return true;
629 }
630 }
631
632 /**
633 * Check if someone is able to login based on IP.
634 */
635 public function has_login_ability() {
636 if ( $this->is_current_ip_whitelisted() ) {
637 return true;
638 }
639 $status = $this->get_cached_status();
640 if ( empty( $status ) || 'ok' === $status ) {
641 return true;
642 }
643 return false;
644 }
645
646 /**
647 * Check the status of the cached transient.
648 */
649 public function get_cached_status() {
650 $transient_name = $this->get_transient_name();
651 $value = $this->get_transient( $transient_name );
652 if ( isset( $value['status'] ) ) {
653 return $value['status'];
654 }
655 return '';
656 }
657
658 /**
659 * Check if we need to block with a math question to continue logging in.
660 */
661 public function block_with_math() {
662 /**
663 * By default, Protect will allow a user who has been blocked for too
664 * many failed logins to start answering math questions to continue logging in
665 *
666 * For added security, you can disable this.
667 *
668 * @module protect
669 *
670 * @since 3.6.0
671 *
672 * @param bool Whether to allow math for blocked users or not.
673 */
674
675 $this->block_login_with_math = 1;
676 /**
677 * Allow Math fallback for blocked IPs.
678 *
679 * @module protect
680 *
681 * @since 3.6.0
682 *
683 * @param bool true Should we fallback to the Math questions when an IP is blocked. Default to true.
684 */
685 $allow_math_fallback_on_fail = apply_filters( 'jpp_use_captcha_when_blocked', true );
686 if ( ! $allow_math_fallback_on_fail ) {
687 $this->kill_login();
688 }
689 include_once __DIR__ . '/protect/math-fallback.php';
690 new Jetpack_Protect_Math_Authenticate();
691
692 return false;
693 }
694
695 /**
696 * Kill a login attempt
697 */
698 public function kill_login() {
699 if (
700 isset( $_GET['action'], $_GET['_wpnonce'] ) &&
701 'logout' === $_GET['action'] &&
702 wp_verify_nonce( $_GET['_wpnonce'], 'log-out' ) && // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
703 wp_get_current_user()
704
705 ) {
706 // Allow users to logout.
707 return;
708 }
709
710 $ip = jetpack_protect_get_ip();
711 /**
712 * Fires before every killed login.
713 *
714 * @module protect
715 *
716 * @since 3.4.0
717 *
718 * @param string $ip IP flagged by Protect.
719 */
720 do_action( 'jpp_kill_login', $ip );
721
722 if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
723 // translators: variable is the IP address that was flagged.
724 $die_string = sprintf( __( 'Your IP (%1$s) has been flagged for potential security violations.', 'jetpack' ), str_replace( 'http://', '', esc_url( 'http://' . $ip ) ) );
725 wp_die(
726 $die_string, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_url used when forming string.
727 esc_html__( 'Login Blocked by Jetpack', 'jetpack' ),
728 array( 'response' => 403 )
729 );
730 }
731
732 require_once __DIR__ . '/protect/blocked-login-page.php';
733 $blocked_login_page = Jetpack_Protect_Blocked_Login_Page::instance( $ip );
734
735 if ( $blocked_login_page->is_blocked_user_valid() ) {
736 return;
737 }
738
739 $blocked_login_page->render_and_die();
740 }
741
742 /**
743 * Checks if the protect API call has failed, and if so initiates the math captcha fallback.
744 */
745 public function check_use_math() {
746 $use_math = $this->get_transient( 'brute_use_math' );
747 if ( $use_math ) {
748 include_once __DIR__ . '/protect/math-fallback.php';
749 new Jetpack_Protect_Math_Authenticate();
750 }
751 }
752
753 /**
754 * If we're in a multisite network, return the blog ID of the primary blog
755 *
756 * @return int
757 */
758 public function get_main_blog_id() {
759 if ( ! is_multisite() ) {
760 return false;
761 }
762
763 global $current_site;
764 $primary_blog_id = $current_site->blog_id;
765
766 return $primary_blog_id;
767 }
768
769 /**
770 * Get jetpack blog id, or the jetpack blog id of the main blog in the main network
771 *
772 * @return int
773 */
774 public function get_main_blog_jetpack_id() {
775 if ( ! is_main_site() ) {
776 switch_to_blog( $this->get_main_blog_id() );
777 $id = Jetpack::get_option( 'id', false );
778 restore_current_blog();
779 } else {
780 $id = Jetpack::get_option( 'id' );
781 }
782
783 return $id;
784 }
785
786 /**
787 * Checks the API key.
788 */
789 public function check_api_key() {
790 $response = $this->protect_call( 'check_key' );
791
792 if ( isset( $response['ckval'] ) ) {
793 return true;
794 }
795
796 if ( isset( $response['error'] ) ) {
797
798 if ( 'Invalid API Key' === $response['error'] ) {
799 $this->api_key_error = __( 'Your API key is invalid', 'jetpack' );
800 }
801
802 if ( 'API Key Required' === $response['error'] ) {
803 $this->api_key_error = __( 'No API key', 'jetpack' );
804 }
805 }
806
807 $this->api_key_error = __( 'There was an error contacting Jetpack servers.', 'jetpack' );
808
809 return false;
810 }
811
812 /**
813 * Calls over to the api using wp_remote_post
814 *
815 * @param string $action - 'check_ip', 'check_key', or 'failed_attempt'.
816 * @param array $request - Any custom data to post to the api.
817 *
818 * @return array
819 */
820 public function protect_call( $action = 'check_ip', $request = array() ) {
821 global $wp_version;
822
823 $api_key = $this->maybe_get_protect_key();
824
825 $user_agent = "WordPress/{$wp_version} | Jetpack/" . constant( 'JETPACK__VERSION' );
826
827 $request['action'] = $action;
828 $request['ip'] = jetpack_protect_get_ip();
829 $request['host'] = $this->get_local_host();
830 $request['headers'] = wp_json_encode( $this->get_headers() );
831 $request['jetpack_version'] = constant( 'JETPACK__VERSION' );
832 $request['wordpress_version'] = (string) $wp_version;
833 $request['api_key'] = $api_key;
834 $request['multisite'] = '0';
835
836 if ( is_multisite() ) {
837 $request['multisite'] = get_blog_count();
838 }
839
840 /**
841 * Filter controls maximum timeout in waiting for reponse from Protect servers.
842 *
843 * @module protect
844 *
845 * @since 4.0.4
846 *
847 * @param int $timeout Max time (in seconds) to wait for a response.
848 */
849 $timeout = apply_filters( 'jetpack_protect_connect_timeout', 30 );
850
851 $args = array(
852 'body' => $request,
853 'user-agent' => $user_agent,
854 'httpversion' => '1.0',
855 'timeout' => absint( $timeout ),
856 );
857
858 $response_json = wp_remote_post( JETPACK_PROTECT__API_HOST, $args );
859 $this->last_response_raw = $response_json;
860
861 $transient_name = $this->get_transient_name();
862 $this->delete_transient( $transient_name );
863
864 if ( is_array( $response_json ) ) {
865 $response = json_decode( $response_json['body'], true );
866 }
867
868 if ( isset( $response['blocked_attempts'] ) && $response['blocked_attempts'] ) {
869 update_site_option( 'jetpack_protect_blocked_attempts', $response['blocked_attempts'] );
870 }
871
872 if ( isset( $response['status'] ) && ! isset( $response['error'] ) ) {
873 $response['expire'] = time() + $response['seconds_remaining'];
874 $this->set_transient( $transient_name, $response, $response['seconds_remaining'] );
875 $this->delete_transient( 'brute_use_math' );
876 } else { // Fallback to Math Captcha if no response from API host.
877 $this->set_transient( 'brute_use_math', 1, 600 );
878 $response['status'] = 'ok';
879 $response['math'] = true;
880 }
881
882 if ( isset( $response['error'] ) ) {
883 update_site_option( 'jetpack_protect_error', $response['error'] );
884 } else {
885 delete_site_option( 'jetpack_protect_error' );
886 }
887
888 return $response;
889 }
890
891 /**
892 * Gets the transient name.
893 */
894 public function get_transient_name() {
895 $headers = $this->get_headers();
896 $header_hash = md5( wp_json_encode( $headers ) );
897
898 return 'jpp_li_' . $header_hash;
899 }
900
901 /**
902 * Wrapper for WordPress set_transient function, our version sets
903 * the transient on the main site in the network if this is a multisite network
904 *
905 * We do it this way (instead of set_site_transient) because of an issue where
906 * sitewide transients are always autoloaded
907 * https://core.trac.wordpress.org/ticket/22846
908 *
909 * @param string $transient Transient name. Expected to not be SQL-escaped. Must be
910 * 45 characters or fewer in length.
911 * @param mixed $value Transient value. Must be serializable if non-scalar.
912 * Expected to not be SQL-escaped.
913 * @param int $expiration Optional. Time until expiration in seconds. Default 0.
914 *
915 * @return bool False if value was not set and true if value was set.
916 */
917 public function set_transient( $transient, $value, $expiration ) {
918 if ( is_multisite() && ! is_main_site() ) {
919 switch_to_blog( $this->get_main_blog_id() );
920 $return = set_transient( $transient, $value, $expiration );
921 restore_current_blog();
922
923 return $return;
924 }
925
926 return set_transient( $transient, $value, $expiration );
927 }
928
929 /**
930 * Wrapper for WordPress delete_transient function, our version deletes
931 * the transient on the main site in the network if this is a multisite network
932 *
933 * @param string $transient Transient name. Expected to not be SQL-escaped.
934 *
935 * @return bool true if successful, false otherwise
936 */
937 public function delete_transient( $transient ) {
938 if ( is_multisite() && ! is_main_site() ) {
939 switch_to_blog( $this->get_main_blog_id() );
940 $return = delete_transient( $transient );
941 restore_current_blog();
942
943 return $return;
944 }
945
946 return delete_transient( $transient );
947 }
948
949 /**
950 * Wrapper for WordPress get_transient function, our version gets
951 * the transient on the main site in the network if this is a multisite network
952 *
953 * @param string $transient Transient name. Expected to not be SQL-escaped.
954 *
955 * @return mixed Value of transient.
956 */
957 public function get_transient( $transient ) {
958 if ( is_multisite() && ! is_main_site() ) {
959 switch_to_blog( $this->get_main_blog_id() );
960 $return = get_transient( $transient );
961 restore_current_blog();
962
963 return $return;
964 }
965
966 return get_transient( $transient );
967 }
968
969 /**
970 * Returns the local host.
971 */
972 public function get_local_host() {
973 if ( isset( $this->local_host ) ) {
974 return $this->local_host;
975 }
976
977 $uri = 'http://' . strtolower( isset( $_SERVER['HTTP_HOST'] ) ? filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '' );
978
979 if ( is_multisite() ) {
980 $uri = network_home_url();
981 }
982
983 $uridata = wp_parse_url( $uri );
984
985 $domain = $uridata['host'];
986
987 // If we still don't have the site_url, get it.
988 if ( ! $domain ) {
989 $uri = get_site_url( 1 );
990 $uridata = wp_parse_url( $uri );
991 $domain = $uridata['host'];
992 }
993
994 $this->local_host = $domain;
995
996 return $this->local_host;
997 }
998
999 }
1000
1001 $jetpack_protect = Jetpack_Protect_Module::instance();
1002
1003 global $pagenow;
1004 if ( isset( $pagenow ) && 'wp-login.php' === $pagenow ) {
1005 $jetpack_protect->check_login_ability();
1006 }
1007