Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/user-login-history/inc/admin/class-admin-login-list-table.php
+++ b/user-login-history/inc/admin/class-admin-login-list-table.php
@@ -11,14 +11,14 @@
namespace User_Login_HistoryIncAdmin;
-use User_Login_History as NS;
use User_Login_HistoryIncCommonHelpersDb as Db_Helper;
-use User_Login_HistoryIncCommonHelpersDate_Time as Date_Time_Helper;
-use User_Login_HistoryIncAdminUser_Profile;
-use User_Login_HistoryIncCommonAbstractsList_Table as List_Table_Abstract;
use User_Login_HistoryIncCommonInterfacesAdmin_Csv as Admin_Csv_Interface;
use User_Login_HistoryIncCommonInterfacesAdmin_List_Table as Admin_List_Table_Interface;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
/**
* Render the login listing page.
*/
@@ -41,15 +41,19 @@
. ' FROM ' . $table . ' AS FaUserLogin'
. ' WHERE 1 ';
- $where_query = $this->prepare_where_query();
+ $where = $this->prepare_where_query();
+ $where_query = $where['where_query'] ?? '';
+ $where_query_values = $where['where_query_values'] ?? array();
if ( $where_query ) {
$sql .= $where_query;
}
- if ( ! empty( $_REQUEST['orderby'] ) ) {
- $direction = ! empty( $_REQUEST['order'] ) ? $_REQUEST['order'] : ' ASC';
- $sanitize_sql_orderby = sanitize_sql_orderby( $_REQUEST['orderby'] . ' ' . $direction );
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required here to fetch records.
+ $the_get = $_REQUEST;
+ if ( ! empty( $the_get['orderby'] ) ) {
+ $direction = ! empty( $the_get['order'] ) ? $the_get['order'] : ' ASC';
+ $sanitize_sql_orderby = sanitize_sql_orderby( $the_get['orderby'] . ' ' . $direction );
if ( $sanitize_sql_orderby ) {
$sql .= ' ORDER BY ' . $sanitize_sql_orderby;
}
@@ -62,7 +66,8 @@
$sql .= ' OFFSET ' . ( $page_number - 1 ) * $per_page;
}
- return Db_Helper::get_results( $sql );
+ // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $sql is built internally with placeholders, not from user input.
+ return $wpdb->get_results( $wpdb->prepare( $sql, $where_query_values ), ARRAY_A );
}
/**
@@ -74,17 +79,22 @@
public function record_count() {
global $wpdb;
$table = $wpdb->prefix . $this->table;
- $sql = ' SELECT'
+
+ $sql = ' SELECT'
. ' COUNT(FaUserLogin.id) AS total'
. ' FROM ' . $table . ' AS FaUserLogin'
. ' WHERE 1 ';
- $where_query = $this->prepare_where_query();
+
+ $where = $this->prepare_where_query();
+ $where_query = $where['where_query'] ?? '';
+ $where_query_values = $where['where_query_values'] ?? array();
if ( $where_query ) {
$sql .= $where_query;
}
- return Db_Helper::get_var( $sql );
+ // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- already scaped.
+ return $wpdb->get_var( $wpdb->prepare( $sql, $where_query_values ) );
}
/**
@@ -106,7 +116,15 @@
$delete_nonce = wp_create_nonce( $this->delete_action_nonce );
$actions = array(
- 'delete' => sprintf( '<a href="?page=%s&action=%s&record_id=%s&_wpnonce=%s">%s</a>', esc_attr( $_REQUEST['page'] ), $this->delete_action, absint( $item['id'] ), $delete_nonce, esc_html__( 'Delete', 'faulh' ) ),
+ 'delete' => sprintf(
+ '<a href="?page=%s&action=%s&record_id=%s&_wpnonce=%s">%s</a>',
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required here to fetch records.
+ esc_attr( sanitize_text_field( isset( $_REQUEST['page'] ) ? wp_unslash( $_REQUEST['page'] ) : '' ) ),
+ $this->delete_action,
+ absint( $item['id'] ),
+ $delete_nonce,
+ esc_html__( 'Delete', 'user-login-history' )
+ ),
);
return $title . $this->row_actions( $actions );
}
@@ -129,35 +147,45 @@
public function process_bulk_action() {
$nonce = '_wpnonce';
- if ( ! isset( $_POST[ $this->get_bulk_action_form() ] ) || empty( $_POST[ $nonce ] ) || ! wp_verify_nonce( $_POST[ $nonce ], $this->get_bulk_action_nonce() ) || ! current_user_can( 'administrator' ) ) {
+ if (
+ ! isset( $_POST[ $this->get_bulk_action_form() ] )
+ || empty( $_POST[ $nonce ] )
+ || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ $nonce ] ) ), $this->get_bulk_action_nonce() )
+ || ! current_user_can( 'edit_users' )
+ ) {
return;
}
- $message = esc_html__( 'Please try again.', 'faulh' );
+ $message = esc_html__( 'Please try again.', 'user-login-history' );
$status = false;
switch ( $this->current_action() ) {
case 'bulk-delete':
if ( ! empty( $_POST['bulk-action-ids'] ) ) {
- $status = Db_Helper::delete_rows_by_table_and_ids( $this->table, $_POST['bulk-action-ids'] );
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized with absint.
+ $ids = (array) wp_unslash( $_POST['bulk-action-ids'] );
+ $ids = array_filter( array_map( 'absint', $ids ) );
+ $status = Db_Helper::delete_rows_by_table_and_ids( $this->table, $ids );
if ( $status ) {
- $message = esc_html__( 'Selected record(s) deleted.', 'faulh' );
+ $message = esc_html__( 'Selected record(s) deleted.', 'user-login-history' );
}
}
break;
case 'bulk-delete-all-admin':
- $status = Db_Helper::truncate_table( $this->table );
+ global $wpdb;
+ $status = $wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %i', $wpdb->prefix . $this->table ) );
if ( $status ) {
- $message = esc_html__( 'All record(s) deleted.', 'faulh' );
+ $message = esc_html__( 'All record(s) deleted.', 'user-login-history' );
}
break;
}
$this->admin_notice->add_notice( $message, $status ? 'success' : 'error' );
- wp_safe_redirect( esc_url( 'admin.php?page=' . $_GET['page'] ) );
+ $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
+ wp_safe_redirect( esc_url( 'admin.php?page=' . $page ) );
exit;
}
@@ -167,24 +195,33 @@
public function process_single_action() {
$nonce = '_wpnonce';
- if ( empty( $_GET['record_id'] ) || empty( $_GET[ $nonce ] ) || ! wp_verify_nonce( $_GET[ $nonce ], $this->get_delete_action_nonce() ) || ! current_user_can( 'administrator' ) ) {
+ if (
+ empty( $_GET['record_id'] )
+ || empty( $_GET[ $nonce ] )
+ || ! wp_verify_nonce(
+ sanitize_text_field( wp_unslash( $_GET[ $nonce ] ) ),
+ $this->get_delete_action_nonce()
+ )
+ || ! current_user_can( 'edit_users' )
+ ) {
return;
}
$id = absint( $_GET['record_id'] );
$status = false;
- $message = esc_html__( 'Please try again.', 'faulh' );
+ $message = esc_html__( 'Please try again.', 'user-login-history' );
switch ( $this->current_action() ) {
case $this->delete_action:
$status = Db_Helper::delete_rows_by_table_and_ids( $this->table, array( $id ) );
if ( $status ) {
- $message = esc_html__( 'Record deleted.', 'faulh' );
+ $message = esc_html__( 'Record deleted.', 'user-login-history' );
}
break;
}
$this->admin_notice->add_notice( $message, $status ? 'success' : 'error' );
- wp_safe_redirect( esc_url( 'admin.php?page=' . $_GET['page'] ) );
+ $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
+ wp_safe_redirect( esc_url( 'admin.php?page=' . $page ) );
exit;
}
@@ -194,7 +231,7 @@
* @overridden
*/
public function get_columns() {
- return apply_filters( $this->plugin_name . '_admin_login_list_get_columns', parent::get_columns() );
+ return apply_filters( 'faulh_admin_login_list_get_columns', parent::get_columns() );
}
/**
@@ -203,7 +240,6 @@
* @overridden
*/
public function get_sortable_columns() {
- return apply_filters( $this->plugin_name . '_admin_login_list_get_sortable_columns', parent::get_sortable_columns() );
+ return apply_filters( 'faulh_admin_login_list_get_sortable_columns', parent::get_sortable_columns() );
}
-
}
--- a/user-login-history/inc/admin/class-admin-notice.php
+++ b/user-login-history/inc/admin/class-admin-notice.php
@@ -11,6 +11,10 @@
namespace User_Login_HistoryIncAdmin;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
/**
* Handle admin notice functionality.
*
@@ -76,11 +80,10 @@
if ( false !== $notices ) {
foreach ( $notices as $notice ) {
if ( ! empty( $notice[1] && ! empty( $notice[0] ) ) ) {
- echo '<div class="notice notice-' . esc_attr($notice[1]) . ' is-dismissible"><p>' . esc_html($notice[0]) . '</p></div>';
+ echo '<div class="notice notice-' . esc_attr( $notice[1] ) . ' is-dismissible"><p>' . esc_html( $notice[0] ) . '</p></div>';
}
}
delete_transient( $this->transient_name );
}
}
-
}
--- a/user-login-history/inc/admin/class-admin.php
+++ b/user-login-history/inc/admin/class-admin.php
@@ -11,17 +11,16 @@
namespace User_Login_HistoryIncAdmin;
-use User_Login_History as NS;
use User_Login_HistoryIncCoreActivator;
-use User_Login_HistoryIncCommonHelpersDb as Db_Helper;
use User_Login_HistoryIncAdminListing_Table_Csv;
use User_Login_HistoryIncAdminAdmin_Login_List_Table;
use User_Login_HistoryIncAdminNetwork_Admin_Login_List_Table;
use User_Login_HistoryIncAdminUser_Profile;
-use User_Login_HistoryIncCommonInterfacesAdmin_Csv;
-use User_Login_HistoryIncCommonLogin_Tracker;
use User_Login_HistoryIncAdminSettings as Admin_Settings;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
/**
* Backend Functionality.
*/
@@ -83,7 +82,12 @@
* @param User_Login_HistoryIncAdminAdmin_Notice $admin_notice The notice object.
*/
public function __construct(
- $plugin_name, $version, User_Profile $user_profile, Listing_Table_Csv $listing_table_csv, Admin_Settings $admin_settings, Admin_Notice $admin_notice
+ $plugin_name,
+ $version,
+ User_Profile $user_profile,
+ Listing_Table_Csv $listing_table_csv,
+ Admin_Settings $admin_settings,
+ Admin_Notice $admin_notice
) {
$this->plugin_name = $plugin_name;
@@ -143,18 +147,17 @@
*/
private function enqueue_scripts_for_plugin_login_list_page() {
if ( $this->is_plugin_login_list_page() ) {
- wp_enqueue_script( $this->plugin_name . '-admin-jquery-ui.min', plugin_dir_url( __FILE__ ) . 'js/jquery-ui.min.js', array(), $this->version, 'all' );
- wp_enqueue_script( $this->plugin_name . '-admin', plugin_dir_url( __FILE__ ) . 'js/admin.js', array(), $this->version, 'all' );
+ wp_enqueue_script( $this->plugin_name . '-admin', plugin_dir_url( __FILE__ ) . 'js/admin.js', array( 'jquery', 'jquery-ui-datepicker' ), $this->version, 'all' );
wp_localize_script(
$this->plugin_name . '-admin',
'admin_custom_object',
array(
- 'delete_confirm_message' => esc_html__( 'Are your sure?', 'faulh' ),
- 'invalid_date_range_message' => esc_html__( 'Please provide a valid date range.', 'faulh' ),
+ 'delete_confirm_message' => esc_html__( 'Are your sure?', 'user-login-history' ),
+ 'invalid_date_range_message' => esc_html__( 'Please provide a valid date range.', 'user-login-history' ),
'admin_url' => admin_url(),
'plugin_name' => $this->plugin_name,
- 'show_advanced_filters' => esc_html__( 'Show Advanced Filters', 'faulh' ),
- 'hide_advanced_filters' => esc_html__( 'Hide Advanced Filters', 'faulh' ),
+ 'show_advanced_filters' => esc_html__( 'Show Advanced Filters', 'user-login-history' ),
+ 'hide_advanced_filters' => esc_html__( 'Hide Advanced Filters', 'user-login-history' ),
)
);
}
@@ -179,9 +182,8 @@
global $pagenow, $plugin_page;
if ( 'admin.php' == $pagenow && $this->plugin_name . '-pro' == $plugin_page ) {
- wp_enqueue_style( $this->plugin_name . '-admin-bt', '//maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css', array(), $this->version, 'all' );
- wp_enqueue_style( $this->plugin_name . '-admin-fa', '//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css', array(), $this->version, 'all' );
- wp_enqueue_style( $this->plugin_name . '-admin-gf', '//fonts.googleapis.com/css?family=Poppins&display=swap', array(), $this->version, 'all' );
+ wp_enqueue_style( $this->plugin_name . '-admin-bt', plugin_dir_url( __FILE__ ) . '/css/bootstrap.min.css', array(), $this->version, 'all' );
+ wp_enqueue_style( $this->plugin_name . '-admin-fa', plugin_dir_url( __FILE__ ) . '/css/font-awesome.min.css', array(), $this->version, 'all' );
wp_enqueue_style( $this->plugin_name . '-admin-gp', plugin_dir_url( __FILE__ ) . 'css/go-pro.css', array(), $this->version, 'all' );
}
@@ -213,17 +215,17 @@
$menu_slug = $this->get_plugin_login_list_page_slug();
$hook = add_menu_page(
- esc_html__( 'Login List', 'faulh' ),
- NSPLUGIN_NAME,
+ esc_html__( 'Login List', 'user-login-history' ),
+ FAULH_PLUGIN_NAME,
'administrator',
$menu_slug,
array( $this, 'render_login_list' ),
plugin_dir_url( __FILE__ ) . 'images/icon.png',
30
);
- add_submenu_page( $menu_slug, esc_html__( 'Login List', 'faulh' ), esc_html__( 'Login List', 'faulh' ), 'administrator', $menu_slug, array( $this, 'render_login_list' ) );
- add_submenu_page( $menu_slug, esc_html__( 'Pro Features', 'faulh' ), esc_html__( 'Pro Features', 'faulh' ), 'administrator', $this->plugin_name . '-pro', array( $this, 'render_pro' ) );
- add_submenu_page( $menu_slug, esc_html__( 'More Plugins', 'faulh' ), esc_html__( 'More Plugins', 'faulh' ), 'administrator', $this->plugin_name . '-more-plugins', array( $this, 'render_more_plugins' ) );
+ add_submenu_page( $menu_slug, esc_html__( 'Login List', 'user-login-history' ), esc_html__( 'Login List', 'user-login-history' ), 'administrator', $menu_slug, array( $this, 'render_login_list' ) );
+ add_submenu_page( $menu_slug, esc_html__( 'Pro Features', 'user-login-history' ), esc_html__( 'Pro Features', 'user-login-history' ), 'administrator', $this->plugin_name . '-pro', array( $this, 'render_pro' ) );
+ add_submenu_page( $menu_slug, esc_html__( 'More Plugins', 'user-login-history' ), esc_html__( 'More Plugins', 'user-login-history' ), 'administrator', $this->plugin_name . '-more-plugins', array( $this, 'render_more_plugins' ) );
add_action( "load-$hook", array( $this, 'screen_option' ) );
}
@@ -232,22 +234,21 @@
* Render the login listing page
*/
public function render_login_list() {
- require plugin_dir_path( dirname( __FILE__ ) ) . 'admin/views/login-list-table.php';
+ require plugin_dir_path( __DIR__ ) . 'admin/views/login-list-table.php';
}
/**
* Render the login listing page
*/
public function render_pro() {
- require plugin_dir_path( dirname( __FILE__ ) ) . 'admin/views/pro.php';
+ require plugin_dir_path( __DIR__ ) . 'admin/views/pro.php';
}
/**
* Render the more plugins page.
*/
- public function render_more_plugins()
- {
- require plugin_dir_path(dirname(__FILE__)) . 'admin/views/more_plugins.php';
+ public function render_more_plugins() {
+ require plugin_dir_path( __DIR__ ) . 'admin/views/more_plugins.php';
}
/**
@@ -268,7 +269,7 @@
public function screen_option() {
$option = 'per_page';
$args = array(
- 'label' => __( 'Show Records Per Page', 'faulh' ),
+ 'label' => __( 'Show Records Per Page', 'user-login-history' ),
'default' => 20,
'option' => $this->plugin_name . '_rows_per_page',
);
@@ -303,7 +304,7 @@
return;
}
// Current version.
- $current_version = get_option( NSPLUGIN_OPTION_NAME_VERSION );
+ $current_version = get_option( FAULH_PLUGIN_OPTION_NAME_VERSION );
// If the version is older.
if ( $current_version && version_compare( $current_version, $this->version, '<' ) ) {
@@ -311,8 +312,8 @@
require_once ABSPATH . '/wp-admin/includes/plugin.php';
}
- if ( is_plugin_active_for_network( NSPLUGIN_BOOTSTRAP_FILE_PATH_FROM_PLUGIN_FOLDER ) ) {
- $blog_ids = Db_Helper::get_blog_ids_by_site_id();
+ if ( is_plugin_active_for_network( FAULH_PLUGIN_BASENAME ) ) {
+ $blog_ids = get_sites( array( 'fields' => 'ids' ) );
foreach ( $blog_ids as $blog_id ) {
switch_to_blog( $blog_id );
Activator::create_table();
@@ -325,12 +326,11 @@
}
}
}
-
- public function add_action_links($actions) {
- $links = array(
- sprintf('<a target="_blank" href="%s">%s</a>', esc_url(NSPLUGIN_GO_PRO_LINK), esc_html__('Buy Pro', 'faulh')),
- );
- return array_merge($actions, $links);
- }
+ public function add_action_links( $actions ) {
+ $links = array(
+ sprintf( '<a target="_blank" href="%s">%s</a>', esc_url( FAULH_PLUGIN_GO_PRO_LINK ), esc_html__( 'Buy Pro', 'user-login-history' ) ),
+ );
+ return array_merge( $actions, $links );
+ }
}
--- a/user-login-history/inc/admin/class-listing-table-csv.php
+++ b/user-login-history/inc/admin/class-listing-table-csv.php
@@ -11,9 +11,12 @@
namespace User_Login_HistoryIncAdmin;
-use User_Login_HistoryIncCommonHelpersDate_Time as Date_Time_Helper;
use User_Login_HistoryIncCommonInterfacesAdmin_Csv as Admin_Csv_Interface;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
/**
* CSV Export Functionality
*
@@ -35,7 +38,7 @@
*
* @var string
*/
- private $unknown_symbol = '---';
+ private $unknown_symbol = '';
/**
* Set the listing table.
@@ -50,8 +53,9 @@
* Set content type in header.
*/
private function set_headers() {
- header( 'Content-Type: text/csv' );
- header( 'Content-Disposition: attachment;filename=' . $this->get_suffix() . '.csv' );
+ $filename = sanitize_file_name( $this->get_suffix() . '.csv' );
+ header( 'Content-Type: text/csv; charset=UTF-8' );
+ header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
}
/**
@@ -60,7 +64,7 @@
* @return string
*/
private function get_suffix() {
- return 'login_list_' . date( 'n-j-y_H-i' );
+ return 'login_list_' . wp_date( 'n-j-y_H-i' );
}
/**
@@ -85,40 +89,113 @@
* Exports CSV.
*/
private function export() {
- $data = $this->listing_table->get_all_rows();
-
- if ( ! $data ) {
- $this->listing_table->no_items();
- exit;
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- WP_Filesystem is actually not ideal for CSV export.
+ $handle = fopen( 'php://output', 'w' );
+ if ( false === $handle ) {
+ return;
}
-
- $csv = LeagueCsvWriter::createFromString();
$this->listing_table->set_unknown_symbol( $this->unknown_symbol );
$columns = $this->listing_table->get_columns();
$i = 0;
$record = array();
- foreach ( $data as $row ) {
+ $page = 1;
+ $limit = 100;
- foreach ( $columns as $field_name => $field_label ) {
- if ( ! key_exists( $field_name, $row ) ) {
- continue;
- }
+ while ( true ) {
+ $batch = $this->listing_table->get_rows( $limit, $page );
- $record[ $field_name ] = $this->listing_table->column_default( $row, $field_name );
+ if ( empty( $batch ) ) {
+ if ( 0 === $i ) {
+ fputcsv( $handle, array( __( 'No records found', 'user-login-history' ) ), ',', '"', '\' );
+ exit;
+ }
+ break;
}
- if ( 0 == $i ) {
- $csv->insertOne(array_keys( $record ) );
+ foreach ( $batch as $row ) {
+ $record = array();
+ foreach ( $columns as $field_name => $field_label ) {
+ if ( ! key_exists( $field_name, $row ) ) {
+ continue;
+ }
- }
+ $record[ $field_name ] = $this->listing_table->column_default( $row, $field_name );
+ }
+
+ if ( 0 == $i ) {
+ fputcsv( $handle, $this->escape_csv_record( array_keys( $record ) ), ',', '"', '\' );
+ }
+
+ fputcsv( $handle, $this->escape_csv_record( $record ), ',', '"', '\' );
- $csv->insertOne( $record );
+ ++$i;
+ }
- $i++;
+ ++$page;
+ wp_cache_flush_runtime();
}
- $csv->output();
+ wp_cache_flush_runtime();
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- WP_Filesystem is actually not ideal for CSV export.
+ fclose( $handle );
die();
}
+ /**
+ * Escape CSV formula injection characters.
+ *
+ * @param array $record CSV record.
+ * @return array
+ */
+ private function escape_csv_record( array $record ) {
+ foreach ( $record as $key => $value ) {
+ $record[ $key ] = $this->escape_csv_field( $value );
+ }
+
+ return $record;
+ }
+
+ /**
+ * Escape a single CSV field if it looks like a formula.
+ *
+ * @param mixed $value CSV field value.
+ * @return mixed
+ */
+ private function escape_csv_field( $value ) {
+ if ( is_string( $value ) ) {
+ $str = $value;
+ } elseif ( is_object( $value ) && method_exists( $value, '__toString' ) ) {
+ $str = (string) $value;
+ } else {
+ return $value;
+ }
+
+ // Decode HTML entities from DB storage
+ $str = html_entity_decode( $str, ENT_QUOTES, 'UTF-8' );
+
+ // Strip HTML tags
+ $str = wp_strip_all_tags( $str );
+
+ // Strip NULL bytes
+ $str = str_replace( "