From 7171654dc5cef129dc0b5075d2bab81209c11b87 Mon Sep 17 00:00:00 2001 From: Aslam Doctor Date: Thu, 26 Feb 2026 12:47:40 +0530 Subject: [PATCH 1/7] Fix PHPCS and PHPStan issues across multiple files - Add PHPStan bootstrap file for runtime constants (TWO_FACTOR_DIR, TWO_FACTOR_VERSION) - Add missing properties ($new, $last_used) to Registration class - Fix PHPDoc types for show_two_factor_login, process_provider, authentication_page, rename_link, delete_link, and pack64 - Fix undefined variable bug in wp_ajax_inline_save - Add input validation, sanitization, and wp_unslash for $_POST/$_REQUEST usage - Remove redundant isset($user->ID) checks and always-true conditions - Cast base_convert() result to int for array offset usage --- class-two-factor-core.php | 10 +++--- includes/Yubico/U2F.php | 6 ++++ phpstan-bootstrap.php | 10 ++++++ phpstan.dist.neon | 2 ++ providers/class-two-factor-email.php | 6 ++-- providers/class-two-factor-fido-u2f-admin.php | 34 +++++++++++++------ providers/class-two-factor-totp.php | 10 ++---- 7 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 phpstan-bootstrap.php diff --git a/class-two-factor-core.php b/class-two-factor-core.php index 7c4b4c8a..b6381d0c 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -907,7 +907,7 @@ public static function is_api_request() { * * @since 0.2.0 * - * @param WP_User $user WP_User object of the logged-in user. + * @param WP_User|false $user WP_User object of the logged-in user. */ public static function show_two_factor_login( $user ) { if ( ! $user ) { @@ -1750,9 +1750,9 @@ public static function _login_form_revalidate_2fa( $nonce = '', $provider = '', * * @since 0.9.0 * - * @param object $provider The Two Factor Provider. - * @param WP_User $user The user being authenticated. - * @param bool $is_post_request Whether the request is a POST request. + * @param object|null $provider The Two Factor Provider. + * @param WP_User $user The user being authenticated. + * @param bool $is_post_request Whether the request is a POST request. * @return false|WP_Error|true WP_Error when an error occurs, true when the user is authenticated, false if no action occurred. */ public static function process_provider( $provider, $user, $is_post_request ) { @@ -2059,7 +2059,7 @@ public static function user_two_factor_options( $user ) {

$notice ) : ?> -
+

diff --git a/includes/Yubico/U2F.php b/includes/Yubico/U2F.php index bbb6e9a0..a1f8b382 100644 --- a/includes/Yubico/U2F.php +++ b/includes/Yubico/U2F.php @@ -486,6 +486,12 @@ class Registration /** The counter associated with this registration */ public $counter = -1; + + /** Whether this is a new registration */ + public $new; + + /** Timestamp when this registration was last used */ + public $last_used; } /** diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php new file mode 100644 index 00000000..23cdddac --- /dev/null +++ b/phpstan-bootstrap.php @@ -0,0 +1,10 @@ +ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- non-distructive option that relies on user state. + if ( isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- non-distructive option that relies on user state. $this->generate_and_email_token( $user ); return true; } @@ -413,7 +413,7 @@ public function pre_process_authentication( $user ) { */ public function validate_authentication( $user ) { $code = $this->sanitize_code_from_request( 'two-factor-email-code' ); - if ( ! isset( $user->ID ) || ! $code ) { + if ( ! $code ) { return false; } diff --git a/providers/class-two-factor-fido-u2f-admin.php b/providers/class-two-factor-fido-u2f-admin.php index 8bc9af83..9e398a46 100644 --- a/providers/class-two-factor-fido-u2f-admin.php +++ b/providers/class-two-factor-fido-u2f-admin.php @@ -237,11 +237,15 @@ public static function show_user_profile( $user ) { * @return void|never */ public static function catch_submission( $user_id ) { - if ( ! empty( $_REQUEST['do_new_security_key'] ) ) { + if ( ! empty( $_REQUEST['do_new_security_key'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is verified immediately below. check_admin_referer( "user_security_keys-{$user_id}", '_nonce_user_security_keys' ); + if ( ! isset( $_POST['u2f_response'] ) ) { + return; + } + try { - $response = json_decode( stripslashes( $_POST['u2f_response'] ) ); + $response = json_decode( wp_unslash( $_POST['u2f_response'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data decoded immediately. $reg = Two_Factor_FIDO_U2F::$u2f->doRegister( get_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, true ), $response ); $reg->new = true; @@ -277,8 +281,8 @@ public static function catch_submission( $user_id ) { public static function catch_delete_security_key() { $user_id = Two_Factor_Core::current_user_being_edited(); - if ( ! empty( $user_id ) && ! empty( $_REQUEST['delete_security_key'] ) ) { - $slug = $_REQUEST['delete_security_key']; + if ( ! empty( $user_id ) && ! empty( $_REQUEST['delete_security_key'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce requires the slug value, verified immediately below. + $slug = sanitize_text_field( wp_unslash( $_REQUEST['delete_security_key'] ) ); check_admin_referer( "delete_security_key-{$slug}", '_nonce_delete_security_key' ); @@ -297,10 +301,10 @@ public static function catch_delete_security_key() { * @access public * @static * - * @param array $item The current item. + * @param object $item The current item. * @return string */ - public static function rename_link( $item ) { + public static function rename_link( $item ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Required by WP_List_Table column callback interface. return sprintf( '%s', esc_html__( 'Rename', 'two-factor' ) ); } @@ -312,7 +316,7 @@ public static function rename_link( $item ) { * @access public * @static * - * @param array $item The current item. + * @param object $item The current item. * @return string */ public static function delete_link( $item ) { @@ -345,13 +349,23 @@ public static function wp_ajax_inline_save() { wp_die(); } - foreach ( $security_keys as &$key ) { - if ( $key->keyHandle === $_POST['keyHandle'] ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $key = null; + foreach ( $security_keys as $security_key ) { + if ( $security_key->keyHandle === $_POST['keyHandle'] ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $key = $security_key; break; } } - $key->name = $_POST['name']; + if ( ! $key ) { + wp_die(); + } + + if ( ! isset( $_POST['name'] ) ) { + wp_die(); + } + + $key->name = sanitize_text_field( wp_unslash( $_POST['name'] ) ); $updated = Two_Factor_FIDO_U2F::update_security_key( $user_id, $key ); if ( ! $updated ) { diff --git a/providers/class-two-factor-totp.php b/providers/class-two-factor-totp.php index 701687f3..47ef72e0 100644 --- a/providers/class-two-factor-totp.php +++ b/providers/class-two-factor-totp.php @@ -326,10 +326,6 @@ public static function generate_qr_code_url( $user, $secret_key ) { * @codeCoverageIgnore */ public function user_two_factor_options( $user ) { - if ( ! isset( $user->ID ) ) { - return; - } - $key = $this->get_user_totp_key( $user->ID ); wp_enqueue_script( 'two-factor-qr-code-generator' ); @@ -720,11 +716,11 @@ public static function pack64( int $value ): string { if ( 8 === PHP_INT_SIZE ) { return pack( 'J', $value ); } - + // 32-bit PHP fallback $higher = ( $value >> 32 ) & 0xFFFFFFFF; $lower = $value & 0xFFFFFFFF; - + return pack( 'NN', $higher, $lower ); } @@ -890,7 +886,7 @@ public static function base32_encode( $string ) { $base32_string = ''; foreach ( $five_bit_sections as $five_bit_section ) { - $base32_string .= self::$base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ]; + $base32_string .= self::$base_32_chars[ (int) base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ]; } return $base32_string; From 19d592ea46df3af963d655413854c7a0198c0741 Mon Sep 17 00:00:00 2001 From: Aslam Doctor Date: Sun, 1 Mar 2026 17:41:56 +0530 Subject: [PATCH 2/7] Revert FIDO/U2F file changes per PR #818 review FIDO/U2F files will be removed entirely in PR #439, so changes to U2F.php and class-two-factor-fido-u2f-admin.php are unnecessary. --- includes/Yubico/U2F.php | 6 ---- providers/class-two-factor-fido-u2f-admin.php | 34 ++++++------------- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/includes/Yubico/U2F.php b/includes/Yubico/U2F.php index a1f8b382..bbb6e9a0 100644 --- a/includes/Yubico/U2F.php +++ b/includes/Yubico/U2F.php @@ -486,12 +486,6 @@ class Registration /** The counter associated with this registration */ public $counter = -1; - - /** Whether this is a new registration */ - public $new; - - /** Timestamp when this registration was last used */ - public $last_used; } /** diff --git a/providers/class-two-factor-fido-u2f-admin.php b/providers/class-two-factor-fido-u2f-admin.php index 9e398a46..8bc9af83 100644 --- a/providers/class-two-factor-fido-u2f-admin.php +++ b/providers/class-two-factor-fido-u2f-admin.php @@ -237,15 +237,11 @@ public static function show_user_profile( $user ) { * @return void|never */ public static function catch_submission( $user_id ) { - if ( ! empty( $_REQUEST['do_new_security_key'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is verified immediately below. + if ( ! empty( $_REQUEST['do_new_security_key'] ) ) { check_admin_referer( "user_security_keys-{$user_id}", '_nonce_user_security_keys' ); - if ( ! isset( $_POST['u2f_response'] ) ) { - return; - } - try { - $response = json_decode( wp_unslash( $_POST['u2f_response'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data decoded immediately. + $response = json_decode( stripslashes( $_POST['u2f_response'] ) ); $reg = Two_Factor_FIDO_U2F::$u2f->doRegister( get_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, true ), $response ); $reg->new = true; @@ -281,8 +277,8 @@ public static function catch_submission( $user_id ) { public static function catch_delete_security_key() { $user_id = Two_Factor_Core::current_user_being_edited(); - if ( ! empty( $user_id ) && ! empty( $_REQUEST['delete_security_key'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce requires the slug value, verified immediately below. - $slug = sanitize_text_field( wp_unslash( $_REQUEST['delete_security_key'] ) ); + if ( ! empty( $user_id ) && ! empty( $_REQUEST['delete_security_key'] ) ) { + $slug = $_REQUEST['delete_security_key']; check_admin_referer( "delete_security_key-{$slug}", '_nonce_delete_security_key' ); @@ -301,10 +297,10 @@ public static function catch_delete_security_key() { * @access public * @static * - * @param object $item The current item. + * @param array $item The current item. * @return string */ - public static function rename_link( $item ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Required by WP_List_Table column callback interface. + public static function rename_link( $item ) { return sprintf( '%s', esc_html__( 'Rename', 'two-factor' ) ); } @@ -316,7 +312,7 @@ public static function rename_link( $item ) { // phpcs:ignore Generic.CodeAnalys * @access public * @static * - * @param object $item The current item. + * @param array $item The current item. * @return string */ public static function delete_link( $item ) { @@ -349,23 +345,13 @@ public static function wp_ajax_inline_save() { wp_die(); } - $key = null; - foreach ( $security_keys as $security_key ) { - if ( $security_key->keyHandle === $_POST['keyHandle'] ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $key = $security_key; + foreach ( $security_keys as &$key ) { + if ( $key->keyHandle === $_POST['keyHandle'] ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase break; } } - if ( ! $key ) { - wp_die(); - } - - if ( ! isset( $_POST['name'] ) ) { - wp_die(); - } - - $key->name = sanitize_text_field( wp_unslash( $_POST['name'] ) ); + $key->name = $_POST['name']; $updated = Two_Factor_FIDO_U2F::update_security_key( $user_id, $key ); if ( ! $updated ) { From b1975bcb4906b748180fb791bd3fd40a84f3c535 Mon Sep 17 00:00:00 2001 From: Aslam Doctor Date: Sun, 1 Mar 2026 18:29:19 +0530 Subject: [PATCH 3/7] Add early return guards for false $user in email provider Tests pass false as $user to authentication methods. Replace the removed isset($user->ID) checks with explicit early return guards to safely handle this case without accessing properties on false. --- providers/class-two-factor-email.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/providers/class-two-factor-email.php b/providers/class-two-factor-email.php index 7b00f7f8..f0d6f990 100644 --- a/providers/class-two-factor-email.php +++ b/providers/class-two-factor-email.php @@ -391,10 +391,14 @@ public function authentication_page( $user ) { * * @since 0.2.0 * - * @param WP_User $user WP_User object of the logged-in user. + * @param WP_User|false $user WP_User object of the logged-in user. * @return boolean */ public function pre_process_authentication( $user ) { + if ( ! $user ) { + return false; + } + if ( isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- non-distructive option that relies on user state. $this->generate_and_email_token( $user ); return true; @@ -408,10 +412,14 @@ public function pre_process_authentication( $user ) { * * @since 0.1-dev * - * @param WP_User $user WP_User object of the logged-in user. + * @param WP_User|false $user WP_User object of the logged-in user. * @return boolean */ public function validate_authentication( $user ) { + if ( ! $user ) { + return false; + } + $code = $this->sanitize_code_from_request( 'two-factor-email-code' ); if ( ! $code ) { return false; From fb934b8154f940e2cf23edfe1c8295ae3a7ac766 Mon Sep 17 00:00:00 2001 From: Aslam Doctor Date: Tue, 3 Mar 2026 13:02:07 +0530 Subject: [PATCH 4/7] Consolidate plugin constants into a shared file Rename phpstan-bootstrap.php to constants.php and include it from two-factor.php so TWO_FACTOR_DIR and TWO_FACTOR_VERSION are defined in a single place. --- constants.php | 17 +++++++++++++++++ phpstan-bootstrap.php | 10 ---------- phpstan.dist.neon | 2 +- two-factor.php | 10 +--------- 4 files changed, 19 insertions(+), 20 deletions(-) create mode 100644 constants.php delete mode 100644 phpstan-bootstrap.php diff --git a/constants.php b/constants.php new file mode 100644 index 00000000..e43048c2 --- /dev/null +++ b/constants.php @@ -0,0 +1,17 @@ + Date: Tue, 3 Mar 2026 20:05:42 +0530 Subject: [PATCH 5/7] Move constants before ABSPATH guard and bump PHPStan to level 4 - Define TWO_FACTOR_DIR and TWO_FACTOR_VERSION in two-factor.php before the ABSPATH check so PHPStan can discover them from scanned files - Remove constants.php to keep version strings in a single file - Bump PHPStan analysis level from 0 to 4 - Exclude FIDO/U2F files from PHPStan analysis - Add phpstan-ignore for Jetpack runtime compatibility check --- class-two-factor-compat.php | 1 + constants.php | 17 ----------------- phpstan.dist.neon | 8 +++++--- two-factor.php | 10 ++++++++-- 4 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 constants.php diff --git a/class-two-factor-compat.php b/class-two-factor-compat.php index 94e47f4d..7ed74e80 100644 --- a/class-two-factor-compat.php +++ b/class-two-factor-compat.php @@ -58,6 +58,7 @@ public function jetpack_rememberme( $rememberme ) { * @return boolean */ public function jetpack_is_sso_active() { + // @phpstan-ignore function.impossibleType (Jetpack may or may not have this method depending on version) return ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'sso' ) ); } } diff --git a/constants.php b/constants.php deleted file mode 100644 index e43048c2..00000000 --- a/constants.php +++ /dev/null @@ -1,17 +0,0 @@ - Date: Tue, 3 Mar 2026 20:10:29 +0530 Subject: [PATCH 6/7] Revert PHPStan level back to 0 --- phpstan.dist.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 9b9a8605..779c63b8 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,7 +1,7 @@ includes: - vendor/szepeviktor/phpstan-wordpress/extension.neon parameters: - level: 4 + level: 0 paths: - includes - providers From ac3dd5f35c855a26025177375f6b0dab34d38667 Mon Sep 17 00:00:00 2001 From: Aslam Doctor Date: Tue, 3 Mar 2026 20:17:16 +0530 Subject: [PATCH 7/7] Remove unnecessary PHPStan ignore comment in compat class --- class-two-factor-compat.php | 1 - 1 file changed, 1 deletion(-) diff --git a/class-two-factor-compat.php b/class-two-factor-compat.php index 7ed74e80..94e47f4d 100644 --- a/class-two-factor-compat.php +++ b/class-two-factor-compat.php @@ -58,7 +58,6 @@ public function jetpack_rememberme( $rememberme ) { * @return boolean */ public function jetpack_is_sso_active() { - // @phpstan-ignore function.impossibleType (Jetpack may or may not have this method depending on version) return ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'sso' ) ); } }