Completed
Push — add/changelog-77 ( fc32a5...198265 )
by Jeremy
26:28 queued 19:02
created

Jetpack_Protect_Module::admin_banner_styles()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 0
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Module Name: Protect
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
 * Auto Activate: Yes
10
 * Module Tags: Recommended
11
 * Feature: Security
12
 * 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
13
 */
14
15
use Automattic\Jetpack\Constants;
16
17
include_once JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php';
18
19
class Jetpack_Protect_Module {
20
21
	private static $__instance = null;
22
	public $api_key;
23
	public $api_key_error;
24
	public $whitelist;
25
	public $whitelist_error;
26
	public $whitelist_saved;
27
	private $user_ip;
28
	private $local_host;
29
	private $api_endpoint;
30
	public $last_request;
31
	public $last_response_raw;
32
	public $last_response;
33
	private $block_login_with_math;
34
35
	/**
36
	 * Singleton implementation
37
	 *
38
	 * @return object
39
	 */
40
	public static function instance() {
41
		if ( ! is_a( self::$__instance, 'Jetpack_Protect_Module' ) ) {
42
			self::$__instance = new Jetpack_Protect_Module();
43
		}
44
45
		return self::$__instance;
46
	}
47
48
	/**
49
	 * Registers actions
50
	 */
51
	private function __construct() {
52
		add_action( 'jetpack_activate_module_protect', array ( $this, 'on_activation' ) );
53
		add_action( 'jetpack_deactivate_module_protect', array ( $this, 'on_deactivation' ) );
54
		add_action( 'jetpack_modules_loaded', array ( $this, 'modules_loaded' ) );
55
		add_action( 'login_form', array ( $this, 'check_use_math' ), 0 );
56
		add_filter( 'authenticate', array ( $this, 'check_preauth' ), 10, 3 );
57
		add_action( 'wp_login', array ( $this, 'log_successful_login' ), 10, 2 );
58
		add_action( 'wp_login_failed', array ( $this, 'log_failed_attempt' ) );
59
		add_action( 'admin_init', array ( $this, 'maybe_update_headers' ) );
60
		add_action( 'admin_init', array ( $this, 'maybe_display_security_warning' ) );
61
62
		// This is a backup in case $pagenow fails for some reason
63
		add_action( 'login_form', array ( $this, 'check_login_ability' ), 1 );
64
65
		// Load math fallback after math page form submission
66
		if ( isset( $_POST[ 'jetpack_protect_process_math_form' ] ) ) {
67
			include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
68
			new Jetpack_Protect_Math_Authenticate;
69
		}
70
71
		// Runs a script every day to clean up expired transients so they don't
72
		// clog up our users' databases
73
		require_once( JETPACK__PLUGIN_DIR . '/modules/protect/transient-cleanup.php' );
74
	}
75
76
	/**
77
	 * On module activation, try to get an api key
78
	 */
79
	public function on_activation() {
80
		if ( is_multisite() && is_main_site() && get_site_option( 'jetpack_protect_active', 0 ) == 0 ) {
81
			update_site_option( 'jetpack_protect_active', 1 );
82
		}
83
84
		update_site_option( 'jetpack_protect_activating', 'activating' );
85
86
		// Get BruteProtect's counter number
87
		Jetpack_Protect_Module::protect_call( 'check_key' );
88
	}
89
90
	/**
91
	 * On module deactivation, unset protect_active
92
	 */
93
	public function on_deactivation() {
94
		if ( is_multisite() && is_main_site() ) {
95
			update_site_option( 'jetpack_protect_active', 0 );
96
		}
97
	}
98
99
	public function maybe_get_protect_key() {
100
		if ( get_site_option( 'jetpack_protect_activating', false ) && ! get_site_option( 'jetpack_protect_key', false ) ) {
101
			$key = $this->get_protect_key();
102
			delete_site_option( 'jetpack_protect_activating' );
103
			return $key;
104
		}
105
106
		return get_site_option( 'jetpack_protect_key' );
107
	}
108
109
	/**
110
	 * Sends a "check_key" API call once a day.  This call allows us to track IP-related
111
	 * headers for this server via the Protect API, in order to better identify the source
112
	 * IP for login attempts
113
	 */
114
	public function maybe_update_headers( $force = false ) {
115
		$updated_recently = $this->get_transient( 'jpp_headers_updated_recently' );
116
117
		if ( ! $force ) {
118
			if ( isset( $_GET['protect_update_headers'] ) ) {
119
				$force = true;
120
			}
121
		}
122
123
		// check that current user is admin so we prevent a lower level user from adding
124
		// a trusted header, allowing them to brute force an admin account
125
		if ( ( $updated_recently && ! $force ) || ! current_user_can( 'update_plugins' ) ) {
126
			return;
127
		}
128
129
		$response = Jetpack_Protect_Module::protect_call( 'check_key' );
130
		$this->set_transient( 'jpp_headers_updated_recently', 1, DAY_IN_SECONDS );
131
132
		if ( isset( $response['msg'] ) && $response['msg'] ) {
133
			update_site_option( 'trusted_ip_header', json_decode( $response['msg'] ) );
134
		}
135
136
	}
137
138
	public function maybe_display_security_warning() {
139
		if ( is_multisite() && current_user_can( 'manage_network' ) ) {
140
			if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
141
				require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
142
			}
143
144
			if ( ! is_plugin_active_for_network( plugin_basename( JETPACK__PLUGIN_FILE ) ) ) {
145
				add_action( 'load-index.php', array( $this, 'prepare_jetpack_protect_multisite_notice' ) );
146
				add_action( 'wp_ajax_jetpack-protect-dismiss-multisite-banner', array( $this, 'ajax_dismiss_handler' ) );
147
			}
148
		}
149
	}
150
151
	public function prepare_jetpack_protect_multisite_notice() {
152
		$dismissed = get_site_option( 'jetpack_dismissed_protect_multisite_banner' );
153
		if ( $dismissed ) {
154
			return;
155
		}
156
157
		add_action( 'admin_notices', array ( $this, 'admin_jetpack_manage_notice' ) );
158
	}
159
160
	public function ajax_dismiss_handler() {
161
		check_ajax_referer( 'jetpack_protect_multisite_banner_opt_out' );
162
163
		if ( ! current_user_can( 'manage_network' ) ) {
164
			wp_send_json_error( new WP_Error( 'insufficient_permissions' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'insufficient_permissions'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
165
		}
166
167
		update_site_option( 'jetpack_dismissed_protect_multisite_banner', true );
168
169
		wp_send_json_success();
170
	}
171
172
	/**
173
	 * Displays a warning about Jetpack Protect's network activation requirement.
174
	 * Attaches some custom JS to Core's `is-dismissible` UI to save the dismissed state.
175
	 */
176
	public function admin_jetpack_manage_notice() {
177
		?>
178
		<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' ) ); ?>">
179
			<h2><?php esc_html_e( 'Jetpack Brute Force Attack Prevention cannot keep your site secure', 'jetpack' ); ?></h2>
180
181
			<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>
182
183
			<p>
184
				<a class="button-primary" href="<?php echo esc_url( network_admin_url( 'plugins.php' ) ); ?>">
185
					<?php esc_html_e( 'View Network Admin', 'jetpack' ); ?>
186
				</a>
187
				<a class="button" href="<?php echo esc_url( __( 'http://jetpack.com/support/multisite-protect', 'jetpack' ) ); ?>" target="_blank">
188
					<?php esc_html_e( 'Learn More' ); ?>
189
				</a>
190
			</p>
191
		</div>
192
		<script>
193
			jQuery( function( $ ) {
194
				$( '.jetpack-protect-warning' ).on( 'click', 'button.notice-dismiss', function( event ) {
195
					event.preventDefault();
196
197
					wp.ajax.post(
198
						'jetpack-protect-dismiss-multisite-banner',
199
						{
200
							_wpnonce: $( event.delegateTarget ).data( 'dismiss-nonce' ),
201
						}
202
					).fail( function( error ) { <?php
203
						// A failure here is really strange, and there's not really anything a site owner can do to fix one.
204
						// Just log the error for now to help debugging. ?>
205
206
						if ( 'function' === typeof error.done && '-1' === error.responseText ) {
207
							console.error( 'Notice dismissal failed: check_ajax_referer' );
208
						} else {
209
							console.error( 'Notice dismissal failed: ' + JSON.stringify( error ) );
210
						}
211
					} )
212
				} );
213
			} );
214
		</script>
215
		<?php
216
	}
217
218
	/**
219
	 * Request an api key from wordpress.com
220
	 *
221
	 * @return bool | string
222
	 */
223
	public function get_protect_key() {
224
225
		$protect_blog_id = Jetpack_Protect_Module::get_main_blog_jetpack_id();
226
227
		// If we can't find the the blog id, that means we are on multisite, and the main site never connected
228
		// the protect api key is linked to the main blog id - instruct the user to connect their main blog
229
		if ( ! $protect_blog_id ) {
230
			$this->api_key_error = __( 'Your main blog is not connected to WordPress.com. Please connect to get an API key.', 'jetpack' );
231
232
			return false;
233
		}
234
235
		$request = array (
236
			'jetpack_blog_id'      => $protect_blog_id,
237
			'bruteprotect_api_key' => get_site_option( 'bruteprotect_api_key' ),
238
			'multisite'            => '0',
239
		);
240
241
		// Send the number of blogs on the network if we are on multisite
242
		if ( is_multisite() ) {
243
			$request['multisite'] = get_blog_count();
244
			if ( ! $request['multisite'] ) {
245
				global $wpdb;
246
				$request['multisite'] = $wpdb->get_var( "SELECT COUNT(blog_id) as c FROM $wpdb->blogs WHERE spam = '0' AND deleted = '0' and archived = '0'" );
247
			}
248
		}
249
250
		// Request the key
251
		$xml = new Jetpack_IXR_Client( array (
252
			'user_id' => get_current_user_id()
253
		) );
254
		$xml->query( 'jetpack.protect.requestKey', $request );
255
256
		// Hmm, can't talk to wordpress.com
257
		if ( $xml->isError() ) {
258
			$code                = $xml->getErrorCode();
259
			$message             = $xml->getErrorMessage();
260
			$this->api_key_error = sprintf( __( 'Error connecting to WordPress.com. Code: %1$s, %2$s', 'jetpack' ), $code, $message );
261
262
			return false;
263
		}
264
265
		$response = $xml->getResponse();
266
267
		// Hmm. Can't talk to the protect servers ( api.bruteprotect.com )
268
		if ( ! isset( $response['data'] ) ) {
269
			$this->api_key_error = __( 'No reply from Jetpack servers', 'jetpack' );
270
271
			return false;
272
		}
273
274
		// There was an issue generating the key
275
		if ( empty( $response['success'] ) ) {
276
			$this->api_key_error = $response['data'];
277
278
			return false;
279
		}
280
281
		// Key generation successful!
282
		$active_plugins = Jetpack::get_active_plugins();
283
284
		// We only want to deactivate BruteProtect if we successfully get a key
285
		if ( in_array( 'bruteprotect/bruteprotect.php', $active_plugins ) ) {
286
			Jetpack_Client_Server::deactivate_plugin( 'bruteprotect/bruteprotect.php', 'BruteProtect' );
287
		}
288
289
		$key = $response['data'];
290
		update_site_option( 'jetpack_protect_key', $key );
291
292
		return $key;
293
	}
294
295
	/**
296
	 * Called via WP action wp_login_failed to log failed attempt with the api
297
	 *
298
	 * Fires custom, plugable action jpp_log_failed_attempt with the IP
299
	 *
300
	 * @return void
301
	 */
302
	function log_failed_attempt( $login_user = null ) {
303
304
		/**
305
		 * Fires before every failed login attempt.
306
		 *
307
		 * @module protect
308
		 *
309
		 * @since 3.4.0
310
		 *
311
		 * @param array Information about failed login attempt
312
		 *   [
313
		 *     'login' => (string) Username or email used in failed login attempt
314
		 *   ]
315
		 */
316
		do_action( 'jpp_log_failed_attempt', array( 'login' => $login_user ) );
317
318
		if ( isset( $_COOKIE['jpp_math_pass'] ) ) {
319
320
			$transient = $this->get_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'] );
321
			$transient--;
322
323
			if ( ! $transient || $transient < 1 ) {
324
				$this->delete_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'] );
325
				setcookie( 'jpp_math_pass', 0, time() - DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false );
326
			} else {
327
				$this->set_transient( 'jpp_math_pass_' . $_COOKIE['jpp_math_pass'], $transient, DAY_IN_SECONDS );
328
			}
329
330
		}
331
		$this->protect_call( 'failed_attempt' );
332
	}
333
334
	/**
335
	 * Set up the Protect configuration page
336
	 */
337
	public function modules_loaded() {
338
		Jetpack::enable_module_configurable( __FILE__ );
339
	}
340
341
	/**
342
	 * Logs a successful login back to our servers, this allows us to make sure we're not blocking
343
	 * a busy IP that has a lot of good logins along with some forgotten passwords. Also saves current user's ip
344
	 * to the ip address whitelist
345
	 */
346
	public function log_successful_login( $user_login, $user = null ) {
347
		if ( ! $user ) { // For do_action( 'wp_login' ) calls that lacked passing the 2nd arg.
348
			$user = get_user_by( 'login', $user_login );
349
		}
350
351
		$this->protect_call( 'successful_login', array ( 'roles' => $user->roles ) );
352
	}
353
354
355
	/**
356
	 * Checks for loginability BEFORE authentication so that bots don't get to go around the log in form.
357
	 *
358
	 * If we are using our math fallback, authenticate via math-fallback.php
359
	 *
360
	 * @param string $user
361
	 * @param string $username
362
	 * @param string $password
363
	 *
364
	 * @return string $user
365
	 */
366
	function check_preauth( $user = 'Not Used By Protect', $username = 'Not Used By Protect', $password = 'Not Used By Protect' ) {
367
		$allow_login = $this->check_login_ability( true );
368
		$use_math    = $this->get_transient( 'brute_use_math' );
369
370
		if ( ! $allow_login ) {
371
			$this->block_with_math();
372
		}
373
374
		if ( ( 1 == $use_math || 1 == $this->block_login_with_math ) && isset( $_POST['log'] ) ) {
375
			include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
376
			Jetpack_Protect_Math_Authenticate::math_authenticate();
377
		}
378
379
		return $user;
380
	}
381
382
	/**
383
	 * Get all IP headers so that we can process on our server...
384
	 *
385
	 * @return string
386
	 */
387
	function get_headers() {
388
		$ip_related_headers = array (
389
			'GD_PHP_HANDLER',
390
			'HTTP_AKAMAI_ORIGIN_HOP',
391
			'HTTP_CF_CONNECTING_IP',
392
			'HTTP_CLIENT_IP',
393
			'HTTP_FASTLY_CLIENT_IP',
394
			'HTTP_FORWARDED',
395
			'HTTP_FORWARDED_FOR',
396
			'HTTP_INCAP_CLIENT_IP',
397
			'HTTP_TRUE_CLIENT_IP',
398
			'HTTP_X_CLIENTIP',
399
			'HTTP_X_CLUSTER_CLIENT_IP',
400
			'HTTP_X_FORWARDED',
401
			'HTTP_X_FORWARDED_FOR',
402
			'HTTP_X_IP_TRAIL',
403
			'HTTP_X_REAL_IP',
404
			'HTTP_X_VARNISH',
405
			'REMOTE_ADDR'
406
		);
407
408
		foreach ( $ip_related_headers as $header ) {
409
			if ( isset( $_SERVER[ $header ] ) ) {
410
				$output[ $header ] = $_SERVER[ $header ];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$output was never initialized. Although not strictly required by PHP, it is generally a good practice to add $output = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
411
			}
412
		}
413
414
		return $output;
0 ignored issues
show
Bug introduced by
The variable $output does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
415
	}
416
417
	/*
418
	 * Checks if the IP address has been whitelisted
419
	 *
420
	 * @param string $ip
421
	 *
422
	 * @return bool
423
	 */
424
	function ip_is_whitelisted( $ip ) {
425
		// If we found an exact match in wp-config
426
		if ( defined( 'JETPACK_IP_ADDRESS_OK' ) && JETPACK_IP_ADDRESS_OK == $ip ) {
427
			return true;
428
		}
429
430
		$whitelist = jetpack_protect_get_local_whitelist();
431
432
		if ( is_multisite() ) {
433
			$whitelist = array_merge( $whitelist, get_site_option( 'jetpack_protect_global_whitelist', array () ) );
434
		}
435
436
		if ( ! empty( $whitelist ) ) :
437
			foreach ( $whitelist as $item ) :
438
				// If the IPs are an exact match
439
				if ( ! $item->range && isset( $item->ip_address ) && $item->ip_address == $ip ) {
440
					return true;
441
				}
442
443
				if ( $item->range && isset( $item->range_low ) && isset( $item->range_high ) ) {
444
					if ( jetpack_protect_ip_address_is_in_range( $ip, $item->range_low, $item->range_high ) ) {
445
						return true;
446
					}
447
				}
448
			endforeach;
449
		endif;
450
451
		return false;
452
	}
453
454
	/**
455
	 * Checks the status for a given IP. API results are cached as transients
456
	 *
457
	 * @param bool $preauth Whether or not we are checking prior to authorization
458
	 *
459
	 * @return bool Either returns true, fires $this->kill_login, or includes a math fallback and returns false
460
	 */
461
	function check_login_ability( $preauth = false ) {
462
463
		/**
464
		 * JETPACK_ALWAYS_PROTECT_LOGIN will always disable the login page, and use a page provided by Jetpack.
465
		 */
466
		if ( Constants::is_true( 'JETPACK_ALWAYS_PROTECT_LOGIN' ) ) {
467
			$this->kill_login();
468
		}
469
470
		if ( $this->is_current_ip_whitelisted() ) {
471
		    return true;
472
        }
473
474
		$status = $this->get_cached_status();
475
476
		if ( empty( $status ) ) {
477
			// If we've reached this point, this means that the IP isn't cached.
478
			// Now we check with the Protect API to see if we should allow login
479
			$response = $this->protect_call( $action = 'check_ip' );
480
481
			if ( isset( $response['math'] ) && ! function_exists( 'brute_math_authenticate' ) ) {
482
				include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
483
				new Jetpack_Protect_Math_Authenticate;
484
485
				return false;
486
			}
487
488
			$status = $response['status'];
489
		}
490
491
		if ( 'blocked' == $status ) {
492
			$this->block_with_math();
493
		}
494
495
		if ( 'blocked-hard' == $status ) {
496
			$this->kill_login();
497
		}
498
499
		return true;
500
	}
501
502
	function is_current_ip_whitelisted() {
503
		$ip = jetpack_protect_get_ip();
504
505
		// Server is misconfigured and we can't get an IP
506
		if ( ! $ip && class_exists( 'Jetpack' ) ) {
507
			Jetpack::deactivate_module( 'protect' );
508
			ob_start();
509
			Jetpack::state( 'message', 'protect_misconfigured_ip' );
510
			ob_end_clean();
511
			return true;
512
		}
513
514
		/**
515
		 * Short-circuit check_login_ability.
516
		 *
517
		 * If there is an alternate way to validate the current IP such as
518
		 * a hard-coded list of IP addresses, we can short-circuit the rest
519
		 * of the login ability checks and return true here.
520
		 *
521
		 * @module protect
522
		 *
523
		 * @since 4.4.0
524
		 *
525
		 * @param bool false Should we allow all logins for the current ip? Default: false
526
		 */
527
		if ( apply_filters( 'jpp_allow_login', false, $ip ) ) {
528
			return true;
529
		}
530
531
		if ( jetpack_protect_ip_is_private( $ip ) ) {
532
			return true;
533
		}
534
535
		if ( $this->ip_is_whitelisted( $ip ) ) {
536
			return true;
537
		}
538
    }
539
540
    function has_login_ability() {
541
	    if ( $this->is_current_ip_whitelisted() ) {
542
		    return true;
543
	    }
544
	    $status = $this->get_cached_status();
545
	    if ( empty( $status ) || $status === 'ok' ) {
546
	        return true;
547
        }
548
        return false;
549
    }
550
551
	function get_cached_status() {
552
		$transient_name  = $this->get_transient_name();
553
		$value = $this->get_transient( $transient_name );
554
		if ( isset( $value['status'] ) ) {
555
		    return $value['status'];
556
        }
557
        return '';
558
	}
559
560
	function block_with_math() {
561
		/**
562
		 * By default, Protect will allow a user who has been blocked for too
563
		 * many failed logins to start answering math questions to continue logging in
564
		 *
565
		 * For added security, you can disable this.
566
		 *
567
		 * @module protect
568
		 *
569
		 * @since 3.6.0
570
		 *
571
		 * @param bool Whether to allow math for blocked users or not.
572
		 */
573
574
		$this->block_login_with_math = 1;
575
		/**
576
		 * Allow Math fallback for blocked IPs.
577
		 *
578
		 * @module protect
579
		 *
580
		 * @since 3.6.0
581
		 *
582
		 * @param bool true Should we fallback to the Math questions when an IP is blocked. Default to true.
583
		 */
584
		$allow_math_fallback_on_fail = apply_filters( 'jpp_use_captcha_when_blocked', true );
585
		if ( ! $allow_math_fallback_on_fail  ) {
586
			$this->kill_login();
587
		}
588
		include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
589
		new Jetpack_Protect_Math_Authenticate;
590
591
		return false;
592
	}
593
594
	/*
595
	 * Kill a login attempt
596
	 */
597
	function kill_login() {
598
		if (
599
			isset( $_GET['action'], $_GET['_wpnonce'] ) &&
600
			'logout' === $_GET['action'] &&
601
			wp_verify_nonce( $_GET['_wpnonce'], 'log-out' ) &&
602
			wp_get_current_user()
603
604
		) {
605
			// Allow users to logout
606
			return;
607
		}
608
609
		$ip = jetpack_protect_get_ip();
610
		/**
611
		 * Fires before every killed login.
612
		 *
613
		 * @module protect
614
		 *
615
		 * @since 3.4.0
616
		 *
617
		 * @param string $ip IP flagged by Protect.
618
		 */
619
		do_action( 'jpp_kill_login', $ip );
620
621
		if( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
622
			$die_string = sprintf( __( 'Your IP (%1$s) has been flagged for potential security violations.', 'jetpack' ), str_replace( 'http://', '', esc_url( 'http://' . $ip ) ) );
623
			wp_die(
624
				$die_string,
625
				__( 'Login Blocked by Jetpack', 'jetpack' ),
626
				array ( 'response' => 403 )
627
			);
628
		}
629
630
		require_once dirname( __FILE__ ) . '/protect/blocked-login-page.php';
631
		$blocked_login_page = Jetpack_Protect_Blocked_Login_Page::instance( $ip );
632
633
		if ( $blocked_login_page->is_blocked_user_valid() ) {
634
			return;
635
		}
636
637
		$blocked_login_page->render_and_die();
638
	}
639
640
	/*
641
	 * Checks if the protect API call has failed, and if so initiates the math captcha fallback.
642
	 */
643
	public function check_use_math() {
644
		$use_math = $this->get_transient( 'brute_use_math' );
645
		if ( $use_math ) {
646
			include_once dirname( __FILE__ ) . '/protect/math-fallback.php';
647
			new Jetpack_Protect_Math_Authenticate;
648
		}
649
	}
650
651
	/**
652
	 * If we're in a multisite network, return the blog ID of the primary blog
653
	 *
654
	 * @return int
655
	 */
656
	public function get_main_blog_id() {
657
		if ( ! is_multisite() ) {
658
			return false;
659
		}
660
661
		global $current_site;
662
		$primary_blog_id = $current_site->blog_id;
663
664
		return $primary_blog_id;
665
	}
666
667
	/**
668
	 * Get jetpack blog id, or the jetpack blog id of the main blog in the main network
669
	 *
670
	 * @return int
671
	 */
672
	public function get_main_blog_jetpack_id() {
673
		if ( ! is_main_site() ) {
674
			switch_to_blog( $this->get_main_blog_id() );
675
			$id = Jetpack::get_option( 'id', false );
676
			restore_current_blog();
677
		} else {
678
			$id = Jetpack::get_option( 'id' );
679
		}
680
681
		return $id;
682
	}
683
684
	public function check_api_key() {
685
		$response = $this->protect_call( 'check_key' );
686
687
		if ( isset( $response['ckval'] ) ) {
688
			return true;
689
		}
690
691
		if ( isset( $response['error'] ) ) {
692
693
			if ( $response['error'] == 'Invalid API Key' ) {
694
				$this->api_key_error = __( 'Your API key is invalid', 'jetpack' );
695
			}
696
697
			if ( $response['error'] == 'API Key Required' ) {
698
				$this->api_key_error = __( 'No API key', 'jetpack' );
699
			}
700
		}
701
702
		$this->api_key_error = __( 'There was an error contacting Jetpack servers.', 'jetpack' );
703
704
		return false;
705
	}
706
707
	/**
708
	 * Calls over to the api using wp_remote_post
709
	 *
710
	 * @param string $action 'check_ip', 'check_key', or 'failed_attempt'
711
	 * @param array $request Any custom data to post to the api
712
	 *
713
	 * @return array
714
	 */
715
	function protect_call( $action = 'check_ip', $request = array () ) {
716
		global $wp_version;
717
718
		$api_key = $this->maybe_get_protect_key();
719
720
		$user_agent = "WordPress/{$wp_version} | Jetpack/" . constant( 'JETPACK__VERSION' );
721
722
		$request['action']            = $action;
723
		$request['ip']                = jetpack_protect_get_ip();
724
		$request['host']              = $this->get_local_host();
725
		$request['headers']           = json_encode( $this->get_headers() );
726
		$request['jetpack_version']   = constant( 'JETPACK__VERSION' );
727
		$request['wordpress_version'] = strval( $wp_version );
728
		$request['api_key']           = $api_key;
729
		$request['multisite']         = "0";
730
731
		if ( is_multisite() ) {
732
			$request['multisite'] = get_blog_count();
733
		}
734
735
736
		/**
737
		 * Filter controls maximum timeout in waiting for reponse from Protect servers.
738
		 *
739
		 * @module protect
740
		 *
741
		 * @since 4.0.4
742
		 *
743
		 * @param int $timeout Max time (in seconds) to wait for a response.
744
		 */
745
		$timeout = apply_filters( 'jetpack_protect_connect_timeout', 30 );
746
747
		$args = array (
748
			'body'        => $request,
749
			'user-agent'  => $user_agent,
750
			'httpversion' => '1.0',
751
			'timeout'     => absint( $timeout )
752
		);
753
754
		$response_json           = wp_remote_post( $this->get_api_host(), $args );
755
		$this->last_response_raw = $response_json;
756
757
		$transient_name = $this->get_transient_name();
758
		$this->delete_transient( $transient_name );
759
760
		if ( is_array( $response_json ) ) {
761
			$response = json_decode( $response_json['body'], true );
762
		}
763
764
		if ( isset( $response['blocked_attempts'] ) && $response['blocked_attempts'] ) {
765
			update_site_option( 'jetpack_protect_blocked_attempts', $response['blocked_attempts'] );
766
		}
767
768
		if ( isset( $response['status'] ) && ! isset( $response['error'] ) ) {
769
			$response['expire'] = time() + $response['seconds_remaining'];
770
			$this->set_transient( $transient_name, $response, $response['seconds_remaining'] );
771
			$this->delete_transient( 'brute_use_math' );
772
		} else { // Fallback to Math Captcha if no response from API host
773
			$this->set_transient( 'brute_use_math', 1, 600 );
774
			$response['status'] = 'ok';
0 ignored issues
show
Bug introduced by
The variable $response does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
775
			$response['math']   = true;
776
		}
777
778
		if ( isset( $response['error'] ) ) {
779
			update_site_option( 'jetpack_protect_error', $response['error'] );
780
		} else {
781
			delete_site_option( 'jetpack_protect_error' );
782
		}
783
784
		return $response;
785
	}
786
787
	function get_transient_name() {
788
		$headers     = $this->get_headers();
789
		$header_hash = md5( json_encode( $headers ) );
790
791
		return 'jpp_li_' . $header_hash;
792
	}
793
794
	/**
795
	 * Wrapper for WordPress set_transient function, our version sets
796
	 * the transient on the main site in the network if this is a multisite network
797
	 *
798
	 * We do it this way (instead of set_site_transient) because of an issue where
799
	 * sitewide transients are always autoloaded
800
	 * https://core.trac.wordpress.org/ticket/22846
801
	 *
802
	 * @param string $transient Transient name. Expected to not be SQL-escaped. Must be
803
	 *                           45 characters or fewer in length.
804
	 * @param mixed $value Transient value. Must be serializable if non-scalar.
805
	 *                           Expected to not be SQL-escaped.
806
	 * @param int $expiration Optional. Time until expiration in seconds. Default 0.
807
	 *
808
	 * @return bool False if value was not set and true if value was set.
809
	 */
810
	function set_transient( $transient, $value, $expiration ) {
811
		if ( is_multisite() && ! is_main_site() ) {
812
			switch_to_blog( $this->get_main_blog_id() );
813
			$return = set_transient( $transient, $value, $expiration );
814
			restore_current_blog();
815
816
			return $return;
817
		}
818
819
		return set_transient( $transient, $value, $expiration );
820
	}
821
822
	/**
823
	 * Wrapper for WordPress delete_transient function, our version deletes
824
	 * the transient on the main site in the network if this is a multisite network
825
	 *
826
	 * @param string $transient Transient name. Expected to not be SQL-escaped.
827
	 *
828
	 * @return bool true if successful, false otherwise
829
	 */
830 View Code Duplication
	function delete_transient( $transient ) {
831
		if ( is_multisite() && ! is_main_site() ) {
832
			switch_to_blog( $this->get_main_blog_id() );
833
			$return = delete_transient( $transient );
834
			restore_current_blog();
835
836
			return $return;
837
		}
838
839
		return delete_transient( $transient );
840
	}
841
842
	/**
843
	 * Wrapper for WordPress get_transient function, our version gets
844
	 * the transient on the main site in the network if this is a multisite network
845
	 *
846
	 * @param string $transient Transient name. Expected to not be SQL-escaped.
847
	 *
848
	 * @return mixed Value of transient.
849
	 */
850 View Code Duplication
	function get_transient( $transient ) {
851
		if ( is_multisite() && ! is_main_site() ) {
852
			switch_to_blog( $this->get_main_blog_id() );
853
			$return = get_transient( $transient );
854
			restore_current_blog();
855
856
			return $return;
857
		}
858
859
		return get_transient( $transient );
860
	}
861
862
	function get_api_host() {
863
		if ( isset( $this->api_endpoint ) ) {
864
			return $this->api_endpoint;
865
		}
866
867
		//Check to see if we can use SSL
868
		$this->api_endpoint = Jetpack::fix_url_for_bad_hosts( JETPACK_PROTECT__API_HOST );
869
870
		return $this->api_endpoint;
871
	}
872
873
	function get_local_host() {
874
		if ( isset( $this->local_host ) ) {
875
			return $this->local_host;
876
		}
877
878
		$uri = 'http://' . strtolower( $_SERVER['HTTP_HOST'] );
879
880
		if ( is_multisite() ) {
881
			$uri = network_home_url();
882
		}
883
884
		$uridata = parse_url( $uri );
885
886
		$domain = $uridata['host'];
887
888
		// If we still don't have the site_url, get it
889
		if ( ! $domain ) {
890
			$uri     = get_site_url( 1 );
891
			$uridata = parse_url( $uri );
892
			$domain  = $uridata['host'];
893
		}
894
895
		$this->local_host = $domain;
896
897
		return $this->local_host;
898
	}
899
900
}
901
902
$jetpack_protect = Jetpack_Protect_Module::instance();
903
904
global $pagenow;
905
if ( isset( $pagenow ) && 'wp-login.php' == $pagenow ) {
906
	$jetpack_protect->check_login_ability();
907
}
908