| @@ -1216,8 +1216,20 @@ | ||
| 1216 | 1216 | } |
| 1217 | 1217 | } |
| 1218 | 1218 | } |
| 1219 | 1219 | |
| 1220 | + if ( wp_doing_ajax() && is_wp_error( $user ) && $this->wordfence_2fa_available() ) { | |
| 1221 | + $wfls_2fa = $this->check_wordfence_2fa( $user, $result ); | |
| 1222 | + if ( ! empty( $wfls_2fa ) ) { | |
| 1223 | + wp_send_json_success( | |
| 1224 | + array( | |
| 1225 | + 'html' => $wfls_2fa, | |
| 1226 | + 'is_2fa' => true, | |
| 1227 | + ) | |
| 1228 | + ); | |
| 1229 | + } | |
| 1230 | + } | |
| 1231 | + | |
| 1220 | 1232 | if ( is_wp_error( $user ) ) { |
| 1221 | 1233 | $message = aui()->alert( |
| 1222 | 1234 | array( |
| 1223 | 1235 | 'type' => 'error', |
| @@ -1436,8 +1448,110 @@ | ||
| 1436 | 1448 | |
| 1437 | 1449 | return ob_get_clean(); |
| 1438 | 1450 | } |
| 1439 | 1451 | |
| 1452 | + /** | |
| 1453 | + * Checks if the Wordfence Login Security module (2FA) is available. | |
| 1454 | + * | |
| 1455 | + * @since 1.2.5 | |
| 1456 | + * @package userswp | |
| 1457 | + * | |
| 1458 | + * @return bool | |
| 1459 | + */ | |
| 1460 | + public function wordfence_2fa_available() { | |
| 1461 | + return class_exists( '\WordfenceLS\Controller_Users' ) && class_exists( '\WordfenceLS\Controller_TOTP' ); | |
| 1462 | + } | |
| 1463 | + | |
| 1464 | + /** | |
| 1465 | + * Checks whether Wordfence's 2FA requires a verification code for the | |
| 1466 | + * failed login attempt and, if so, returns the markup for the code entry form. | |
| 1467 | + * | |
| 1468 | + * @since 1.2.5 | |
| 1469 | + * @package userswp | |
| 1470 | + * | |
| 1471 | + * @param WP_Error $error The error returned by wp_signon(). | |
| 1472 | + * @param array $result The validated login fields (username/password). | |
| 1473 | + * | |
| 1474 | + * @return string|void The 2FA form markup, or nothing if not applicable. | |
| 1475 | + */ | |
| 1476 | + public function check_wordfence_2fa( $error, $result ) { | |
| 1477 | + if ( 1 == uwp_get_option( 'disable_wordfence_2fa' ) ) { | |
| 1478 | + return; | |
| 1479 | + } | |
| 1480 | + | |
| 1481 | + if ( ! $this->wordfence_2fa_available() ) { | |
| 1482 | + return; | |
| 1483 | + } | |
| 1484 | + | |
| 1485 | + if ( ! is_wp_error( $error ) || 'wfls_twofactor_required' !== $error->get_error_code() ) { | |
| 1486 | + return; | |
| 1487 | + } | |
| 1488 | + | |
| 1489 | + $username = ! empty( $result['username'] ) ? $result['username'] : ''; | |
| 1490 | + if ( empty( $username ) ) { | |
| 1491 | + return; | |
| 1492 | + } | |
| 1493 | + | |
| 1494 | + $user = is_email( $username ) ? get_user_by( 'email', $username ) : get_user_by( 'login', $username ); | |
| 1495 | + if ( ! $user ) { | |
| 1496 | + return; | |
| 1497 | + } | |
| 1498 | + | |
| 1499 | + if ( ! \WordfenceLS\Controller_Users::shared()->has_2fa_active( $user ) ) { | |
| 1500 | + return; | |
| 1501 | + } | |
| 1502 | + | |
| 1503 | + if ( \WordfenceLS\Controller_Users::shared()->has_remembered_2fa( $user ) ) { | |
| 1504 | + return; | |
| 1505 | + } | |
| 1506 | + | |
| 1507 | + $login_nonce = wp_create_nonce( 'uwp-wfls-2fa-' . $user->ID ); | |
| 1508 | + | |
| 1509 | + ob_start(); | |
| 1510 | + ?> | |
| 1511 | + | |
| 1512 | + <div class="uwp-2fa-methods-wrap"> | |
| 1513 | + <form name="validate_2fa_form" id="validate_2fa_form" class="validate_2fa_form" action="" method="post" | |
| 1514 | + autocomplete="off"> | |
| 1515 | + <input type="hidden" name="provider" id="provider" value="wordfence"/> | |
| 1516 | + <input type="hidden" name="uwp-auth-id" id="uwp-auth-id" value="<?php echo esc_attr( $user->ID ); ?>"/> | |
| 1517 | + <input type="hidden" name="wp-auth-nonce" id="wp-auth-nonce" | |
| 1518 | + value="<?php echo esc_attr( $login_nonce ); ?>"/> | |
| 1519 | + | |
| 1520 | + <p><?php esc_html_e( 'Please enter the authentication code from your two-factor authentication app, or a recovery code, to login:', 'userswp' ); ?></p> | |
| 1521 | + | |
| 1522 | + <?php | |
| 1523 | + echo aui()->input( | |
| 1524 | + array( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 1525 | + 'type' => 'text', | |
| 1526 | + 'id' => 'authcode', | |
| 1527 | + 'name' => 'authcode', | |
| 1528 | + 'placeholder' => esc_attr__( 'Authentication Code', 'userswp' ), | |
| 1529 | + 'value' => '', | |
| 1530 | + 'label' => esc_html__( 'Authentication Code', 'userswp' ), | |
| 1531 | + 'extra_attributes' => array( | |
| 1532 | + 'autocomplete' => 'one-time-code', | |
| 1533 | + ), | |
| 1534 | + ) | |
| 1535 | + ); | |
| 1536 | + | |
| 1537 | + echo aui()->button( | |
| 1538 | + array( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 1539 | + 'type' => 'submit', | |
| 1540 | + 'class' => 'btn btn-primary btn-block text-uppercase uwp-2fa-submit', | |
| 1541 | + 'name' => 'submit', | |
| 1542 | + 'icon' => '', | |
| 1543 | + 'content' => esc_html__( 'Log In', 'userswp' ), | |
| 1544 | + ) | |
| 1545 | + ); | |
| 1546 | + ?> | |
| 1547 | + </form> | |
| 1548 | + </div> | |
| 1549 | + | |
| 1550 | + <?php | |
| 1551 | + return ob_get_clean(); | |
| 1552 | + } | |
| 1553 | + | |
| 1440 | 1554 | public function get_wp2fa_provider_for_user( $user ) { |
| 1441 | 1555 | if ( class_exists( '\WP2FA\Authenticator\Login' ) && method_exists( '\WP2FA\Authenticator\Login', 'get_available_providers_for_user' ) ) { |
| 1442 | 1556 | $provider = \WP2FA\Authenticator\Login::get_available_providers_for_user( $user ); |
| 1443 | 1557 | if ( is_array( $provider ) ) { |
| @@ -1501,8 +1615,77 @@ | ||
| 1501 | 1615 | |
| 1502 | 1616 | return false; |
| 1503 | 1617 | } |
| 1504 | 1618 | |
| 1619 | + /** | |
| 1620 | + * Validates the Wordfence 2FA code submitted from the uwp-2fa form and, | |
| 1621 | + * if valid, completes the login by setting the auth cookie. | |
| 1622 | + * | |
| 1623 | + * @since 1.2.5 | |
| 1624 | + * @package userswp | |
| 1625 | + * | |
| 1626 | + * @param WP_User $user The user attempting to complete 2FA login. | |
| 1627 | + * | |
| 1628 | + * @return void | |
| 1629 | + */ | |
| 1630 | + public function process_login_wordfence_2fa( $user ) { | |
| 1631 | + if ( ! $this->wordfence_2fa_available() ) { | |
| 1632 | + $message = aui()->alert( | |
| 1633 | + array( | |
| 1634 | + 'type' => 'error', | |
| 1635 | + 'content' => __( 'Invalid request! Please try again.', 'userswp' ), | |
| 1636 | + ) | |
| 1637 | + ); | |
| 1638 | + | |
| 1639 | + wp_send_json_error( array( 'message' => $message ) ); | |
| 1640 | + } | |
| 1641 | + | |
| 1642 | + $nonce = ( isset( $_POST['wp-auth-nonce'] ) ) ? sanitize_textarea_field( wp_unslash( $_POST['wp-auth-nonce'] ) ) : ''; | |
| 1643 | + | |
| 1644 | + if ( ! wp_verify_nonce( $nonce, 'uwp-wfls-2fa-' . $user->ID ) ) { | |
| 1645 | + $message = aui()->alert( | |
| 1646 | + array( | |
| 1647 | + 'type' => 'error', | |
| 1648 | + 'content' => __( 'Invalid request! Please try again.', 'userswp' ), | |
| 1649 | + ) | |
| 1650 | + ); | |
| 1651 | + | |
| 1652 | + wp_send_json_error( array( 'message' => $message ) ); | |
| 1653 | + } | |
| 1654 | + | |
| 1655 | + $code = isset( $_POST['authcode'] ) ? sanitize_text_field( wp_unslash( $_POST['authcode'] ) ) : ''; | |
| 1656 | + | |
| 1657 | + if ( empty( $code ) || true !== \WordfenceLS\Controller_TOTP::shared()->validate_2fa( $user, $code ) ) { | |
| 1658 | + do_action( 'wp_login_failed', $user->user_login ); | |
| 1659 | + | |
| 1660 | + $message = aui()->alert( | |
| 1661 | + array( | |
| 1662 | + 'type' => 'error', | |
| 1663 | + 'content' => __( 'Invalid verification code.', 'userswp' ), | |
| 1664 | + ) | |
| 1665 | + ); | |
| 1666 | + | |
| 1667 | + wp_send_json_error( array( 'message' => $message ) ); | |
| 1668 | + } | |
| 1669 | + | |
| 1670 | + $remember = ( isset( $_REQUEST['rememberme'] ) ) ? filter_var( $_REQUEST['rememberme'], FILTER_VALIDATE_BOOLEAN ) : false; | |
| 1671 | + | |
| 1672 | + // Complete the login the same way wp_signon() would have, now that 2FA has been verified. | |
| 1673 | + wp_set_auth_cookie( $user->ID, $remember ); | |
| 1674 | + wp_set_current_user( $user->ID ); | |
| 1675 | + | |
| 1676 | + do_action( 'wp_login', $user->user_login, $user ); | |
| 1677 | + | |
| 1678 | + $message = aui()->alert( | |
| 1679 | + array( | |
| 1680 | + 'type' => 'success', | |
| 1681 | + 'content' => __( 'Validation successful. Redirecting...', 'userswp' ), | |
| 1682 | + ) | |
| 1683 | + ); | |
| 1684 | + | |
| 1685 | + wp_send_json_success( array( 'message' => $message ) ); | |
| 1686 | + } | |
| 1687 | + | |
| 1505 | 1688 | public function process_login_2fa() { |
| 1506 | 1689 | global $wp2fa; |
| 1507 | 1690 | |
| 1508 | 1691 | if ( ! isset( $_POST['uwp-auth-id'], $_POST['wp-auth-nonce'] ) ) { |
| @@ -1522,8 +1705,20 @@ | ||
| 1522 | 1705 | |
| 1523 | 1706 | wp_send_json_error( array( 'message' => $message ) ); |
| 1524 | 1707 | } |
| 1525 | 1708 | |
| 1709 | + if ( isset( $_POST['provider'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1710 | + $provider = sanitize_textarea_field( wp_unslash( $_POST['provider'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1711 | + } else { | |
| 1712 | + $provider = ''; | |
| 1713 | + } | |
| 1714 | + | |
| 1715 | + if ( 'wordfence' === $provider ) { | |
| 1716 | + $this->process_login_wordfence_2fa( $user ); | |
| 1717 | + | |
| 1718 | + return; | |
| 1719 | + } | |
| 1720 | + | |
| 1526 | 1721 | $nonce = ( isset( $_POST['wp-auth-nonce'] ) ) ? sanitize_textarea_field( wp_unslash( $_POST['wp-auth-nonce'] ) ) : ''; |
| 1527 | 1722 | |
| 1528 | 1723 | if ( true !== \WP2FA\Authenticator\Login::verify_login_nonce( $user->ID, $nonce ) ) { |
| 1529 | 1724 | $message = aui()->alert( |
| @@ -1535,14 +1730,8 @@ | ||
| 1535 | 1730 | |
| 1536 | 1731 | wp_send_json_error( array( 'message' => $message ) ); |
| 1537 | 1732 | } |
| 1538 | 1733 | |
| 1539 | - if ( isset( $_POST['provider'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1540 | - $provider = sanitize_textarea_field( wp_unslash( $_POST['provider'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1541 | - } else { | |
| 1542 | - $provider = ''; | |
| 1543 | - } | |
| 1544 | - | |
| 1545 | 1734 | $error = ''; |
| 1546 | 1735 | |
| 1547 | 1736 | try { |
| 1548 | 1737 | $is_enabled = \WP2FA\Admin\Controllers\Settings::is_provider_enabled_for_role( \WP2FA\Admin\Helpers\User_Helper::get_user_role( $user ), $provider ); |
| @@ -2127,8 +2316,21 @@ | ||
| 2127 | 2316 | unset( $uploads_result[ $upload_file_key ] ); |
| 2128 | 2317 | } |
| 2129 | 2318 | } |
| 2130 | 2319 | |
| 2320 | + global $wpdb; | |
| 2321 | + $file_field_names = $wpdb->get_col( | |
| 2322 | + $wpdb->prepare( | |
| 2323 | + "SELECT htmlvar_name FROM " . uwp_get_table_prefix() . "uwp_form_fields WHERE form_type = %s AND field_type IN ('file','image')", | |
| 2324 | + 'account' | |
| 2325 | + ) | |
| 2326 | + ); | |
| 2327 | + foreach ( $file_field_names as $file_field_name ) { | |
| 2328 | + if ( isset( $result[ $file_field_name ] ) && ! isset( $uploads_result[ $file_field_name ] ) ) { | |
| 2329 | + unset( $result[ $file_field_name ] ); | |
| 2330 | + } | |
| 2331 | + } | |
| 2332 | + | |
| 2131 | 2333 | $result = array_merge( $result, $uploads_result ); |
| 2132 | 2334 | |
| 2133 | 2335 | $args = array( |
| 2134 | 2336 | 'ID' => get_current_user_id(), |
| @@ -2427,17 +2629,24 @@ | ||
| 2427 | 2629 | } |
| 2428 | 2630 | |
| 2429 | 2631 | $unlink_file = untrailingslashit( $upload_path ) . '/' . trim( $value, '/\\' ); |
| 2430 | 2632 | |
| 2431 | - if ( is_file( $unlink_file ) && file_exists( $unlink_file ) ) { | |
| 2432 | - wp_delete_file( $unlink_file ); | |
| 2633 | + // Canonicalize and enforce containment inside the uploads directory before deleting. | |
| 2634 | + $real_upload_path = realpath( $upload_path ); | |
| 2635 | + $real_unlink_file = realpath( $unlink_file ); | |
| 2433 | 2636 | |
| 2637 | + if ( $real_upload_path && $real_unlink_file && is_file( $real_unlink_file ) | |
| 2638 | + && strpos( $real_unlink_file, $real_upload_path . DIRECTORY_SEPARATOR ) === 0 ) { | |
| 2639 | + wp_delete_file( $real_unlink_file ); | |
| 2640 | + | |
| 2434 | 2641 | // For avatar/banner, also remove the original (non-thumb) file. |
| 2435 | 2642 | if ( $type ) { |
| 2436 | - $unlink_ori_file = str_replace( '_uwp_' . $type . '_thumb' . '.', '.', $unlink_file ); | |
| 2643 | + $unlink_ori_file = str_replace( '_uwp_' . $type . '_thumb' . '.', '.', $real_unlink_file ); | |
| 2644 | + $real_unlink_ori_file = realpath( $unlink_ori_file ); | |
| 2437 | 2645 | |
| 2438 | - if ( is_file( $unlink_ori_file ) && file_exists( $unlink_ori_file ) ) { | |
| 2439 | - wp_delete_file( $unlink_ori_file ); | |
| 2646 | + if ( $real_unlink_ori_file && is_file( $real_unlink_ori_file ) | |
| 2647 | + && strpos( $real_unlink_ori_file, $real_upload_path . DIRECTORY_SEPARATOR ) === 0 ) { | |
| 2648 | + wp_delete_file( $real_unlink_ori_file ); | |
| 2440 | 2649 | } |
| 2441 | 2650 | } |
| 2442 | 2651 | } |
| 2443 | 2652 | } |