Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add/protect blocked send email #8117

Merged
merged 13 commits into from
Nov 13, 2017
30 changes: 18 additions & 12 deletions modules/protect.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ private function __construct() {
add_action( 'admin_init', array ( $this, 'maybe_display_security_warning' ) );

// This is a backup in case $pagenow fails for some reason
add_action( 'login_head', array ( $this, 'check_login_ability' ) );
add_action( 'login_head', array ( $this, 'check_login_ability' ), 100, 3 );

// Runs a script every day to clean up expired transients so they don't
// clog up our users' databases
Expand Down Expand Up @@ -541,7 +541,7 @@ function block_with_math() {
* Kill a login attempt
*/
function kill_login() {
$ip = jetpack_protect_get_ip();
$ip = jetpack_protect_get_ip();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the spacing off here?

/**
* Fires before every killed login.
*
Expand All @@ -552,19 +552,24 @@ function kill_login() {
* @param string $ip IP flagged by Protect.
*/
do_action( 'jpp_kill_login', $ip );
$help_url = 'https://jetpack.com/support/security-features/#unblock';

$die_string = sprintf( __( 'Your IP (%1$s) has been flagged for potential security violations. <a href="%2$s">Find out more...</a>', 'jetpack' ), str_replace( 'http://', '', esc_url( 'http://' . $ip ) ), esc_url( $help_url ) );

if( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
$die_string = sprintf( __( 'Your IP (%1$s) has been flagged for potential security violations.', 'jetpack' ), str_replace( 'http://', '', esc_url( 'http://' . $ip ) ) );
wp_die(
$die_string,
__( 'Login Blocked by Jetpack', 'jetpack' ),
array ( 'response' => 403 )
);
}

wp_die(
$die_string,
__( 'Login Blocked by Jetpack', 'jetpack' ),
array ( 'response' => 403 )
);
require_once dirname( __FILE__ ) . '/protect/blocked-login-page.php';
$blocked_login_page = Jetpack_Protect_Blocked_Login_Page::instance( $ip );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spacing appears off.


if ( $blocked_login_page->is_blocked_user_valid() ) {
return;
}

$blocked_login_page->render_and_die();
}

/*
Expand Down Expand Up @@ -862,8 +867,9 @@ function get_local_host() {

}

Jetpack_Protect_Module::instance();
$jetpack_protect = Jetpack_Protect_Module::instance();

global $pagenow;
if ( isset( $pagenow ) && 'wp-login.php' == $pagenow ) {
Jetpack_Protect_Module::check_login_ability();
$jetpack_protect->check_login_ability();
}
299 changes: 299 additions & 0 deletions modules/protect/blocked-login-page.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
<?php


/**
* Class Jetpack_Protect_Blocked_Login_Page
*
* Instanciated on the wp-login page when Jetpack modules are loaded and $pagenow
* is available, or during the login_head hook.
*
* Class will only be instanciated if Protect has detected a hard blocked IP address.
*
*
*/
class Jetpack_Protect_Blocked_Login_Page {

private static $__instance = null;
public $can_send_recovery_emails;
public $ip_address;
public $valid_blocked_user_id;
public $page_title;
public $email_address;
public $help_url = 'https://jetpack.com/support/security-features/#unblock';

/**
* Singleton implementation
*
* @return object
*/
public static function instance( $ip_address ) {
if ( ! is_a( self::$__instance, 'Jetpack_Protect_Blocked_Login_Page' ) ) {
self::$__instance = new Jetpack_Protect_Blocked_Login_Page( $ip_address );
}

return self::$__instance;
}


function __construct( $ip_address ) {
/**
* Filter controls if an email recovery form is shown to blocked IPs.
*
* A recovery form allows folks to re-gain access to the login form
* via an email link if their IP was mistakenly blocked.
*
* @module protect
*
* @since 5.6
*
* @param bool $can_send_recovery_emails Defaults to true.
*/
$this->can_send_recovery_emails = apply_filters( 'jetpack_protect_can_send_recovery_emails', true );
$this->ip_address = $ip_address;

add_filter( 'wp_authenticate_user', array( $this, 'check_valid_blocked_user' ), 10, 1 );
add_filter( 'site_url', array( $this, 'add_args_to_login_post_url' ), 10, 3 );
add_filter( 'network_site_url', array( $this, 'add_args_to_login_post_url' ), 10, 3 );
add_filter( 'lostpassword_url', array( $this, 'add_args_to_lostpassword_url' ), 10, 2 );
add_filter( 'login_url', array( $this, 'add_args_to_login_url' ), 10,3 );
add_filter( 'lostpassword_redirect', array( $this, 'add_args_to_lostpassword_redirect_url' ), 10, 1 );
}

public function add_args_to_lostpassword_redirect_url( $url ) {
if ( $this->valid_blocked_user_id ) {
$url = ( empty( $url ) ) ? wp_login_url() : $url;
$url = add_query_arg(
array(
'validate_jetpack_protect_recovery' => $_GET['validate_jetpack_protect_recovery'],
'user_id' => $_GET['user_id'],
'checkemail' => 'confirm',
),
$url
);
}
return $url;
}

public function add_args_to_lostpassword_url( $url, $redirect ) {
if ( $this->valid_blocked_user_id ) {
$args = array(
'validate_jetpack_protect_recovery' => $_GET['validate_jetpack_protect_recovery'],
'user_id' => $_GET['user_id'],
'action' => 'lostpassword',
);
if ( ! empty( $redirect ) ) {
$args['redirect_to'] = $redirect;
}
$url = add_query_arg( $args, $url );
}
return $url;
}

public function add_args_to_login_post_url( $url, $path, $scheme ) {
if ( $this->valid_blocked_user_id && ( 'login_post' === $scheme || 'login' === $scheme ) ) {
$url = add_query_arg(
array(
'validate_jetpack_protect_recovery' => $_GET['validate_jetpack_protect_recovery'],
'user_id' => $_GET['user_id'],
),
$url
);

}
return $url;
}

public function add_args_to_login_url( $url, $redirect, $force_reauth ) {
if ( $this->valid_blocked_user_id ) {
$args = array(
'validate_jetpack_protect_recovery' => $_GET['validate_jetpack_protect_recovery'],
'user_id' => $_GET['user_id'],
);

if ( ! empty( $redirect ) ) {
$args['redirect_to'] = $redirect;
}

if ( ! empty( $force_reauth ) ) {
$args['reauth'] = '1';
}
$url = add_query_arg( $args, $url );
}
return $url;
}

public function check_valid_blocked_user( $user ) {
if ( $this->valid_blocked_user_id && $this->valid_blocked_user_id != $user->ID ) {
return new WP_Error( 'invalid_recovery_token', __( 'The recovery token is not valid for this user.', 'jetpack' ) );
}
return $user;
}

public function is_blocked_user_valid() {
if ( ! $this->can_send_recovery_emails ) {
return false;
}

if ( $this->valid_blocked_user_id ) {
return true;
}

if ( ! isset( $_GET['validate_jetpack_protect_recovery'], $_GET['user_id'] ) ) {
return false;
}

if ( ! $this->is_valid_protect_recovery_key( $_GET['validate_jetpack_protect_recovery'], $_GET['user_id'] ) ) {
return false;
}

$this->valid_blocked_user_id = (int) $_GET['user_id'];
return true;
}

public function is_valid_protect_recovery_key( $key, $user_id ) {

$path = sprintf( '/sites/%d/protect/recovery/confirm', Jetpack::get_option( 'id' ) );
$response = Jetpack_Client::wpcom_json_api_request_as_blog(
$path,
'1.1',
array(
'method' => 'post'
),
array(
'token' => $key,
'user_id' => $user_id,
'ip' => $this->ip_address,
)
);

$result = json_decode( wp_remote_retrieve_body( $response ) );

if ( is_wp_error( $result ) || empty( $result ) || isset( $result->error ) ) {
return false;
}
return true;
}

public function render_and_die() {
$this->page_title = __( 'Login Blocked by Jetpack', 'jetpack' );

if ( ! $this->can_send_recovery_emails ) {
$this->render_blocked_login_message();
return;
}

if ( isset( $_GET['validate_jetpack_protect_recovery'] ) && $_GET['user_id'] ) {
$this->protect_die( __( 'Could not validate recovery token.', 'jetpack' ) );
return;
}

if (
isset( $_GET['jetpack-protect-recovery'] ) &&
isset( $_POST['_wpnonce'] ) &&
wp_verify_nonce( $_POST['_wpnonce'], 'bypass-protect' )
) {
$this->process_recovery_email();
return;
}

$this->render_recovery_form();
}

public function render_blocked_login_message() {
$this->protect_die( $this->get_html_blocked_login_message() );
}

function process_recovery_email() {
$sent = $this->send_recovery_email();

if ( is_wp_error( $sent ) ) {
$this->protect_die( $sent, true );
} else {
$this->render_recovery_success();
}
}

function send_recovery_email() {
$email = isset( $_POST['email'] ) ? $_POST['email'] : '';
if ( sanitize_email( $email ) !== $email || ! is_email( $email ) ) {
return new WP_Error( 'invalid_email', __( "Oops, looks like that's not the right email address. Please try again!", 'jetpack' ) );
}
$user = get_user_by('email', trim( $email ) );

if ( ! $user ) {
return new WP_Error( 'invalid_user', __( 'Oops, could not find a user with that email address.', 'jetpack' ) );
}
$this->email_address = $email;
$path = sprintf( '/sites/%d/protect/recovery/request', Jetpack::get_option( 'id' ) );


$response = Jetpack_Client::wpcom_json_api_request_as_blog(
$path,
'1.1',
array(
'method' => 'post'
),
array(
'user_id' => $user->ID,
'ip' => $this->ip_address
)
);

$code = wp_remote_retrieve_response_code( $response );
$result = json_decode( wp_remote_retrieve_body( $response ) );

if ( 429 === $code ) {
return new WP_Error( 'email_already_sent', __( 'An email was already sent to this address.', 'jetpack' ) );
} else if ( is_wp_error( $result ) || empty( $result ) || isset( $result->error ) ) {
return new WP_Error( 'email_send_error', __( 'There was an error sending your email.', 'jetpack' ) );
}
return true;
}

function protect_die( $content, $back_link = false ) {
$image = sprintf(
'<img src="%s" width="180" style="display: block; margin: 0 auto;" />',
plugins_url( 'modules/protect/jetpack-security.png', JETPACK__PLUGIN_FILE )
);

if ( is_wp_error( $content ) ) {
$content = $content->get_error_message();
}
// hack to get around default wp_die_handler. https://core.trac.wordpress.org/browser/tags/4.8.1/src/wp-includes/functions.php#L2698
$content = $image . '</p> ' . $content . '<p>';

wp_die( $content, $this->page_title, array( 'back_link' => $back_link ) );
}

function render_recovery_form() {
$content = $this->get_html_blocked_login_message() . $this->get_html_recovery_form();
$this->protect_die( $content );
}

function render_recovery_success() {
$this->protect_die( sprintf( __( 'An email with recovery instructions was sent to %s.', 'jetpack' ), $this->email_address ) );
}

function get_html_blocked_login_message() {
return sprintf(
__( '<p>Your IP (%1$s) has been flagged for potential security violations.</p>', 'jetpack' ),
str_replace( 'http://', '', esc_url( 'http://' . $this->ip_address )
) );
}

function get_html_recovery_form() {
ob_start(); ?>
<p><?php _e( 'Email yourself a special link to regain access the login form.', 'jetpack' ); ?></p>
<form method="post" action="?jetpack-protect-recovery=true">
<?php echo wp_nonce_field( 'bypass-protect' ); ?>
<label for="email">Email Address:</label>
<input type="email" name="email" />
<input type="submit" value="<?php echo esc_attr( __( 'Send', 'jetpack' ) ); ?>" />
</form>

<?php
$contents = ob_get_contents();
ob_end_clean();
return $contents;
}
}
Binary file added modules/protect/jetpack-security.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.