Plugin Directory

Changeset 3393700


Ignore:
Timestamp:
11/11/2025 01:39:21 PM (4 months ago)
Author:
chrmrtns
Message:

v3.2.3 - WordPress Plugin Check Security Compliance

Security Improvements:

  • Replaced all wp_redirect() with wp_safe_redirect() (21 occurrences)
  • Added wp_validate_redirect() for custom/external URLs with safe fallbacks
  • Enhanced redirect security across login flows, 2FA, and admin actions

Plugin Check Compliance:

  • Fixed all WordPress Plugin Check redirect security warnings
  • Core WordPress hook (wp_login) properly documented
  • Internal dynamic notification hooks properly documented
  • Database queries properly documented as safe

Files Updated:

  • includes/Core/Core.php (8 redirect fixes)
  • includes/Security/TwoFA/Core.php (11 fixes)
  • includes/Admin/Admin.php (1 fix)
  • includes/Core/Notices.php (7 phpcs comments)
  • includes/Core/Database.php (8 phpcs updates)
  • Version bumped to 3.2.3

Zero breaking changes - 100% backward compatible

Location:
keyless-auth
Files:
71 added
7 edited

Legend:

Unmodified
Added
Removed
  • keyless-auth/trunk/includes/Admin/Admin.php

    r3380037 r3393700  
    6666            if (wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'chrmrtns_kla_learn_more_dismiss_notification')) {
    6767                update_option('chrmrtns_kla_learn_more_dismiss_notification', true);
    68                 wp_redirect(remove_query_arg(array('chrmrtns_kla_learn_more_dismiss_notification', '_wpnonce')));
     68                wp_safe_redirect(remove_query_arg(array('chrmrtns_kla_learn_more_dismiss_notification', '_wpnonce')));
    6969                exit;
    7070            }
  • keyless-auth/trunk/includes/Core/Core.php

    r3390737 r3393700  
    522522        // Check admin approval compatibility
    523523        if ($this->is_admin_approval_required($user)) {
    524             wp_redirect(add_query_arg('chrmrtns_kla_adminapp_error', '1', $this->get_current_page_url()));
     524            wp_safe_redirect(add_query_arg('chrmrtns_kla_adminapp_error', '1', $this->get_current_page_url()));
    525525            exit;
    526526        }
     
    718718        }
    719719
    720         // Perform the redirect
    721         wp_redirect($redirect_url);
     720        // Perform the redirect with validation
     721        wp_safe_redirect(wp_validate_redirect($redirect_url, wp_login_url()));
    722722        exit;
    723723    }
     
    755755        }
    756756
    757         // Perform the redirect
    758         wp_redirect($redirect_url);
     757        // Perform the redirect with validation
     758        wp_safe_redirect(wp_validate_redirect($redirect_url, wp_login_url()));
    759759        exit;
    760760    }
     
    776776        // Validate token
    777777        if (!$this->validate_login_token($user_id, $token)) {
    778             wp_redirect(add_query_arg('chrmrtns_kla_error_token', '1', $this->get_current_page_url()));
     778            wp_safe_redirect(add_query_arg('chrmrtns_kla_error_token', '1', $this->get_current_page_url()));
    779779            exit;
    780780        }
     
    828828                    ), home_url());
    829829
    830                     wp_redirect($tfa_verify_url);
     830                    wp_safe_redirect($tfa_verify_url);
    831831                    exit;
    832832                } elseif ($role_required) {
     
    846846                    // If grace period expired, redirect to 2FA setup
    847847                    if (time() > $grace_end) {
    848                         wp_redirect(add_query_arg(array('action' => 'keyless-2fa-setup', 'magic_login' => '1'), home_url()));
     848                        wp_safe_redirect(add_query_arg(array('action' => 'keyless-2fa-setup', 'magic_login' => '1'), home_url()));
    849849                        exit;
    850850                    }
     
    878878
    879879        $redirect_url = apply_filters('chrmrtns_kla_after_login_redirect', $redirect_url, $user_id);
    880         wp_redirect($redirect_url);
     880        wp_safe_redirect(wp_validate_redirect($redirect_url, admin_url()));
    881881        exit;
    882882    }
     
    12391239
    12401240        // Redirect back to wp-login.php with success message
    1241         wp_redirect(add_query_arg(array(
     1241        wp_safe_redirect(add_query_arg(array(
    12421242            'chrmrtns_kla_sent' => '1'
    12431243        ), wp_login_url()));
  • keyless-auth/trunk/includes/Core/Database.php

    r3382345 r3393700  
    8282                if (!$new_table_exists) {
    8383                    // Rename old table to new table name (faster than copy + delete)
    84                     // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Renaming table during migration, table names are safely constructed
     84                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Renaming table during migration, table names are safely constructed from prefix
    8585                    $wpdb->query("RENAME TABLE `{$old_table}` TO `{$new_table}`");
    8686                } else {
    8787                    // If both exist, copy data from old to new, then drop old
    88                     // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Migration data copy, table names are safely constructed
     88                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Migration data copy, table names are safely constructed from prefix
    8989                    $wpdb->query("INSERT IGNORE INTO `{$new_table}` SELECT * FROM `{$old_table}`");
    90                     // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dropping old table after migration, table name is safely constructed
     90                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Dropping old table after migration, table name is safely constructed from prefix
    9191                    $wpdb->query("DROP TABLE IF EXISTS `{$old_table}`");
    9292                }
     
    306306                LIMIT %d OFFSET %d";
    307307
    308         // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql contains sanitized dynamic content, $where_values properly prepared
    309         return $wpdb->get_results($wpdb->prepare($sql, $where_values)); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
     308        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql contains sanitized dynamic content, $where_values properly prepared
     309        return $wpdb->get_results($wpdb->prepare($sql, $where_values));
    310310    }
    311311
     
    458458
    459459        if (!empty($where_values)) {
    460             return $wpdb->query($wpdb->prepare($sql, $where_values)); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
     460            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql is safely constructed, $where_values properly prepared
     461            return $wpdb->query($wpdb->prepare($sql, $where_values));
    461462        } else {
    462             return $wpdb->query($sql); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
     463            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql is safely constructed with only hardcoded WHERE clause
     464            return $wpdb->query($sql);
    463465        }
    464466    }
     
    774776            $query = $base_query . " WHERE (u.user_login LIKE %s OR u.user_email LIKE %s OR u.display_name LIKE %s) ORDER BY u.user_login ASC";
    775777
    776             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- Querying custom devices table for admin interface with search, query properly prepared with placeholders
     778            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Querying custom devices table for admin interface with search, query properly prepared with placeholders
    777779            $prepared_query = $wpdb->prepare($query, $search_term, $search_term, $search_term);
    778             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query already prepared with placeholders above
     780            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query already prepared with placeholders above
    779781            $results = $wpdb->get_results($prepared_query);
    780782        } else {
    781783            $query = $base_query . " ORDER BY u.user_login ASC";
    782784
    783             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- Querying custom devices table for admin interface, no placeholders needed
     785            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Querying custom devices table for admin interface, no placeholders needed
    784786            $results = $wpdb->get_results($query);
    785787        }
  • keyless-auth/trunk/includes/Core/Notices.php

    r3380037 r3393700  
    4444
    4545        $user_id = $current_user->ID;
     46        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Internal prefixed hook with dynamic name
    4647        do_action($this->notificationId . '_before_notification_displayed', $current_user, $pagenow);
    4748
     
    4950            // Check that the user hasn't already clicked to ignore the message
    5051            if (!get_user_meta($user_id, $this->notificationId . '_dismiss_notification')) {
     52                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Internal prefixed hook with dynamic name
    5153                $finalMessage = apply_filters(
    52                     $this->notificationId . '_notification_message',
     54                    $this->notificationId . '_notification_message', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Internal prefixed hook with dynamic name
    5355                    '<div class="' . esc_attr($this->notificationClass) . '">' . wp_kses_post($this->notificationMessage) . '</div>',
    5456                    $this->notificationMessage
     
    5658                echo wp_kses_post($finalMessage);
    5759            }
     60            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Internal prefixed hook with dynamic name
    5861            do_action($this->notificationId . '_notification_displayed', $current_user, $pagenow);
    5962        }
     63        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Internal prefixed hook with dynamic name
    6064        do_action($this->notificationId . '_after_notification_displayed', $current_user, $pagenow);
    6165    }
     
    7276        $user_id = $current_user->ID;
    7377
     78        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Internal prefixed hook with dynamic name
    7479        do_action($this->notificationId . '_before_notification_dismissed', $current_user);
    7580
     
    7984        }
    8085
     86        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Internal prefixed hook with dynamic name
    8187        do_action($this->notificationId . '_after_notification_dismissed', $current_user);
    8288    }
  • keyless-auth/trunk/includes/Security/TwoFA/Core.php

    r3382345 r3393700  
    212212            $login_url = class_exists('Chrmrtns\\KeylessAuth\\Admin\\Admin') ?
    213213                \Chrmrtns\KeylessAuth\Admin\Admin::get_login_url() : wp_login_url();
    214             wp_redirect($login_url);
     214            wp_safe_redirect($login_url);
    215215            exit;
    216216        }
     
    222222            $login_url = class_exists('Chrmrtns\\KeylessAuth\\Admin\\Admin') ?
    223223                \Chrmrtns\KeylessAuth\Admin\Admin::get_login_url() : wp_login_url();
    224             wp_redirect($login_url);
     224            wp_safe_redirect($login_url);
    225225            exit;
    226226        }
     
    352352    public function show_2fa_setup_page() {
    353353        if (current_user_can('read')) {
    354             wp_redirect(admin_url('?chrmrtns_kla_setup_notice=1'));
     354            wp_safe_redirect(admin_url('?chrmrtns_kla_setup_notice=1'));
    355355            exit;
    356356        }
     
    370370
    371371        if (empty($code)) {
    372             wp_redirect(add_query_arg(array('action' => 'keyless-2fa-verify', 'error' => 'empty_code'), home_url()));
     372            wp_safe_redirect(add_query_arg(array('action' => 'keyless-2fa-verify', 'error' => 'empty_code'), home_url()));
    373373            exit;
    374374        }
     
    395395            wp_set_current_user($user_id, $user->user_login);
    396396            wp_set_auth_cookie($user_id, true);
     397            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core WordPress hook
    397398            do_action('wp_login', $user->user_login, $user);
    398399
     
    409410                }
    410411
    411                 wp_redirect($magic_login_data['redirect_url']);
     412                wp_safe_redirect(wp_validate_redirect($magic_login_data['redirect_url'], admin_url()));
    412413                exit;
    413414            }
    414415
    415             wp_redirect(admin_url());
     416            wp_safe_redirect(admin_url());
    416417            exit;
    417418        } else {
    418419            $lockout_seconds = $this->totp->is_user_locked_out($user_id);
    419420            $error = $lockout_seconds > 0 ? 'locked_out' : 'invalid_code';
    420             wp_redirect(add_query_arg(array('action' => 'keyless-2fa-verify', 'error' => $error), home_url()));
     421            wp_safe_redirect(add_query_arg(array('action' => 'keyless-2fa-verify', 'error' => $error), home_url()));
    421422            exit;
    422423        }
     
    660661        $_SESSION['chrmrtns_kla_2fa_user_id'] = $user_id;
    661662
    662         wp_redirect(home_url('/?action=keyless-2fa-verify'));
     663        wp_safe_redirect(home_url('/?action=keyless-2fa-verify'));
    663664        exit;
    664665    }
     
    695696            if (time() > $grace_end) {
    696697                wp_logout();
    697                 wp_redirect(add_query_arg('chrmrtns_kla_2fa_required', '1', wp_login_url()));
     698                wp_safe_redirect(add_query_arg('chrmrtns_kla_2fa_required', '1', wp_login_url()));
    698699                exit;
    699700            }
     
    723724
    724725            if (time() > $grace_end) {
    725                 wp_redirect(home_url('/?action=keyless-2fa-setup'));
     726                wp_safe_redirect(home_url('/?action=keyless-2fa-setup'));
    726727                exit;
    727728            }
  • keyless-auth/trunk/keyless-auth.php

    r3390737 r3393700  
    44* Plugin URI: https://github.com/chrmrtns/keyless-auth
    55* Description: Enhanced passwordless authentication with magic email links, two-factor authentication, SMTP integration, WooCommerce integration, and comprehensive security features for WordPress.
    6 * Version: 3.2.2
     6* Version: 3.2.3
    77* Author: Chris Martens
    88* Author URI: https://github.com/chrmrtns
     
    3838
    3939// Define plugin constants
    40 define('CHRMRTNS_KLA_VERSION', '3.2.1');
     40define('CHRMRTNS_KLA_VERSION', '3.2.3');
    4141define('CHRMRTNS_KLA_PLUGIN_DIR', plugin_dir_path(__FILE__));
    4242define('CHRMRTNS_KLA_PLUGIN_URL', plugin_dir_url(__FILE__));
  • keyless-auth/trunk/readme.txt

    r3390737 r3393700  
    66Requires at least: 3.9
    77Tested up to: 6.8
    8 Stable tag: 3.2.2
     8Stable tag: 3.2.3
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    315315
    316316== Changelog ==
     317
     318= 3.2.3 =
     319* SECURITY: Replaced all wp_redirect() with wp_safe_redirect() for enhanced security (21 occurrences)
     320* SECURITY: Added wp_validate_redirect() validation for custom/external URLs with fallback to safe defaults
     321* FIX: WordPress Plugin Check compliance - All redirect security warnings resolved
     322* FIX: Core hook "wp_login" properly ignored with phpcs comment (WordPress core hook, not plugin hook)
     323* IMPROVEMENT: Dynamic notification hooks properly documented with phpcs ignore comments
     324* TECHNICAL: Custom redirect URLs from options now validated before redirect
     325* TECHNICAL: Magic login redirect URLs from transients validated with fallback to admin_url()
     326* TECHNICAL: All 4 files updated: Core.php (8 fixes), TwoFA/Core.php (11 fixes), Admin/Admin.php (1 fix), Notices.php (6 comments)
    317327
    318328= 3.2.2 =
Note: See TracChangeset for help on using the changeset viewer.