ew Brute_Force_Protection_Math_Authenticate(); return false; } /** * Kill a login attempt */ public function kill_login() { if ( isset( $_GET['action'] ) && isset( $_GET['_wpnonce'] ) && 'logout' === $_GET['action'] && wp_verify_nonce( $_GET['_wpnonce'], 'log-out' ) && // phpcs:ignore WordPress.Security.ValidatedSanitizedInput wp_get_current_user() ) { // Allow users to logout. return; } $ip = IP_Utils::get_ip(); /** * Fires before every killed login. * * @module protect * * @since 3.4.0 * * @param string $ip IP flagged by Protect. */ do_action( 'jpp_kill_login', $ip ); if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { // translators: variable is the IP address that was flagged. $die_string = sprintf( __( 'Your IP (%1$s) has been flagged for potential security violations.', 'jetpack-waf' ), str_replace( 'http://', '', esc_url( 'http://' . $ip ) ) ); wp_die( $die_string, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_url used when forming string. esc_html__( 'Login Blocked by Jetpack', 'jetpack-waf' ), array( 'response' => 403 ) ); } $blocked_login_page = Brute_Force_Protection_Blocked_Login_Page::instance( $ip ); if ( $blocked_login_page->is_blocked_user_valid() ) { return; } $blocked_login_page->render_and_die(); } /** * Checks if the protect API call has failed, and if so initiates the math captcha fallback. */ public function check_use_math() { $use_math = $this->get_transient( 'brute_use_math' ); if ( $use_math ) { new Brute_Force_Protection_Math_Authenticate(); } } /** * If we're in a multisite network, return the blog ID of the primary blog * * @return int */ public function get_main_blog_id() { if ( ! is_multisite() ) { return false; } global $current_site; $primary_blog_id = $current_site->blog_id; return $primary_blog_id; } /** * Get jetpack blog id, or the jetpack blog id of the main blog in the main network * * @return int */ public function get_main_blog_jetpack_id() { if ( ! is_main_site() ) { switch_to_blog( $this->get_main_blog_id() ); $id = Jetpack_Options::get_option( 'id', false ); restore_current_blog(); } else { $id = Jetpack_Options::get_option( 'id' ); } return $id; } /** * Checks the API key. */ public function check_api_key() { $response = $this->protect_call( 'check_key' ); if ( isset( $response['ckval'] ) ) { return true; } if ( isset( $response['error'] ) ) { if ( 'Invalid API Key' === $response['error'] ) { $this->api_key_error = __( 'Your API key is invalid', 'jetpack-waf' ); } if ( 'API Key Required' === $response['error'] ) { $this->api_key_error = __( 'No API key', 'jetpack-waf' ); } } $this->api_key_error = __( 'There was an error contacting Jetpack servers.', 'jetpack-waf' ); return false; } /** * Calls over to the api using wp_remote_post * * @param string $action - 'check_ip', 'check_key', or 'failed_attempt'. * @param array $request - Any custom data to post to the api. * * @return array */ public function protect_call( $action = 'check_ip', $request = array() ) { global $wp_version; $api_key = $this->maybe_get_protect_key(); $user_agent = "WordPress/{$wp_version}"; $request['action'] = $action; $request['ip'] = IP_Utils::get_ip(); $request['host'] = $this->get_local_host(); $request['headers'] = wp_json_encode( $this->get_headers() ); $request['jetpack_version'] = null; $request['wordpress_version'] = (string) $wp_version; $request['api_key'] = $api_key; $request['multisite'] = '0'; if ( defined( 'JETPACK__VERSION' ) ) { $request['jetpack_version'] = constant( 'JETPACK__VERSION' ); $user_agent .= ' | Jetpack/' . constant( 'JETPACK__VERSION' ); } if ( defined( 'JETPACK_PROTECT_VERSION' ) && ! defined( 'JETPACK__VERSION' ) ) { $request['jetpack_version'] = '12.1'; $user_agent .= ' | JetpackProtect/' . constant( 'JETPACK_PROTECT_VERSION' ); } if ( is_multisite() ) { $request['multisite'] = get_blog_count(); } /** * Filter controls maximum timeout in waiting for reponse from Protect servers. * * @module protect * * @since 4.0.4 * * @param int $timeout Max time (in seconds) to wait for a response. */ $timeout = apply_filters( 'jetpack_protect_connect_timeout', 30 ); $args = array( 'body' => $request, 'user-agent' => $user_agent, 'httpversion' => '1.0', 'timeout' => absint( $timeout ), ); Waf_Constants::define_brute_force_api_host(); $response_json = wp_remote_post( JETPACK_PROTECT__API_HOST, $args ); $this->last_response_raw = $response_json; $transient_name = $this->get_transient_name(); $this->delete_transient( $transient_name ); if ( is_array( $response_json ) ) { $response = json_decode( $response_json['body'], true ); } if ( isset( $response['blocked_attempts'] ) && $response['blocked_attempts'] ) { update_site_option( 'jetpack_protect_blocked_attempts', $response['blocked_attempts'] ); } if ( isset( $response['status'] ) && ! isset( $response['error'] ) ) { $response['expire'] = time() + $response['seconds_remaining']; $this->set_transient( $transient_name, $response, $response['seconds_remaining'] ); $this->delete_transient( 'brute_use_math' ); } else { // Fallback to Math Captcha if no response from API host. $this->set_transient( 'brute_use_math', 1, 600 ); $response['status'] = 'ok'; $response['math'] = true; } if ( isset( $response['error'] ) ) { update_site_option( 'jetpack_protect_error', $response['error'] ); } else { delete_site_option( 'jetpack_protect_error' ); } return $response; } /** * Gets the transient name. */ public function get_transient_name() { $headers = $this->get_headers(); $header_hash = md5( wp_json_encode( $headers ) ); return 'jpp_li_' . $header_hash; } /** * Wrapper for WordPress set_transient function, our version sets * the transient on the main site in the network if this is a multisite network * * We do it this way (instead of set_site_transient) because of an issue where * sitewide transients are always autoloaded * https://core.trac.wordpress.org/ticket/22846 * * @param string $transient Transient name. Expected to not be SQL-escaped. Must be * 45 characters or fewer in length. * @param mixed $value Transient value. Must be serializable if non-scalar. * Expected to not be SQL-escaped. * @param int $expiration Optional. Time until expiration in seconds. Default 0. * * @return bool False if value was not set and true if value was set. */ public function set_transient( $transient, $value, $expiration ) { if ( is_multisite() && ! is_main_site() ) { switch_to_blog( $this->get_main_blog_id() ); $return = set_transient( $transient, $value, $expiration ); restore_current_blog(); return $return; } return set_transient( $transient, $value, $expiration ); } /** * Wrapper for WordPress delete_transient function, our version deletes * the transient on the main site in the network if this is a multisite network * * @param string $transient Transient name. Expected to not be SQL-escaped. * * @return bool true if successful, false otherwise */ public function delete_transient( $transient ) { if ( is_multisite() && ! is_main_site() ) { switch_to_blog( $this->get_main_blog_id() ); $return = delete_transient( $transient ); restore_current_blog(); return $return; } return delete_transient( $transient ); } /** * Wrapper for WordPress get_transient function, our version gets * the transient on the main site in the network if this is a multisite network * * @param string $transient Transient name. Expected to not be SQL-escaped. * * @return mixed Value of transient. */ public function get_transient( $transient ) { if ( is_multisite() && ! is_main_site() ) { switch_to_blog( $this->get_main_blog_id() ); $return = get_transient( $transient ); restore_current_blog(); return $return; } return get_transient( $transient ); } /** * Returns the local host. */ public function get_local_host() { if ( isset( $this->local_host ) ) { return $this->local_host; } $uri = 'http://' . strtolower( isset( $_SERVER['HTTP_HOST'] ) ? filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '' ); if ( is_multisite() ) { $uri = network_home_url(); } $uridata = wp_parse_url( $uri ); $domain = $uridata['host']; // If we still don't have the site_url, get it. if ( ! $domain ) { $uri = get_site_url( 1 ); $uridata = wp_parse_url( $uri ); $domain = $uridata['host']; } $this->local_host = $domain; return $this->local_host; } }