Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg
<input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" />
<?php } ?>
<input type="hidden" name="rememberme" id="rememberme" value="<?php echo esc_attr( $rememberme ); ?>" />
<?php self::print_custom_post_fields(); ?>

<?php $provider->authentication_page( $user ); ?>
</form>
Expand Down Expand Up @@ -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 '<input type="hidden" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '" />' . "\n";
}
}
}

67 changes: 67 additions & 0 deletions tests/class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -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( '<input type="hidden" name="custom_field_1" value="value1" />', $output );
$this->assertStringContainsString( '<input type="hidden" name="custom_array[key1]" value="val1" />', $output );
$this->assertStringContainsString( '<input type="hidden" name="custom_array[key2]" value="val2" />', $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( '<input type="hidden" name="custom_ok" value="allowed" />', $output );
$this->assertStringContainsString( '<input type="hidden" name="nested[ok]" value="nested_allowed" />', $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 );
}
}
Loading