PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 11.7.2
Jetpack – WP Security, Backup, Speed, & Growth v11.7.2
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.7.2, at modules/protect.php

1,008 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 /**
216 * Handle discplaying a security warning.
217 */
218 public function maybe_display_security_warning() {
219 if ( is_multisite() && current_user_can( 'manage_network' ) ) {
220 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
221 require_once ABSPATH . '/wp-admin/includes/plugin.php';
222 }
223
224 if ( ! is_plugin_active_for_network( plugin_basename( JETPACK__PLUGIN_FILE ) ) ) {
225 add_action( 'load-index.php', array( $this, 'prepare_jetpack_protect_multisite_notice' ) );
226 add_action( 'wp_ajax_jetpack-protect-dismiss-multisite-banner', array( $this, 'ajax_dismiss_handler' ) );
227 }
228 }
229 }
230
231 /**
232 * Handles preparing the multisite notice.
233 */
234 public function prepare_jetpack_protect_multisite_notice() {
235 $dismissed = get_site_option( 'jetpack_dismissed_protect_multisite_banner' );
236 if ( $dismissed ) {
237 return;
238 }
239
240 add_action( 'admin_notices', array( $this, 'admin_jetpack_manage_notice' ) );
241 }
242
243 /**
244 * Handle dismissing the multisite banner.
245 */
246 public function ajax_dismiss_handler() {
247 check_ajax_referer( 'jetpack_protect_multisite_banner_opt_out' );
248
249 if ( ! current_user_can( 'manage_network' ) ) {
250 wp_send_json_error( new WP_Error( 'insufficient_permissions' ) );
251 }
252
253 update_site_option( 'jetpack_dismissed_protect_multisite_banner', true );
254
255 wp_send_json_success();
256 }
257
258 /**
259 * Displays a warning about Jetpack Protect's network activation requirement.
260 * Attaches some custom JS to Core's `is-dismissible` UI to save the dismissed state.
261 */
262 public function admin_jetpack_manage_notice() {
263 ?>
264 <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' ) ); ?>">
265 <h2><?php esc_html_e( 'Jetpack Brute Force Attack Prevention cannot keep your site secure', 'jetpack' ); ?></h2>
266
267 <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>
268
269 <p>
270 <a class="button-primary" href="<?php echo esc_url( network_admin_url( 'plugins.php' ) ); ?>">
271 <?php esc_html_e( 'View Network Admin', 'jetpack' ); ?>
272 </a>
273 <a class="button" href="<?php echo esc_url( __( 'https://jetpack.com/support/multisite-protect', 'jetpack' ) ); ?>" target="_blank">
274 <?php esc_html_e( 'Learn More', 'jetpack' ); ?>
275 </a>
276 </p>
277 </div>
278 <script>
279 jQuery( function( $ ) {
280 $( '.jetpack-protect-warning' ).on( 'click', 'button.notice-dismiss', function( event ) {
281 event.preventDefault();
282
283 wp.ajax.post(
284 'jetpack-protect-dismiss-multisite-banner',
285 {
286 _wpnonce: $( event.delegateTarget ).data( 'dismiss-nonce' ),
287 }
288 ).fail( function( error ) {
289 <?php
290 // A failure here is really strange, and there's not really anything a site owner can do to fix one.
291 // Just log the error for now to help debugging.
292 ?>
293
294 if ( 'function' === typeof error.done && '-1' === error.responseText ) {
295 console.error( 'Notice dismissal failed: check_ajax_referer' );
296 } else {
297 console.error( 'Notice dismissal failed: ' + JSON.stringify( error ) );
298 }
299 } )
300 } );
301 } );
302 </script>
303 <?php
304 }
305
306 /**
307 * Request an api key from wordpress.com
308 *
309 * @return bool | string
310 */
311 public function get_protect_key() {
312
313 $protect_blog_id = self::get_main_blog_jetpack_id();
314
315 // If we can't find the the blog id, that means we are on multisite, and the main site never connected
316 // the protect api key is linked to the main blog id - instruct the user to connect their main blog.
317 if ( ! $protect_blog_id ) {
318 $this->api_key_error = __( 'Your main blog is not connected to WordPress.com. Please connect to get an API key.', 'jetpack' );
319
320 return false;
321 }
322
323 $request = array(
324 'jetpack_blog_id' => $protect_blog_id,
325 'bruteprotect_api_key' => get_site_option( 'bruteprotect_api_key' ),
326 'multisite' => '0',
327 );
328
329 // Send the number of blogs on the network if we are on multisite.
330 if ( is_multisite() ) {
331 $request['multisite'] = get_blog_count();
332 if ( ! $request['multisite'] ) {
333 global $wpdb;
334 $request['multisite'] = $wpdb->get_var( "SELECT COUNT(blog_id) as c FROM $wpdb->blogs WHERE spam = '0' AND deleted = '0' and archived = '0'" );
335 }
336 }
337
338 // Request the key.
339 $xml = new Jetpack_IXR_Client();
340 $xml->query( 'jetpack.protect.requestKey', $request );
341
342 // Hmm, can't talk to wordpress.com.
343 if ( $xml->isError() ) {
344 $code = $xml->getErrorCode();
345 $message = $xml->getErrorMessage();
346 // Translators: The xml error code, and the xml error message.
347 $this->api_key_error = sprintf( __( 'Error connecting to WordPress.com. Code: %1$s, %2$s', 'jetpack' ), $code, $message );
348
349 return false;
350 }
351
352 $response = $xml->getResponse();
353
354 // Hmm, can't talk to the protect servers ( api.bruteprotect.com ).
355 if ( ! isset( $response['data'] ) ) {
356 $this->api_key_error = __( 'No reply from Jetpack servers', 'jetpack' );
357
358 return false;
359 }
360
361 // There was an issue generating the key.
362 if ( empty( $response['success'] ) ) {
363 $this->api_key_error = $response['data'];
364
365 return false;
366 }
367
368 // Key generation successful!
369 $active_plugins = Jetpack::get_active_plugins();
370
371 // We only want to deactivate BruteProtect if we successfully get a key.
372 if ( in_array( 'bruteprotect/bruteprotect.php', $active_plugins, true ) ) {
373 Jetpack_Client_Server::deactivate_plugin( 'bruteprotect/bruteprotect.php', 'BruteProtect' );
374 }
375
376 $key = $response['data'];
377 update_site_option( 'jetpack_protect_key', $key );
378
379 return $key;
380 }
381
382 /**
383 * Called via WP action wp_login_failed to log failed attempt with the api
384 *
385 * Fires custom, plugable action jpp_log_failed_attempt with the IP
386 *
387 * @param string $login_user - the user attempting to log in.
388 * @return void
389 */
390 public function log_failed_attempt( $login_user = null ) {
391
392 /**
393 * Fires before every failed login attempt.
394 *
395 * @module protect
396 *
397 * @since 3.4.0
398 *
399 * @param array Information about failed login attempt
400 * [
401 * 'login' => (string) Username or email used in failed login attempt
402 * ]
403 */
404 do_action( 'jpp_log_failed_attempt', array( 'login' => $login_user ) );
405
406 if ( isset( $_COOKIE['jpp_math_pass'] ) ) {
407
408 $transient = $this->get_transient( 'jpp_math_pass_' . sanitize_key( $_COOKIE['jpp_math_pass'] ) );
409 $transient--;
410
411 if ( ! $transient || $transient < 1 ) {
412 $this->delete_transient( 'jpp_math_pass_' . sanitize_key( $_COOKIE['jpp_math_pass'] ) );
413 setcookie( 'jpp_math_pass', 0, time() - DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
414 } else {
415 $this->set_transient( 'jpp_math_pass_' . sanitize_key( $_COOKIE['jpp_math_pass'] ), $transient, DAY_IN_SECONDS );
416 }
417 }
418 $this->protect_call( 'failed_attempt' );
419 }
420
421 /**
422 * Set up the Protect configuration page
423 */
424 public function modules_loaded() {
425 Jetpack::enable_module_configurable( __FILE__ );
426 }
427
428 /**
429 * Logs a successful login back to our servers, this allows us to make sure we're not blocking
430 * a busy IP that has a lot of good logins along with some forgotten passwords. Also saves current user's ip
431 * to the ip address whitelist
432 *
433 * @param string $user_login - the user loggign in.
434 * @param string $user - the user.
435 */
436 public function log_successful_login( $user_login, $user = null ) {
437 if ( ! $user ) { // For do_action( 'wp_login' ) calls that lacked passing the 2nd arg.
438 $user = get_user_by( 'login', $user_login );
439 }
440
441 $this->protect_call( 'successful_login', array( 'roles' => $user->roles ) );
442 }
443
444 /**
445 * Checks for loginability BEFORE authentication so that bots don't get to go around the log in form.
446 *
447 * If we are using our math fallback, authenticate via math-fallback.php
448 *
449 * @param string $user - the user.
450 * @param string $username - the username.
451 * @param string $password - the password.
452 *
453 * @return string $user
454 */
455 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
456 $allow_login = $this->check_login_ability( true );
457 $use_math = $this->get_transient( 'brute_use_math' );
458
459 if ( ! $allow_login ) {
460 $this->block_with_math();
461 }
462
463 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.
464 include_once __DIR__ . '/protect/math-fallback.php';
465 Jetpack_Protect_Math_Authenticate::math_authenticate();
466 }
467
468 return $user;
469 }
470
471 /**
472 * Get all IP headers so that we can process on our server...
473 *
474 * @return array
475 */
476 public function get_headers() {
477 $output = array();
478 $ip_related_headers = array(
479 'GD_PHP_HANDLER',
480 'HTTP_AKAMAI_ORIGIN_HOP',
481 'HTTP_CF_CONNECTING_IP',
482 'HTTP_CLIENT_IP',
483 'HTTP_FASTLY_CLIENT_IP',
484 'HTTP_FORWARDED',
485 'HTTP_FORWARDED_FOR',
486 'HTTP_INCAP_CLIENT_IP',
487 'HTTP_TRUE_CLIENT_IP',
488 'HTTP_X_CLIENTIP',
489 'HTTP_X_CLUSTER_CLIENT_IP',
490 'HTTP_X_FORWARDED',
491 'HTTP_X_FORWARDED_FOR',
492 'HTTP_X_IP_TRAIL',
493 'HTTP_X_REAL_IP',
494 'HTTP_X_VARNISH',
495 'REMOTE_ADDR',
496 );
497
498 foreach ( $ip_related_headers as $header ) {
499 if ( ! empty( $_SERVER[ $header ] ) ) {
500 $output[ $header ] = wp_unslash( $_SERVER[ $header ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
501 }
502 }
503
504 return $output;
505 }
506
507 /**
508 * Checks if the IP address has been whitelisted
509 *
510 * @param string $ip - the IP address.
511 *
512 * @return bool
513 */
514 public function ip_is_whitelisted( $ip ) {
515 // If we found an exact match in wp-config.
516 if ( defined( 'JETPACK_IP_ADDRESS_OK' ) && JETPACK_IP_ADDRESS_OK === $ip ) {
517 return true;
518 }
519
520 $whitelist = jetpack_protect_get_local_whitelist();
521
522 if ( is_multisite() ) {
523 $whitelist = array_merge( $whitelist, get_site_option( 'jetpack_protect_global_whitelist', array() ) );
524 }
525
526 if ( ! empty( $whitelist ) ) :
527 foreach ( $whitelist as $item ) :
528 // If the IPs are an exact match.
529 if ( ! $item->range && isset( $item->ip_address ) && $item->ip_address === $ip ) {
530 return true;
531 }
532
533 if ( $item->range && isset( $item->range_low ) && isset( $item->range_high ) ) {
534 if ( jetpack_protect_ip_address_is_in_range( $ip, $item->range_low, $item->range_high ) ) {
535 return true;
536 }
537 }
538 endforeach;
539 endif;
540
541 return false;
542 }
543
544 /**
545 * Checks the status for a given IP. API results are cached as transients
546 *
547 * @param bool $preauth - Whether or not we are checking prior to authorization.
548 *
549 * @return bool Either returns true, fires $this->kill_login, or includes a math fallback and returns false
550 */
551 public function check_login_ability( $preauth = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
552
553 /**
554 * JETPACK_ALWAYS_PROTECT_LOGIN will always disable the login page, and use a page provided by Jetpack.
555 */
556 if ( Constants::is_true( 'JETPACK_ALWAYS_PROTECT_LOGIN' ) ) {
557 $this->kill_login();
558 }
559
560 if ( $this->is_current_ip_whitelisted() ) {
561 return true;
562 }
563
564 $status = $this->get_cached_status();
565
566 if ( empty( $status ) ) {
567 // If we've reached this point, this means that the IP isn't cached.
568 // Now we check with the Protect API to see if we should allow login.
569 $response = $this->protect_call( $action = 'check_ip' ); // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found
570
571 if ( isset( $response['math'] ) && ! function_exists( 'brute_math_authenticate' ) ) {
572 include_once __DIR__ . '/protect/math-fallback.php';
573 new Jetpack_Protect_Math_Authenticate();
574
575 return false;
576 }
577
578 $status = $response['status'];
579 }
580
581 if ( 'blocked' === $status ) {
582 $this->block_with_math();
583 }
584
585 if ( 'blocked-hard' === $status ) {
586 $this->kill_login();
587 }
588
589 return true;
590 }
591
592 /**
593 * Check if IP is whitelisted.
594 */
595 public function is_current_ip_whitelisted() {
596 $ip = jetpack_protect_get_ip();
597
598 // Server is misconfigured and we can't get an IP.
599 if ( ! $ip && class_exists( 'Jetpack' ) ) {
600 Jetpack::deactivate_module( 'protect' );
601 ob_start();
602 Jetpack::state( 'message', 'protect_misconfigured_ip' );
603 ob_end_clean();
604 return true;
605 }
606
607 /**
608 * Short-circuit check_login_ability.
609 *
610 * If there is an alternate way to validate the current IP such as
611 * a hard-coded list of IP addresses, we can short-circuit the rest
612 * of the login ability checks and return true here.
613 *
614 * @module protect
615 *
616 * @since 4.4.0
617 *
618 * @param bool false Should we allow all logins for the current ip? Default: false
619 */
620 if ( apply_filters( 'jpp_allow_login', false, $ip ) ) {
621 return true;
622 }
623
624 if ( jetpack_protect_ip_is_private( $ip ) ) {
625 return true;
626 }
627
628 if ( $this->ip_is_whitelisted( $ip ) ) {
629 return true;
630 }
631 }
632
633 /**
634 * Check if someone is able to login based on IP.
635 */
636 public function has_login_ability() {
637 if ( $this->is_current_ip_whitelisted() ) {
638 return true;
639 }
640 $status = $this->get_cached_status();
641 if ( empty( $status ) || 'ok' === $status ) {
642 return true;
643 }
644 return false;
645 }
646
647 /**
648 * Check the status of the cached transient.
649 */
650 public function get_cached_status() {
651 $transient_name = $this->get_transient_name();
652 $value = $this->get_transient( $transient_name );
653 if ( isset( $value['status'] ) ) {
654 return $value['status'];
655 }
656 return '';
657 }
658
659 /**
660 * Check if we need to block with a math question to continue logging in.
661 */
662 public function block_with_math() {
663 /**
664 * By default, Protect will allow a user who has been blocked for too
665 * many failed logins to start answering math questions to continue logging in
666 *
667 * For added security, you can disable this.
668 *
669 * @module protect
670 *
671 * @since 3.6.0
672 *
673 * @param bool Whether to allow math for blocked users or not.
674 */
675
676 $this->block_login_with_math = 1;
677 /**
678 * Allow Math fallback for blocked IPs.
679 *
680 * @module protect
681 *
682 * @since 3.6.0
683 *
684 * @param bool true Should we fallback to the Math questions when an IP is blocked. Default to true.
685 */
686 $allow_math_fallback_on_fail = apply_filters( 'jpp_use_captcha_when_blocked', true );
687 if ( ! $allow_math_fallback_on_fail ) {
688 $this->kill_login();
689 }
690 include_once __DIR__ . '/protect/math-fallback.php';
691 new Jetpack_Protect_Math_Authenticate();
692
693 return false;
694 }
695
696 /**
697 * Kill a login attempt
698 */
699 public function kill_login() {
700 if (
701 isset( $_GET['action'], $_GET['_wpnonce'] ) &&
702 'logout' === $_GET['action'] &&
703 wp_verify_nonce( $_GET['_wpnonce'], 'log-out' ) && // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
704 wp_get_current_user()
705
706 ) {
707 // Allow users to logout.
708 return;
709 }
710
711 $ip = jetpack_protect_get_ip();
712 /**
713 * Fires before every killed login.
714 *
715 * @module protect
716 *
717 * @since 3.4.0
718 *
719 * @param string $ip IP flagged by Protect.
720 */
721 do_action( 'jpp_kill_login', $ip );
722
723 if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
724 // translators: variable is the IP address that was flagged.
725 $die_string = sprintf( __( 'Your IP (%1$s) has been flagged for potential security violations.', 'jetpack' ), str_replace( 'http://', '', esc_url( 'http://' . $ip ) ) );
726 wp_die(
727 $die_string, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_url used when forming string.
728 esc_html__( 'Login Blocked by Jetpack', 'jetpack' ),
729 array( 'response' => 403 )
730 );
731 }
732
733 require_once __DIR__ . '/protect/blocked-login-page.php';
734 $blocked_login_page = Jetpack_Protect_Blocked_Login_Page::instance( $ip );
735
736 if ( $blocked_login_page->is_blocked_user_valid() ) {
737 return;
738 }
739
740 $blocked_login_page->render_and_die();
741 }
742
743 /**
744 * Checks if the protect API call has failed, and if so initiates the math captcha fallback.
745 */
746 public function check_use_math() {
747 $use_math = $this->get_transient( 'brute_use_math' );
748 if ( $use_math ) {
749 include_once __DIR__ . '/protect/math-fallback.php';
750 new Jetpack_Protect_Math_Authenticate();
751 }
752 }
753
754 /**
755 * If we're in a multisite network, return the blog ID of the primary blog
756 *
757 * @return int
758 */
759 public function get_main_blog_id() {
760 if ( ! is_multisite() ) {
761 return false;
762 }
763
764 global $current_site;
765 $primary_blog_id = $current_site->blog_id;
766
767 return $primary_blog_id;
768 }
769
770 /**
771 * Get jetpack blog id, or the jetpack blog id of the main blog in the main network
772 *
773 * @return int
774 */
775 public function get_main_blog_jetpack_id() {
776 if ( ! is_main_site() ) {
777 switch_to_blog( $this->get_main_blog_id() );
778 $id = Jetpack::get_option( 'id', false );
779 restore_current_blog();
780 } else {
781 $id = Jetpack::get_option( 'id' );
782 }
783
784 return $id;
785 }
786
787 /**
788 * Checks the API key.
789 */
790 public function check_api_key() {
791 $response = $this->protect_call( 'check_key' );
792
793 if ( isset( $response['ckval'] ) ) {
794 return true;
795 }
796
797 if ( isset( $response['error'] ) ) {
798
799 if ( 'Invalid API Key' === $response['error'] ) {
800 $this->api_key_error = __( 'Your API key is invalid', 'jetpack' );
801 }
802
803 if ( 'API Key Required' === $response['error'] ) {
804 $this->api_key_error = __( 'No API key', 'jetpack' );
805 }
806 }
807
808 $this->api_key_error = __( 'There was an error contacting Jetpack servers.', 'jetpack' );
809
810 return false;
811 }
812
813 /**
814 * Calls over to the api using wp_remote_post
815 *
816 * @param string $action - 'check_ip', 'check_key', or 'failed_attempt'.
817 * @param array $request - Any custom data to post to the api.
818 *
819 * @return array
820 */
821 public function protect_call( $action = 'check_ip', $request = array() ) {
822 global $wp_version;
823
824 $api_key = $this->maybe_get_protect_key();
825
826 $user_agent = "WordPress/{$wp_version} | Jetpack/" . constant( 'JETPACK__VERSION' );
827
828 $request['action'] = $action;
829 $request['ip'] = jetpack_protect_get_ip();
830 $request['host'] = $this->get_local_host();
831 $request['headers'] = wp_json_encode( $this->get_headers() );
832 $request['jetpack_version'] = constant( 'JETPACK__VERSION' );
833 $request['wordpress_version'] = (string) $wp_version;
834 $request['api_key'] = $api_key;
835 $request['multisite'] = '0';
836
837 if ( is_multisite() ) {
838 $request['multisite'] = get_blog_count();
839 }
840
841 /**
842 * Filter controls maximum timeout in waiting for reponse from Protect servers.
843 *
844 * @module protect
845 *
846 * @since 4.0.4
847 *
848 * @param int $timeout Max time (in seconds) to wait for a response.
849 */
850 $timeout = apply_filters( 'jetpack_protect_connect_timeout', 30 );
851
852 $args = array(
853 'body' => $request,
854 'user-agent' => $user_agent,
855 'httpversion' => '1.0',
856 'timeout' => absint( $timeout ),
857 );
858
859 $response_json = wp_remote_post( JETPACK_PROTECT__API_HOST, $args );
860 $this->last_response_raw = $response_json;
861
862 $transient_name = $this->get_transient_name();
863 $this->delete_transient( $transient_name );
864
865 if ( is_array( $response_json ) ) {
866 $response = json_decode( $response_json['body'], true );
867 }
868
869 if ( isset( $response['blocked_attempts'] ) && $response['blocked_attempts'] ) {
870 update_site_option( 'jetpack_protect_blocked_attempts', $response['blocked_attempts'] );
871 }
872
873 if ( isset( $response['status'] ) && ! isset( $response['error'] ) ) {
874 $response['expire'] = time() + $response['seconds_remaining'];
875 $this->set_transient( $transient_name, $response, $response['seconds_remaining'] );
876 $this->delete_transient( 'brute_use_math' );
877 } else { // Fallback to Math Captcha if no response from API host.
878 $this->set_transient( 'brute_use_math', 1, 600 );
879 $response['status'] = 'ok';
880 $response['math'] = true;
881 }
882
883 if ( isset( $response['error'] ) ) {
884 update_site_option( 'jetpack_protect_error', $response['error'] );
885 } else {
886 delete_site_option( 'jetpack_protect_error' );
887 }
888
889 return $response;
890 }
891
892 /**
893 * Gets the transient name.
894 */
895 public function get_transient_name() {
896 $headers = $this->get_headers();
897 $header_hash = md5( wp_json_encode( $headers ) );
898
899 return 'jpp_li_' . $header_hash;
900 }
901
902 /**
903 * Wrapper for WordPress set_transient function, our version sets
904 * the transient on the main site in the network if this is a multisite network
905 *
906 * We do it this way (instead of set_site_transient) because of an issue where
907 * sitewide transients are always autoloaded
908 * https://core.trac.wordpress.org/ticket/22846
909 *
910 * @param string $transient Transient name. Expected to not be SQL-escaped. Must be
911 * 45 characters or fewer in length.
912 * @param mixed $value Transient value. Must be serializable if non-scalar.
913 * Expected to not be SQL-escaped.
914 * @param int $expiration Optional. Time until expiration in seconds. Default 0.
915 *
916 * @return bool False if value was not set and true if value was set.
917 */
918 public function set_transient( $transient, $value, $expiration ) {
919 if ( is_multisite() && ! is_main_site() ) {
920 switch_to_blog( $this->get_main_blog_id() );
921 $return = set_transient( $transient, $value, $expiration );
922 restore_current_blog();
923
924 return $return;
925 }
926
927 return set_transient( $transient, $value, $expiration );
928 }
929
930 /**
931 * Wrapper for WordPress delete_transient function, our version deletes
932 * the transient on the main site in the network if this is a multisite network
933 *
934 * @param string $transient Transient name. Expected to not be SQL-escaped.
935 *
936 * @return bool true if successful, false otherwise
937 */
938 public function delete_transient( $transient ) {
939 if ( is_multisite() && ! is_main_site() ) {
940 switch_to_blog( $this->get_main_blog_id() );
941 $return = delete_transient( $transient );
942 restore_current_blog();
943
944 return $return;
945 }
946
947 return delete_transient( $transient );
948 }
949
950 /**
951 * Wrapper for WordPress get_transient function, our version gets
952 * the transient on the main site in the network if this is a multisite network
953 *
954 * @param string $transient Transient name. Expected to not be SQL-escaped.
955 *
956 * @return mixed Value of transient.
957 */
958 public function get_transient( $transient ) {
959 if ( is_multisite() && ! is_main_site() ) {
960 switch_to_blog( $this->get_main_blog_id() );
961 $return = get_transient( $transient );
962 restore_current_blog();
963
964 return $return;
965 }
966
967 return get_transient( $transient );
968 }
969
970 /**
971 * Returns the local host.
972 */
973 public function get_local_host() {
974 if ( isset( $this->local_host ) ) {
975 return $this->local_host;
976 }
977
978 $uri = 'http://' . strtolower( isset( $_SERVER['HTTP_HOST'] ) ? filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '' );
979
980 if ( is_multisite() ) {
981 $uri = network_home_url();
982 }
983
984 $uridata = wp_parse_url( $uri );
985
986 $domain = $uridata['host'];
987
988 // If we still don't have the site_url, get it.
989 if ( ! $domain ) {
990 $uri = get_site_url( 1 );
991 $uridata = wp_parse_url( $uri );
992 $domain = $uridata['host'];
993 }
994
995 $this->local_host = $domain;
996
997 return $this->local_host;
998 }
999
1000 }
1001
1002 $jetpack_protect = Jetpack_Protect_Module::instance();
1003
1004 global $pagenow;
1005 if ( isset( $pagenow ) && 'wp-login.php' === $pagenow ) {
1006 $jetpack_protect->check_login_ability();
1007 }
1008