diff --git a/class-two-factor-core.php b/class-two-factor-core.php index 1f039866..6ac59ec3 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -1161,6 +1161,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg + authentication_page( $user ); ?> @@ -2624,5 +2625,69 @@ public static function filter_session_information( $session, $user_id ) { return $session; } + + /** + * Output custom $_POST fields as hidden inputs. + * + * Iterates over $_POST and outputs hidden inputs for fields added by third-party plugins, + * ensuring that standard WordPress and Two-Factor fields (especially sensitive ones like `pwd`) + * are explicitly ignored. + * + * @since 0.17.0 + */ + private static function print_custom_post_fields() { + $blocklist = array( + // Standard WP Login fields. + 'log', + 'pwd', + 'wp-submit', + 'redirect_to', + 'rememberme', + 'interim-login', + 'testcookie', + '_wpnonce', + '_wp_http_referer', + // Additional common login fields (e.g., WooCommerce, custom forms). + 'password', + 'user_pass', + 'username', + 'user_login', + // Two Factor specific fields. + 'provider', + 'wp-auth-id', + 'wp-auth-nonce', + 'action', + ); + + // phpcs:ignore WordPress.Security.NonceVerification.Missing + foreach ( $_POST as $key => $value ) { + if ( in_array( $key, $blocklist, true ) ) { + continue; + } + self::print_hidden_inputs( $key, wp_unslash( $value ), $blocklist ); + } + } + + /** + * Recursively output hidden inputs for a given key/value. + * + * @since 0.17.0 + * + * @param string $name Input name. + * @param string|array $value Input value. + * @param array $blocklist Array of keys to skip. + */ + private static function print_hidden_inputs( $name, $value, $blocklist = array() ) { + if ( is_array( $value ) ) { + foreach ( $value as $k => $v ) { + if ( in_array( $k, $blocklist, true ) ) { + continue; + } + self::print_hidden_inputs( $name . '[' . $k . ']', $v, $blocklist ); + } + } else { + echo '' . "\n"; + } + } } diff --git a/tests/class-two-factor-core.php b/tests/class-two-factor-core.php index 38052106..bba5d421 100644 --- a/tests/class-two-factor-core.php +++ b/tests/class-two-factor-core.php @@ -2707,4 +2707,71 @@ public function test_add_settings_action_link() { $this->assertStringContainsString( 'Settings', $first ); $this->assertStringContainsString( 'options-general.php', $first ); } + + /** + * @covers Two_Factor_Core::print_custom_post_fields + */ + public function test_print_custom_post_fields_includes_custom_fields() { + $original_post = $_POST; + $_POST = array( + 'custom_field_1' => 'value1', + 'custom_array' => array( + 'key1' => 'val1', + 'key2' => 'val2', + ), + ); + + $method = new ReflectionMethod( 'Two_Factor_Core', 'print_custom_post_fields' ); + $method->setAccessible( true ); + + ob_start(); + $method->invoke( null ); + $output = ob_get_clean(); + + $_POST = $original_post; + + $this->assertStringContainsString( '', $output ); + $this->assertStringContainsString( '', $output ); + $this->assertStringContainsString( '', $output ); + } + + /** + * @covers Two_Factor_Core::print_custom_post_fields + */ + public function test_print_custom_post_fields_excludes_blocked_fields() { + $original_post = $_POST; + $_POST = array( + 'pwd' => 'my_secret_password', + 'password' => 'my_other_password', + 'log' => 'admin', + 'user_pass' => 'secret', + 'rememberme' => '1', + 'custom_ok' => 'allowed', + 'nested' => array( + 'pwd' => 'nested_secret_password', + 'ok' => 'nested_allowed', + ), + ); + + $method = new ReflectionMethod( 'Two_Factor_Core', 'print_custom_post_fields' ); + $method->setAccessible( true ); + + ob_start(); + $method->invoke( null ); + $output = ob_get_clean(); + + $_POST = $original_post; + + $this->assertStringContainsString( '', $output ); + $this->assertStringContainsString( '', $output ); + $this->assertStringNotContainsString( 'my_secret_password', $output ); + $this->assertStringNotContainsString( 'my_other_password', $output ); + $this->assertStringNotContainsString( 'nested_secret_password', $output ); + $this->assertStringNotContainsString( 'secret', $output ); + $this->assertStringNotContainsString( 'pwd', $output ); + $this->assertStringNotContainsString( 'password', $output ); + $this->assertStringNotContainsString( 'log', $output ); + $this->assertStringNotContainsString( 'user_pass', $output ); + $this->assertStringNotContainsString( 'rememberme', $output ); + } }