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 );
+ }
}