| @@ -710,8 +710,17 @@ | ||
| 710 | 710 | |
| 711 | 711 | //Define $user |
| 712 | 712 | $user = $data[ $form ]; |
| 713 | 713 | |
| 714 | + // SECURITY: Strip admin-only profile fields from form input unless | |
| 715 | + // user has edit members permission. Prevents mass assignment attacks | |
| 716 | + // where users manipulate status, reputation, or email confirmation. | |
| 717 | + if( ! WPF()->usergroup->can( 'em' ) ) { | |
| 718 | + unset( $user['custom_points'] ); | |
| 719 | + unset( $user['status'] ); | |
| 720 | + unset( $user['is_email_confirmed'] ); | |
| 721 | + } | |
| 722 | + | |
| 714 | 723 | //Define $userid |
| 715 | 724 | $userid = intval( $data[ $form ]['userid'] ); |
| 716 | 725 | |
| 717 | 726 | //Check profile editor permissions |
| @@ -892,8 +901,10 @@ | ||
| 892 | 901 | $this->upload_avatar( $userid ); |
| 893 | 902 | } |
| 894 | 903 | |
| 895 | 904 | //Update Custom Fields |
| 905 | + // $custom_fields is still the raw $_POST['data']. The same values were merged into $user, where validate() has since dropped every field the current user may not edit and sanitize() has cleaned the rest, so read them back from $user: a key that is still there is one this user was allowed to submit, and its value is the sanitized one. | |
| 906 | + $custom_fields = array_intersect_key( $user, $custom_fields ); | |
| 896 | 907 | if( ! empty( $custom_fields ) && ( in_array( 'full', $type ) || in_array( 'custom_fields', $type ) ) ) { |
| 897 | 908 | $result_fields = $this->update_custom_fields( $userid, $custom_fields, false ); |
| 898 | 909 | } |
| 899 | 910 | |
| @@ -997,8 +1008,17 @@ | ||
| 997 | 1008 | $result_profile = true; |
| 998 | 1009 | |
| 999 | 1010 | if( $check_permissions ) { |
| 1000 | 1011 | WPF()->perm->can_edit_user( $userid ); |
| 1012 | + | |
| 1013 | + // SECURITY: Strip admin-only fields from user input unless user has | |
| 1014 | + // edit members permission. Prevents mass assignment attacks where | |
| 1015 | + // users manipulate their own status, reputation, or email confirmation. | |
| 1016 | + if( ! WPF()->usergroup->can( 'em' ) ) { | |
| 1017 | + unset( $data['custom_points'] ); | |
| 1018 | + unset( $data['status'] ); | |
| 1019 | + unset( $data['is_email_confirmed'] ); | |
| 1020 | + } | |
| 1001 | 1021 | } |
| 1002 | 1022 | |
| 1003 | 1023 | $member = $this->encode( $data ); |
| 1004 | 1024 | |
| @@ -2290,15 +2310,43 @@ | ||
| 2290 | 2310 | } |
| 2291 | 2311 | |
| 2292 | 2312 | public function blog_comments( $userid, $user_email ) { |
| 2293 | 2313 | global $wpdb; |
| 2294 | - if( ! $userid || ! $user_email ) return 0; | |
| 2295 | 2314 | |
| 2296 | - return (int) $wpdb->get_var( | |
| 2297 | - "SELECT COUNT(*) FROM " . $wpdb->comments . " WHERE `user_id` = " . intval( | |
| 2298 | - $userid | |
| 2299 | - ) . " OR `comment_author_email` = '" . esc_sql( $user_email ) . "'" | |
| 2300 | - ); | |
| 2315 | + $userid = intval( $userid ); | |
| 2316 | + | |
| 2317 | + // Match the author by WP user id and/or by email (for guest/imported comments). | |
| 2318 | + // A comment made while logged in has both columns set, but COUNT(*) still counts | |
| 2319 | + // that single row once, so the OR never double-counts. | |
| 2320 | + $author_conditions = []; | |
| 2321 | + $params = []; | |
| 2322 | + if( $userid ) { | |
| 2323 | + $author_conditions[] = 'c.user_id = %d'; | |
| 2324 | + $params[] = $userid; | |
| 2325 | + } | |
| 2326 | + if( $user_email && is_email( $user_email ) ) { | |
| 2327 | + $author_conditions[] = 'c.comment_author_email = %s'; | |
| 2328 | + $params[] = $user_email; | |
| 2329 | + } | |
| 2330 | + if( empty( $author_conditions ) ) return 0; | |
| 2331 | + | |
| 2332 | + // WordPress stores every kind of comment in the same table (pingbacks, trackbacks, | |
| 2333 | + // reviews, comments on pages/CPTs, etc.). Restrict to real, approved, public blog | |
| 2334 | + // post comments only: | |
| 2335 | + // - comment_approved = '1' -> approved only (not spam/trash/pending) | |
| 2336 | + // - comment_type IN ('','comment') -> regular comments (not pingback/trackback/review) | |
| 2337 | + // - p.post_type = 'post' -> blog posts, not pages/products/other CPTs | |
| 2338 | + // - p.post_status = 'publish' -> public posts only | |
| 2339 | + $sql = "SELECT COUNT(*) | |
| 2340 | + FROM {$wpdb->comments} AS c | |
| 2341 | + INNER JOIN {$wpdb->posts} AS p ON p.ID = c.comment_post_ID | |
| 2342 | + WHERE ( " . implode( ' OR ', $author_conditions ) . " ) | |
| 2343 | + AND c.comment_approved = '1' | |
| 2344 | + AND c.comment_type IN ( '', 'comment' ) | |
| 2345 | + AND p.post_type = 'post' | |
| 2346 | + AND p.post_status = 'publish'"; | |
| 2347 | + | |
| 2348 | + return (int) $wpdb->get_var( $wpdb->prepare( $sql, $params ) ); | |
| 2301 | 2349 | } |
| 2302 | 2350 | |
| 2303 | 2351 | public function show_delete_form( $current_user, $userids ) { |
| 2304 | 2352 | if( empty( $current_user ) || empty( $userids ) ) return; |