| @@ -21,47 +21,9 @@ | ||
| 21 | 21 | private $step; |
| 22 | 22 | private $file; |
| 23 | 23 | private $filename; |
| 24 | 24 | |
| 25 | - /** | |
| 26 | - * Columns that are NEVER writable via CSV import. | |
| 27 | - * Denylist wins over allowlist — a column here can never be accidentally | |
| 28 | - * re-enabled by adding it to $import_meta_allowlist. | |
| 29 | - * | |
| 30 | - * @var array | |
| 31 | - */ | |
| 32 | - private static $import_meta_denylist = array( | |
| 33 | - 'user_id', // Primary key — never importable. | |
| 34 | - 'old_password', // Credential-adjacent — must not be set via import. | |
| 35 | - ); | |
| 36 | 25 | |
| 37 | - /** | |
| 38 | - * Columns that ARE permitted in a CSV import (positive / allowlist). | |
| 39 | - * Everything not listed here is silently skipped — default-deny. | |
| 40 | - * Add new safe meta keys here deliberately; never use a wildcard. | |
| 41 | - * | |
| 42 | - * @var array | |
| 43 | - */ | |
| 44 | - private static $import_meta_allowlist = array( | |
| 45 | - // Core WP user fields | |
| 46 | - 'username', | |
| 47 | - 'email', | |
| 48 | - 'display_name', | |
| 49 | - 'first_name', | |
| 50 | - 'last_name', | |
| 51 | - 'description', | |
| 52 | - 'user_url', | |
| 53 | - 'user_registered', | |
| 54 | - 'role', | |
| 55 | - // uwp_usermeta safe fields | |
| 56 | - 'bio', | |
| 57 | - 'phone', | |
| 58 | - 'user_privacy', | |
| 59 | - 'avatar_thumb', | |
| 60 | - 'banner_thumb', | |
| 61 | - ); | |
| 62 | - | |
| 63 | - | |
| 64 | 26 | public function __construct() { |
| 65 | 27 | global $wp_filesystem; |
| 66 | 28 | |
| 67 | 29 | if ( empty( $wp_filesystem ) ) { |
| @@ -81,8 +43,9 @@ | ||
| 81 | 43 | add_action( 'admin_init', array($this, 'process_settings_import') ); |
| 82 | 44 | add_action( 'wp_ajax_uwp_ajax_export_users', array( $this, 'process_users_export' ) ); |
| 83 | 45 | add_action( 'wp_ajax_uwp_ajax_import_users', array( $this, 'process_users_import' ) ); |
| 84 | 46 | add_action( 'wp_ajax_uwp_ie_upload_file', array( $this, 'ie_upload_file' ) ); |
| 47 | + add_action( 'wp_ajax_nopriv_uwp_ie_upload_file', array( $this, 'ie_upload_file' ) ); | |
| 85 | 48 | add_action( 'admin_notices', array($this, 'ie_admin_notice') ); |
| 86 | 49 | add_filter( 'uwp_get_export_users_status', array( $this, 'get_export_users_status' ) ); |
| 87 | 50 | add_filter( 'uwp_get_import_users_status', array( $this, 'get_import_users_status' ) ); |
| 88 | 51 | } |
| @@ -494,14 +457,12 @@ | ||
| 494 | 457 | * |
| 495 | 458 | */ |
| 496 | 459 | public function ie_upload_file(){ |
| 497 | 460 | |
| 498 | - if ( ! current_user_can( 'manage_options' ) ) { | |
| 499 | - wp_send_json_error( array( 'message' => __( 'Permission denied.', 'userswp' ) ), 403 ); | |
| 461 | + if ( !(!empty($_REQUEST['nonce']) && wp_verify_nonce( $_REQUEST['nonce'], 'uwp-ie-file-upload-nonce' )) ) { | |
| 462 | + echo 'error';return; | |
| 500 | 463 | } |
| 501 | 464 | |
| 502 | - check_ajax_referer( 'uwp-ie-file-upload-nonce', 'nonce' ); | |
| 503 | - | |
| 504 | 465 | $upload_data = array( |
| 505 | 466 | 'name' => $_FILES['import_file']['name'], |
| 506 | 467 | 'type' => $_FILES['import_file']['type'], |
| 507 | 468 | 'tmp_name' => $_FILES['import_file']['tmp_name'], |
| @@ -630,14 +591,14 @@ | ||
| 630 | 591 | $return['msg'] = sprintf(__('Row - %s Error: '. 'Skipped due to invalid/no data.','userswp'), $this->imp_step); |
| 631 | 592 | continue; |
| 632 | 593 | } |
| 633 | 594 | |
| 634 | - $username = isset($row['username']) ? sanitize_user($row['username']) : ''; | |
| 635 | - $email = isset($row['email']) ? sanitize_email($row['email']) : ''; | |
| 636 | - $first_name = isset($row['first_name']) ? sanitize_text_field($row['first_name']) : ''; | |
| 637 | - $last_name = isset($row['last_name']) ? sanitize_text_field($row['last_name']) : ''; | |
| 638 | - $bio = isset($row['bio']) ? sanitize_textarea_field($row['bio']) : ''; | |
| 639 | - $display_name = isset($row['display_name']) ? sanitize_text_field($row['display_name']) : ''; | |
| 595 | + $username = isset($row['username']) ? $row['username'] : ''; | |
| 596 | + $email = isset($row['email']) ? $row['email'] : ''; | |
| 597 | + $first_name = isset($row['first_name']) ? $row['first_name'] : ''; | |
| 598 | + $last_name = isset($row['last_name']) ? $row['last_name'] : ''; | |
| 599 | + $bio = isset($row['bio']) ? $row['bio'] : ''; | |
| 600 | + $display_name = isset($row['display_name']) ? $row['display_name'] : ''; | |
| 640 | 601 | $password = wp_generate_password(); |
| 641 | 602 | $exclude = array('user_id'); |
| 642 | 603 | $exclude = apply_filters('uwp_import_exclude_columns', $exclude, $row); |
| 643 | 604 | |
| @@ -643,8 +604,9 @@ | ||
| 643 | 604 | |
| 644 | 605 | if(isset($row['username']) && username_exists($row['username'])){ |
| 645 | 606 | $user = get_user_by('login', $row['username']); |
| 646 | 607 | $user_id = $user->ID; |
| 608 | + $email = $row['email']; | |
| 647 | 609 | if( !empty( $email ) && $update_existing = apply_filters('uwp_import_update_users', false, $row, $user_id) ) { |
| 648 | 610 | $args = array( |
| 649 | 611 | 'ID' => $user_id, |
| 650 | 612 | 'user_email' => $email, |
| @@ -661,9 +623,9 @@ | ||
| 661 | 623 | } elseif((int)$row['user_id'] > 0){ |
| 662 | 624 | $user = get_user_by('ID', $row['user_id']); |
| 663 | 625 | if(false === $user){ |
| 664 | 626 | $userdata = array( |
| 665 | - 'user_login' => $username, | |
| 627 | + 'user_login' => $row['username'], | |
| 666 | 628 | 'user_email' => $email, |
| 667 | 629 | 'user_pass' => $password, |
| 668 | 630 | 'first_name' => $first_name, |
| 669 | 631 | 'last_name' => $last_name, |
| @@ -707,15 +669,12 @@ | ||
| 707 | 669 | } |
| 708 | 670 | |
| 709 | 671 | if( !is_wp_error( $user_id ) ){ |
| 710 | 672 | foreach ($row as $key => $value){ |
| 711 | - //Only write columns on the allowlist; denylist always wins. | |
| 712 | - if ( ! $this->is_importable_column( $key ) ) { | |
| 713 | - continue; | |
| 673 | + if(!in_array($key, $exclude)){ | |
| 674 | + $value = maybe_unserialize($value); | |
| 675 | + uwp_update_usermeta($user_id, $key, $value); | |
| 714 | 676 | } |
| 715 | - //Never deserialize CSV input. Cast to safe scalar string. | |
| 716 | - $value = $this->sanitize_import_value( $key, $value ); | |
| 717 | - uwp_update_usermeta($user_id, $key, $value); | |
| 718 | 677 | } |
| 719 | 678 | } else { |
| 720 | 679 | $return['msg'] = sprintf(__('Row - %s Error: %s','userswp'), $this->imp_step, $user_id->get_error_message()); |
| 721 | 680 | continue; |
| @@ -840,101 +799,8 @@ | ||
| 840 | 799 | |
| 841 | 800 | public function allowed_upload_mimes($mimes = array()) { |
| 842 | 801 | $mimes['csv'] = "text/csv"; |
| 843 | 802 | return $mimes; |
| 844 | - } | |
| 845 | - | |
| 846 | - /** | |
| 847 | - * Sanitize a single CSV import value. | |
| 848 | - * | |
| 849 | - * CSV data is always a plain string. There is no legitimate reason for it | |
| 850 | - * to contain serialized PHP. We detect the serialization type-prefix | |
| 851 | - * signatures, reject them with a log entry, and return an empty string. | |
| 852 | - * All other values are cast to string and sanitized with sanitize_text_field(). | |
| 853 | - * | |
| 854 | - * @param string $key The CSV column / meta key name. | |
| 855 | - * @param mixed $value The raw value from the CSV row. | |
| 856 | - * @return string A safe scalar string ready for DB insertion. | |
| 857 | - */ | |
| 858 | - private function sanitize_import_value( $key, $value ) { | |
| 859 | - // Reject serialized payloads | |
| 860 | - if ( is_string( $value ) && preg_match( '/^[aAbBdDiIoOsScCnN][:;]/', ltrim( $value ) ) ) { | |
| 861 | - if ( function_exists( 'uwp_log' ) ) { | |
| 862 | - uwp_log( sprintf( 'Import security: serialized payload in column "%s" — discarded.', esc_attr( $key ) ) ); | |
| 863 | - } | |
| 864 | - return ''; | |
| 865 | - } | |
| 866 | - | |
| 867 | - // File path columns: validate as a URL pointing inside the uploads directory only | |
| 868 | - if ( in_array( $key, array( 'avatar_thumb', 'banner_thumb' ), true ) ) { | |
| 869 | - return $this->sanitize_import_thumb( $value ); | |
| 870 | - } | |
| 871 | - | |
| 872 | - return sanitize_text_field( (string) $value ); | |
| 873 | - } | |
| 874 | - | |
| 875 | - /** | |
| 876 | - * Sanitizes a thumbnail path value from CSV import. | |
| 877 | - * | |
| 878 | - * Validates that the given path is a real, existing file located within | |
| 879 | - * the WordPress uploads directory, preventing path traversal attacks and | |
| 880 | - * references to arbitrary files outside the uploads directory. | |
| 881 | - * | |
| 882 | - * @param string $value Raw thumbnail path value from the CSV row. | |
| 883 | - * @return string Resolved absolute path if valid, empty string otherwise. | |
| 884 | - */ | |
| 885 | - private function sanitize_import_thumb( $value ) { | |
| 886 | - $value = trim( (string) $value ); | |
| 887 | - | |
| 888 | - if ( empty( $value ) ) { | |
| 889 | - return ''; | |
| 890 | - } | |
| 891 | - | |
| 892 | - // Resolve any ../ traversal attempts before comparison | |
| 893 | - $real = realpath( $value ); | |
| 894 | - | |
| 895 | - if ( $real === false ) { | |
| 896 | - return ''; // Path doesn't exist on disk — reject | |
| 897 | - } | |
| 898 | - | |
| 899 | - // Must stay within the uploads directory | |
| 900 | - $uploads = wp_upload_dir(); | |
| 901 | - $base_dir = trailingslashit( realpath( $uploads['basedir'] ) ); | |
| 902 | - | |
| 903 | - if ( strpos( $real . DIRECTORY_SEPARATOR, $base_dir ) !== 0 ) { | |
| 904 | - if ( function_exists( 'uwp_log' ) ) { | |
| 905 | - uwp_log( sprintf( | |
| 906 | - 'Import security: thumb path "%s" is outside uploads directory — discarded.', | |
| 907 | - esc_attr( $value ) | |
| 908 | - ) ); | |
| 909 | - } | |
| 910 | - return ''; | |
| 911 | - } | |
| 912 | - | |
| 913 | - // Must be an allowed image extension | |
| 914 | - $ext = strtolower( pathinfo( $real, PATHINFO_EXTENSION ) ); | |
| 915 | - if ( ! in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif', 'webp' ), true ) ) { | |
| 916 | - return ''; | |
| 917 | - } | |
| 918 | - | |
| 919 | - return $real; // Return the resolved, canonical path | |
| 920 | - } | |
| 921 | - | |
| 922 | - /** | |
| 923 | - * Return true only when the given column name is permitted for CSV import. | |
| 924 | - * | |
| 925 | - * @param string $column The CSV column / meta key name. | |
| 926 | - * @return bool | |
| 927 | - */ | |
| 928 | - private function is_importable_column( $column ) { | |
| 929 | - $column = strtolower( trim( (string) $column ) ); | |
| 930 | - | |
| 931 | - // Denylist is checked first — it unconditionally blocks. | |
| 932 | - if ( in_array( $column, self::$import_meta_denylist, true ) ) { | |
| 933 | - return false; | |
| 934 | - } | |
| 935 | - | |
| 936 | - return in_array( $column, self::$import_meta_allowlist, true ); | |
| 937 | 803 | } |
| 938 | 804 | |
| 939 | 805 | /** |
| 940 | 806 | * Escape a string to be used in a CSV export. |