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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
run: npm install

- name: Lint PHP
run: npm run lint:php || true # Ignore for now.
run: npm run lint:php

- name: Lint PHP Compatibility
run: composer lint-compat
Expand Down
92 changes: 66 additions & 26 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,8 @@ public static function uninstall() {
}

// Delete options first since that is faster.
if ( ! empty( $option_keys ) ) {
foreach ( $option_keys as $option_key ) {
delete_option( $option_key );
}
foreach ( $option_keys as $option_key ) {
delete_option( $option_key );
}

foreach ( $user_meta_keys as $meta_key ) {
Expand Down Expand Up @@ -588,7 +586,7 @@ public static function current_user_being_edited() {
*/
public static function trigger_user_settings_action() {
$action_raw = 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 -- Value sanitized below; nonce verified in is_valid_user_action() before do_action.
$action = ( is_scalar( $action_raw ) && (string) $action_raw !== '' ) ? sanitize_key( (string) $action_raw ) : '';
$action = ( is_scalar( $action_raw ) && '' !== (string) $action_raw ) ? sanitize_key( (string) $action_raw ) : '';
$user_id = self::current_user_being_edited();

if ( self::is_valid_user_action( $user_id, $action ) ) {
Expand Down Expand Up @@ -818,7 +816,7 @@ public static function get_primary_provider_for_user( $user = null ) {
return null;
} elseif ( is_wp_error( $available_providers ) ) {
// If it returned an error, the configured methods don't exist, and it couldn't swap in a replacement.
wp_die( $available_providers );
wp_die( esc_html( $available_providers->get_error_message() ) );
} elseif ( 1 === count( $available_providers ) ) {
$provider = key( $available_providers );
} else {
Expand Down Expand Up @@ -1129,7 +1127,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg

if ( is_wp_error( $available_providers ) ) {
// If it returned an error, the configured methods don't exist, and it couldn't swap in a replacement.
wp_die( $available_providers );
wp_die( esc_html( $available_providers->get_error_message() ) );
}

if ( ! function_exists( 'login_header' ) ) {
Expand Down Expand Up @@ -1186,7 +1184,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg

foreach ( $backup_providers as $backup_provider_key => $backup_provider ) {
$backup_link_args['provider'] = $backup_provider_key;
$links[] = array(
$links[] = array(
'url' => self::login_url( $backup_link_args ),
'label' => $backup_provider->get_alternative_provider_label(),
);
Expand Down Expand Up @@ -1582,18 +1580,18 @@ 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; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in _login_form_validate_2fa() via verify_login_nonce() before any use.
$nonce = ( isset( $_REQUEST['wp-auth-nonce'] ) && is_scalar( $_REQUEST['wp-auth-nonce'] ) ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['wp-auth-nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- 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.
$wp_auth_id = ! empty( $_REQUEST['wp-auth-id'] ) ? absint( $_REQUEST['wp-auth-id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in validate_login_form_2fa() before any use.
$nonce = ( isset( $_REQUEST['wp-auth-nonce'] ) && is_scalar( $_REQUEST['wp-auth-nonce'] ) ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['wp-auth-nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in validate_login_form_2fa() before any use.
$provider = ! empty( $_REQUEST['provider'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['provider'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in validate_login_form_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 validate_login_form_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 ) {
return;
}

self::_login_form_validate_2fa( $user, $nonce, $provider, $redirect_to, $is_post_request );
self::validate_login_form_2fa( $user, $nonce, $provider, $redirect_to, $is_post_request );
exit;
}

Expand All @@ -1604,6 +1602,7 @@ public static function login_form_validate_2fa() {
* This function expects the caller exiting after calling.
*
* @since 0.9.0
* @since 0.17.0 Renamed from `_login_form_validate_2fa()`.
*
* @param WP_User $user The WP_User instance.
* @param string $nonce The nonce provided.
Expand All @@ -1612,7 +1611,7 @@ public static function login_form_validate_2fa() {
* @param bool $is_post_request Whether the incoming request was a POST request or not.
* @return void
*/
public static function _login_form_validate_2fa( $user, $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) {
public static function validate_login_form_2fa( $user, $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) {
// Validate the request.
if ( true !== self::verify_login_nonce( $user->ID, $nonce ) ) {
wp_safe_redirect( home_url() );
Expand Down Expand Up @@ -1716,19 +1715,39 @@ public static function _login_form_validate_2fa( $user, $nonce = '', $provider =
exit;
}

/**
* Backward-compatible wrapper for the old login form validation method name.
*
* This method is kept for third-party code that may still call the previous
* public static method directly.
*
* @since 0.9.0
* @deprecated 0.17.0 Use validate_login_form_2fa() instead.
*
* @param WP_User $user The WP_User instance.
* @param string $nonce The nonce provided.
* @param string $provider The provider to use, if known.
* @param string $redirect_to The redirection location.
* @param bool $is_post_request Whether the incoming request was a POST request or not.
* @return void
*/
public static function _login_form_validate_2fa( $user, $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Backward-compatible wrapper for the legacy public method name.
self::validate_login_form_2fa( $user, $nonce, $provider, $redirect_to, $is_post_request );
}


/**
* Display the "Revalidate Two Factor" page.
*
* @since 0.9.0
*/
public static function login_form_revalidate_2fa() {
$nonce = ( isset( $_REQUEST['wp-auth-nonce'] ) && is_scalar( $_REQUEST['wp-auth-nonce'] ) ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['wp-auth-nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- 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.
$nonce = ( isset( $_REQUEST['wp-auth-nonce'] ) && is_scalar( $_REQUEST['wp-auth-nonce'] ) ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['wp-auth-nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in revalidate_login_form_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 revalidate_login_form_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 revalidate_login_form_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 );
self::revalidate_login_form_2fa( $nonce, $provider, $redirect_to, $is_post_request );
exit;
}

Expand All @@ -1739,14 +1758,15 @@ public static function login_form_revalidate_2fa() {
* This function expects the caller exiting after calling.
*
* @since 0.9.0
* @since 0.17.0 Renamed from `_login_form_revalidate_2fa()`.
*
* @param string $nonce The nonce passed with the request.
* @param string $provider The provider to use, if known.
* @param string $redirect_to The redirection location.
* @param bool $is_post_request Whether the incoming request was a POST request or not.
* @return void
*/
public static function _login_form_revalidate_2fa( $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) {
public static function revalidate_login_form_2fa( $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) {
if ( ! is_user_logged_in() ) {
wp_safe_redirect( home_url() );
exit;
Expand Down Expand Up @@ -1823,6 +1843,25 @@ public static function _login_form_revalidate_2fa( $nonce = '', $provider = '',
exit;
}

/**
* Backward-compatible wrapper for the old revalidation method name.
*
* This method is kept for third-party code that may still call the previous
* public static method directly.
*
* @since 0.9.0
* @deprecated 0.17.0 Use revalidate_login_form_2fa() instead.
*
* @param string $nonce The nonce passed with the request.
* @param string $provider The provider to use, if known.
* @param string $redirect_to The redirection location.
* @param bool $is_post_request Whether the incoming request was a POST request or not.
* @return void
*/
public static function _login_form_revalidate_2fa( $nonce = '', $provider = '', $redirect_to = '', $is_post_request = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Backward-compatible wrapper for the legacy public method name.
self::revalidate_login_form_2fa( $nonce, $provider, $redirect_to, $is_post_request );
}

/**
* Process the 2FA provider authentication.
*
Expand Down Expand Up @@ -1998,7 +2037,7 @@ public static function notify_user_password_reset( $user ) {
);
$user_message = str_replace( "\t", '', $user_message );

return wp_mail( $user->user_email, __( 'Your password was compromised and has been reset', 'two-factor' ), $user_message );
return wp_mail( $user->user_email, __( 'Your password was compromised and has been reset', 'two-factor' ), $user_message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail -- Plugin sends a single transactional security email to the affected user.
}

/**
Expand Down Expand Up @@ -2034,7 +2073,7 @@ public static function notify_admin_user_password_reset( $user ) {
);
$message = str_replace( "\t", '', $message );

return wp_mail( $admin_email, $subject, $message );
return wp_mail( $admin_email, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail -- Plugin sends a single transactional security email to the site admin.
}

/**
Expand Down Expand Up @@ -2446,10 +2485,11 @@ public static function user_two_factor_options_update( $user_id ) {
return;
}

$user = self::fetch_user( $user_id );
$providers = self::get_supported_providers_for_user( $user_id );
$enabled_providers = $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ];
$existing_providers = self::get_enabled_providers_for_user( $user_id );
$user = self::fetch_user( $user_id );
$providers = self::get_supported_providers_for_user( $user_id );
$enabled_providers_input = wp_unslash( $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Array values are sanitized below.
$enabled_providers = array_map( 'sanitize_text_field', $enabled_providers_input );
$existing_providers = self::get_enabled_providers_for_user( $user_id );

// Enable only the available providers.
$enabled_providers = array_intersect_key( $providers, array_flip( $enabled_providers ) );
Expand Down Expand Up @@ -2478,7 +2518,7 @@ public static function user_two_factor_options_update( $user_id ) {
update_user_meta( $user_id, self::ENABLED_PROVIDERS_USER_META_KEY, array_keys( $enabled_providers ) );

// Primary provider must be enabled.
$new_provider = isset( $_POST[ self::PROVIDER_USER_META_KEY ] ) ? $_POST[ self::PROVIDER_USER_META_KEY ] : '';
$new_provider = isset( $_POST[ self::PROVIDER_USER_META_KEY ] ) ? sanitize_text_field( wp_unslash( $_POST[ self::PROVIDER_USER_META_KEY ] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Value sanitized inline.
if ( ! empty( $new_provider ) && isset( $enabled_providers[ $new_provider ] ) ) {
update_user_meta( $user_id, self::PROVIDER_USER_META_KEY, $new_provider );
} else {
Expand Down
3 changes: 2 additions & 1 deletion phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
includes:
- vendor/szepeviktor/phpstan-wordpress/extension.neon
parameters:
level: 0
level: 2
paths:
- includes
- providers
- settings
- class-two-factor-compat.php
- class-two-factor-core.php
- two-factor.php
2 changes: 1 addition & 1 deletion providers/class-two-factor-provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ abstract public function authentication_page( $user );
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function pre_process_authentication( $user ) {
public function pre_process_authentication( $user ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Base implementation keeps the provider interface signature but does not use the user.
return false;
}

Expand Down
4 changes: 2 additions & 2 deletions providers/class-two-factor-totp.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,8 @@ public static function generate_qr_code_url( $user, $secret_key ) {
*/
public function user_two_factor_options( $user ) {
if ( ! ( $user instanceof WP_User ) ) {
return;
}
return;
}

$key = $this->get_user_totp_key( $user->ID );

Expand Down
2 changes: 1 addition & 1 deletion settings/class-two-factor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static function render_settings_page() {
if ( isset( $_POST['two_factor_settings_submit'] ) ) {
check_admin_referer( 'two_factor_save_settings', 'two_factor_settings_nonce' );

$posted = isset( $_POST['two_factor_enabled_providers'] ) && is_array( $_POST['two_factor_enabled_providers'] ) ? wp_unslash( $_POST['two_factor_enabled_providers'] ) : array();
$posted = isset( $_POST['two_factor_enabled_providers'] ) && is_array( $_POST['two_factor_enabled_providers'] ) ? wp_unslash( $_POST['two_factor_enabled_providers'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified above; array values sanitized immediately below.

// Sanitize posted values immediately.
$posted = array_map( 'sanitize_text_field', (array) $posted );
Expand Down
1 change: 1 addition & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
require_once $_tests_dir . '/includes/functions.php';
require_once dirname( __DIR__ ) . '/includes/function.login-header.php';
require_once dirname( __DIR__ ) . '/includes/function.login-footer.php';
require_once __DIR__ . '/class-two-factor-redirect-exception.php';

// Activate the plugin.
tests_add_filter(
Expand Down
Loading
Loading