| @@ -349,12 +349,24 @@ | ||
| 349 | 349 | |
| 350 | 350 | $code = isset( $_POST['vigilante_2fa_code'] ) ? sanitize_text_field( wp_unslash( $_POST['vigilante_2fa_code'] ) ) : ''; |
| 351 | 351 | $remember_device = ! empty( $_POST['vigilante_2fa_remember'] ); |
| 352 | 352 | |
| 353 | + // Read before verifying: running out of attempts clears the pending | |
| 354 | + // session inside verify_code(), and the redirect_to of the original | |
| 355 | + // login lives there. | |
| 356 | + $redirect_to = $this->pending_login_redirect(); | |
| 357 | + | |
| 353 | 358 | // Verify code |
| 354 | 359 | $result = $this->verify_code( $user_id, $code ); |
| 355 | 360 | |
| 356 | 361 | if ( is_wp_error( $result ) ) { |
| 362 | + // Out of attempts: the session is gone, so the form that would show | |
| 363 | + // this message is not painted any more. Say it on the login screen | |
| 364 | + // instead of bouncing the visitor there with no explanation. | |
| 365 | + if ( 'max_attempts' === $result->get_error_code() ) { | |
| 366 | + $this->redirect_to_login_with_notice( 'attempts' ); | |
| 367 | + } | |
| 368 | + | |
| 357 | 369 | // Store error for display |
| 358 | 370 | set_transient( 'vigilante_2fa_error_' . $user_id, $result->get_error_message(), 60 ); |
| 359 | 371 | |
| 360 | 372 | // Redirect back to login |
| @@ -380,11 +392,11 @@ | ||
| 380 | 392 | wp_set_auth_cookie( $user_id, false ); |
| 381 | 393 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- wp_login is a WordPress core hook that must be fired on login. |
| 382 | 394 | do_action( 'wp_login', $user->user_login, $user ); |
| 383 | 395 | |
| 384 | - // Redirect to admin dashboard (always use admin_url to avoid issues with popups, | |
| 385 | - // malformed URLs, or query parameters that could cause problems) | |
| 386 | - wp_safe_redirect( admin_url() ); | |
| 396 | + // Where the login was headed, or the dashboard. wp_validate_redirect() | |
| 397 | + // has already dropped anything off this site (pending_login_redirect()). | |
| 398 | + wp_safe_redirect( $redirect_to ); | |
| 387 | 399 | exit; |
| 388 | 400 | } |
| 389 | 401 | |
| 390 | 402 | /** |
| @@ -394,8 +406,13 @@ | ||
| 394 | 406 | * @param string $code Submitted code. |
| 395 | 407 | * @return true|WP_Error |
| 396 | 408 | */ |
| 397 | 409 | private function verify_code( $user_id, $code ) { |
| 410 | + // Mail clients and password managers show the code in groups and paste | |
| 411 | + // it with the separator. Until 2.11.12 that reached wp_hash() as typed | |
| 412 | + // and every correct code pasted that way came back "invalid". | |
| 413 | + $code = preg_replace( '/\D/', '', (string) $code ); | |
| 414 | + | |
| 398 | 415 | $stored = $this->database->get_2fa_code( $user_id ); |
| 399 | 416 | $user = get_user_by( 'ID', $user_id ); |
| 400 | 417 | |
| 401 | 418 | if ( ! $stored ) { |
| @@ -564,10 +581,10 @@ | ||
| 564 | 581 | name="vigilante_2fa_code" |
| 565 | 582 | id="vigilante_2fa_code" |
| 566 | 583 | class="input" |
| 567 | 584 | size="6" |
| 568 | - maxlength="6" | |
| 569 | - pattern="[0-9]{6}" | |
| 585 | + maxlength="20" | |
| 586 | + pattern="[0-9 -]{6,20}" | |
| 570 | 587 | inputmode="numeric" |
| 571 | 588 | autocomplete="one-time-code" |
| 572 | 589 | autofocus |
| 573 | 590 | required> |