diff --git a/class-two-factor-core.php b/class-two-factor-core.php index d98cbfe6..0187ae7d 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -529,7 +529,7 @@ public static function get_user_two_factor_revalidate_url( $interim = false ) { * @return boolean */ public static function is_valid_user_action( $user_id, $action ) { - $request_nonce = isset( $_REQUEST[ self::USER_SETTINGS_ACTION_NONCE_QUERY_ARG ] ) ? wp_unslash( $_REQUEST[ self::USER_SETTINGS_ACTION_NONCE_QUERY_ARG ] ) : ''; + $request_nonce = isset( $_REQUEST[ self::USER_SETTINGS_ACTION_NONCE_QUERY_ARG ] ) ? wp_unslash( $_REQUEST[ self::USER_SETTINGS_ACTION_NONCE_QUERY_ARG ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Value only passed to wp_verify_nonce(). if ( ! $user_id || ! $action || ! $request_nonce ) { return false; @@ -550,8 +550,8 @@ public static function is_valid_user_action( $user_id, $action ) { */ public static function current_user_being_edited() { // Try to resolve the user ID from the request first. - if ( ! empty( $_REQUEST['user_id'] ) ) { - $user_id = intval( $_REQUEST['user_id'] ); + if ( ! empty( $_REQUEST['user_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in trigger_user_settings_action() via is_valid_user_action() before any state change. + $user_id = intval( $_REQUEST['user_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in trigger_user_settings_action() via is_valid_user_action() before any state change. if ( current_user_can( 'edit_user', $user_id ) ) { return $user_id; @@ -570,7 +570,7 @@ public static function current_user_being_edited() { * @return void */ public static function trigger_user_settings_action() { - $action = isset( $_REQUEST[ self::USER_SETTINGS_ACTION_QUERY_VAR ] ) ? wp_unslash( $_REQUEST[ self::USER_SETTINGS_ACTION_QUERY_VAR ] ) : ''; + $action = isset( $_REQUEST[ self::USER_SETTINGS_ACTION_QUERY_VAR ] ) ? wp_unslash( $_REQUEST[ self::USER_SETTINGS_ACTION_QUERY_VAR ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified in is_valid_user_action() before do_action. $user_id = self::current_user_being_edited(); if ( self::is_valid_user_action( $user_id, $action ) ) { @@ -984,7 +984,7 @@ public static function show_two_factor_login( $user ) { wp_die( esc_html__( 'Failed to create a login nonce.', 'two-factor' ) ); } - $redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : admin_url(); + $redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : admin_url(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Value only used for redirect; auth protected by 2FA login nonce later. self::login_html( $user, $login_nonce['key'], $redirect_to ); } @@ -1038,6 +1038,12 @@ public static function maybe_show_reset_password_notice( $errors ) { return $errors; } + // Verify login form nonce when present (e.g. wp-login.php); skip only when nonce is not sent (custom login forms). + if ( isset( $_POST['_wpnonce'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'log-in' ) ) { + return $errors; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above when _wpnonce present; absent for custom login forms. $user_name = sanitize_user( wp_unslash( $_POST['log'] ) ); $attempted_user = get_user_by( 'login', $user_name ); if ( ! $attempted_user && str_contains( $user_name, '@' ) ) { @@ -1558,11 +1564,11 @@ public static function rest_api_can_edit_user_and_update_two_factor_options( $us * @since 0.2.0 */ public static function login_form_validate_2fa() { - $wp_auth_id = ! empty( $_REQUEST['wp-auth-id'] ) ? absint( $_REQUEST['wp-auth-id'] ) : 0; - $nonce = ! empty( $_REQUEST['wp-auth-nonce'] ) ? wp_unslash( $_REQUEST['wp-auth-nonce'] ) : ''; - $provider = ! empty( $_REQUEST['provider'] ) ? wp_unslash( $_REQUEST['provider'] ) : ''; - $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? wp_unslash( $_REQUEST['redirect_to'] ) : ''; - $is_post_request = ( 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ); + $wp_auth_id = ! empty( $_REQUEST['wp-auth-id'] ) ? absint( $_REQUEST['wp-auth-id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_validate_2fa() via verify_login_nonce() before any use. + $nonce = ! empty( $_REQUEST['wp-auth-nonce'] ) ? wp_unslash( $_REQUEST['wp-auth-nonce'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified in _login_form_validate_2fa() before any use. + $provider = ! empty( $_REQUEST['provider'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['provider'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_validate_2fa() before any use. + $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_validate_2fa() before any use. + $is_post_request = isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- REQUEST_METHOD is not user input. $user = get_user_by( 'id', $wp_auth_id ); if ( ! $wp_auth_id || ! $nonce || ! $user ) { @@ -1624,7 +1630,7 @@ public static function _login_form_validate_2fa( $user, $nonce = '', $provider = delete_user_meta( $user->ID, self::USER_FAILED_LOGIN_ATTEMPTS_KEY ); $rememberme = false; - if ( isset( $_REQUEST['rememberme'] ) && $_REQUEST['rememberme'] ) { + if ( isset( $_REQUEST['rememberme'] ) && $_REQUEST['rememberme'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Request read only after successful verify_login_nonce() in this request. $rememberme = true; } @@ -1665,7 +1671,7 @@ public static function _login_form_validate_2fa( $user, $nonce = '', $provider = $interim_login = isset( $_REQUEST['interim-login'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited,WordPress.Security.NonceVerification.Recommended if ( $interim_login ) { - $customize_login = isset( $_REQUEST['customize-login'] ); + $customize_login = isset( $_REQUEST['customize-login'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Request read only after successful verify_login_nonce() in this request. if ( $customize_login ) { wp_enqueue_script( 'customize-base' ); wp_add_inline_script( @@ -1699,10 +1705,10 @@ public static function _login_form_validate_2fa( $user, $nonce = '', $provider = * @since 0.9.0 */ public static function login_form_revalidate_2fa() { - $nonce = ! empty( $_REQUEST['wp-auth-nonce'] ) ? wp_unslash( $_REQUEST['wp-auth-nonce'] ) : ''; - $provider = ! empty( $_REQUEST['provider'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['provider'] ) ) : false; - $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? wp_unslash( $_REQUEST['redirect_to'] ) : admin_url(); - $is_post_request = ( 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ); + $nonce = ! empty( $_REQUEST['wp-auth-nonce'] ) ? wp_unslash( $_REQUEST['wp-auth-nonce'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified in _login_form_revalidate_2fa() for POST before processing. + $provider = ! empty( $_REQUEST['provider'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['provider'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_revalidate_2fa() for POST before processing. + $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : admin_url(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_revalidate_2fa() for POST before processing. + $is_post_request = isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- REQUEST_METHOD is not user input. self::_login_form_revalidate_2fa( $nonce, $provider, $redirect_to, $is_post_request ); exit; @@ -2553,7 +2559,7 @@ public static function get_current_user_session() { public static function rememberme() { $rememberme = false; - if ( ! empty( $_REQUEST['rememberme'] ) ) { + if ( ! empty( $_REQUEST['rememberme'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only used after 2FA login nonce verified by caller. $rememberme = true; } diff --git a/composer.json b/composer.json index c79584a5..06fb5987 100644 --- a/composer.json +++ b/composer.json @@ -25,9 +25,9 @@ }, "require-dev": { "automattic/vipwpcs": "^3.0", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.2", "phpcompatibility/phpcompatibility-wp": "3.0.0-alpha2", - "phpunit/phpunit": "^8.5|^9.6", + "phpunit/phpunit": "^8.5", "spatie/phpunit-watcher": "^1.23", "szepeviktor/phpstan-wordpress": "^1.3", "wp-coding-standards/wpcs": "^3.3", diff --git a/composer.lock b/composer.lock index 68c0cf02..9bb9e131 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c3df3fd602fb474fac8ef78f583ae835", + "content-hash": "a5647e2cb6b783e645fade339412fe6b", "packages": [], "packages-dev": [ { @@ -1188,11 +1188,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.32", + "version": "1.12.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", - "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1", + "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1", "shasum": "" }, "require": { @@ -1237,7 +1237,7 @@ "type": "github" } ], - "time": "2025-09-30T10:16:31+00:00" + "time": "2026-02-28T20:30:03+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4165,5 +4165,5 @@ "platform-overrides": { "php": "7.2.24" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/providers/class-two-factor-provider.php b/providers/class-two-factor-provider.php index 275cbae7..19e2c2b6 100644 --- a/providers/class-two-factor-provider.php +++ b/providers/class-two-factor-provider.php @@ -173,11 +173,11 @@ public static function get_code( $length = 8, $chars = '1234567890' ) { * @return false|string Auth code on success, false if the field is not set or not expected length. */ public static function sanitize_code_from_request( $field, $length = 0 ) { - if ( empty( $_REQUEST[ $field ] ) ) { + if ( empty( $_REQUEST[ $field ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Caller (core) verifies nonce before provider processing. return false; } - $code = wp_unslash( $_REQUEST[ $field ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, handled by the core method already. + $code = wp_unslash( $_REQUEST[ $field ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Caller (core) verifies nonce; value sanitized below. $code = preg_replace( '/\s+/', '', $code ); // Maybe validate the length.