| @@ -640,8 +640,26 @@ | ||
| 640 | 640 | */ |
| 641 | 641 | public function update( $data, $type = 'full', $check_permissions = true ) { |
| 642 | 642 | $type = (array) $type; |
| 643 | 643 | |
| 644 | + // SECURITY: when permission checks are skipped, strip reserved wp_users | |
| 645 | + // column names from custom-field input to block mass assignment. | |
| 646 | + if( ! $check_permissions ) { | |
| 647 | + if( isset( $data['data'] ) && is_array( $data['data'] ) ) { | |
| 648 | + foreach( [ | |
| 649 | + 'user_email', | |
| 650 | + 'user_login', | |
| 651 | + 'user_pass', | |
| 652 | + 'user_pass1', | |
| 653 | + 'user_pass2', | |
| 654 | + 'userid', | |
| 655 | + 'ID', | |
| 656 | + ] as $reserved ) { | |
| 657 | + unset( $data['data'][ $reserved ] ); | |
| 658 | + } | |
| 659 | + } | |
| 660 | + } | |
| 661 | + | |
| 644 | 662 | switch( WPF()->current_object['template'] ) { |
| 645 | 663 | case 'register': |
| 646 | 664 | $form = 'wpfreg'; |
| 647 | 665 | $form_fields = $this->get_register_fields(); |
| @@ -674,8 +692,17 @@ | ||
| 674 | 692 | ) ) { |
| 675 | 693 | $data[ $form ]['userid'] = $data['userid']; |
| 676 | 694 | } |
| 677 | 695 | |
| 696 | + // SECURITY: when permission checks are skipped, force the form userid | |
| 697 | + // to the trusted caller-supplied $data['userid'] (overrides user input). | |
| 698 | + if( ! $check_permissions && wpfval( $data, 'userid' ) ) { | |
| 699 | + if( ! isset( $data[ $form ] ) || ! is_array( $data[ $form ] ) ) { | |
| 700 | + $data[ $form ] = []; | |
| 701 | + } | |
| 702 | + $data[ $form ]['userid'] = (int) $data['userid']; | |
| 703 | + } | |
| 704 | + | |
| 678 | 705 | if( wpfval( $data, $form, 'userid' ) ) { |
| 679 | 706 | $result_user = true; |
| 680 | 707 | $result_fields = true; |
| 681 | 708 | $result_profile = true; |
| @@ -683,8 +710,17 @@ | ||
| 683 | 710 | |
| 684 | 711 | //Define $user |
| 685 | 712 | $user = $data[ $form ]; |
| 686 | 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 | + | |
| 687 | 723 | //Define $userid |
| 688 | 724 | $userid = intval( $data[ $form ]['userid'] ); |
| 689 | 725 | |
| 690 | 726 | //Check profile editor permissions |
| @@ -865,8 +901,10 @@ | ||
| 865 | 901 | $this->upload_avatar( $userid ); |
| 866 | 902 | } |
| 867 | 903 | |
| 868 | 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 ); | |
| 869 | 907 | if( ! empty( $custom_fields ) && ( in_array( 'full', $type ) || in_array( 'custom_fields', $type ) ) ) { |
| 870 | 908 | $result_fields = $this->update_custom_fields( $userid, $custom_fields, false ); |
| 871 | 909 | } |
| 872 | 910 | |
| @@ -970,8 +1008,17 @@ | ||
| 970 | 1008 | $result_profile = true; |
| 971 | 1009 | |
| 972 | 1010 | if( $check_permissions ) { |
| 973 | 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 | + } | |
| 974 | 1021 | } |
| 975 | 1022 | |
| 976 | 1023 | $member = $this->encode( $data ); |
| 977 | 1024 | |
| @@ -1621,16 +1668,17 @@ | ||
| 1621 | 1668 | } else { |
| 1622 | 1669 | $wheres[] = "`" . esc_sql( $field ) . "` LIKE '%" . esc_sql( $needle ) . "%'"; |
| 1623 | 1670 | } |
| 1624 | 1671 | } else { |
| 1625 | - $needle = preg_quote( preg_quote( $needle ) ); | |
| 1672 | + // Local var: $needle must stay raw for the remaining fields of this loop | |
| 1673 | + $n = wpforo_json_regexp_needle( $needle ); | |
| 1626 | 1674 | if( in_array( $f['type'], [ 'text', 'search', 'textarea' ], true ) ) { |
| 1627 | 1675 | $wheres[] = "`fields` REGEXP '[{,]\"" . $field . "\":(\\\[[^\\\[]*)?\"[^\"]*" . esc_sql( |
| 1628 | - $needle | |
| 1676 | + $n | |
| 1629 | 1677 | ) . "[^\"]*\"'"; |
| 1630 | 1678 | } else { |
| 1631 | 1679 | $wheres[] = "`fields` REGEXP '[{,]\"" . $field . "\":(\\\[[^\\\[]*)?\"" . esc_sql( |
| 1632 | - $needle | |
| 1680 | + $n | |
| 1633 | 1681 | ) . "\"'"; |
| 1634 | 1682 | } |
| 1635 | 1683 | } |
| 1636 | 1684 | } |
| @@ -1673,31 +1721,27 @@ | ||
| 1673 | 1721 | } |
| 1674 | 1722 | } else { |
| 1675 | 1723 | if( in_array( $f['type'], [ 'text', 'search', 'textarea' ], true ) ) { |
| 1676 | 1724 | if( is_scalar( $needle ) ) { |
| 1677 | - $needle = preg_quote( preg_quote( wpforo_encode( $needle ) ) ); | |
| 1678 | 1725 | $wheres[] = "`fields` REGEXP '[{,]\"" . $field . "\":(\\\[[^\\\[]*)?\"[^\"]*" . esc_sql( |
| 1679 | - $needle | |
| 1726 | + wpforo_json_regexp_needle( wpforo_encode( $needle ) ) | |
| 1680 | 1727 | ) . "[^\"]*\"'"; |
| 1681 | 1728 | } elseif( is_array( $needle ) ) { |
| 1682 | 1729 | foreach( $needle as $n ) { |
| 1683 | - $n = preg_quote( preg_quote( wpforo_encode( $n ) ) ); | |
| 1684 | 1730 | $wheres[] = "`fields` REGEXP '[{,]\"" . $field . "\":(\\\[[^\\\[]*)?\"[^\"]*" . esc_sql( |
| 1685 | - $n | |
| 1731 | + wpforo_json_regexp_needle( wpforo_encode( $n ) ) | |
| 1686 | 1732 | ) . "[^\"]*\"'"; |
| 1687 | 1733 | } |
| 1688 | 1734 | } |
| 1689 | 1735 | } else { |
| 1690 | 1736 | if( is_scalar( $needle ) ) { |
| 1691 | - $needle = preg_quote( preg_quote( wpforo_encode( $needle ) ) ); | |
| 1692 | 1737 | $wheres[] = "`fields` REGEXP '[{,]\"" . $field . "\":(\\\[[^\\\[]*)?\"" . esc_sql( |
| 1693 | - $needle | |
| 1738 | + wpforo_json_regexp_needle( wpforo_encode( $needle ) ) | |
| 1694 | 1739 | ) . "\"'"; |
| 1695 | 1740 | } elseif( is_array( $needle ) ) { |
| 1696 | 1741 | foreach( $needle as $n ) { |
| 1697 | - $n = preg_quote( preg_quote( wpforo_encode( $n ) ) ); | |
| 1698 | 1742 | $wheres[] = "`fields` REGEXP '[{,]\"" . $field . "\":(\\\[[^\\\[]*)?\"" . esc_sql( |
| 1699 | - $n | |
| 1743 | + wpforo_json_regexp_needle( wpforo_encode( $n ) ) | |
| 1700 | 1744 | ) . "\"'"; |
| 1701 | 1745 | } |
| 1702 | 1746 | } |
| 1703 | 1747 | } |
| @@ -2263,15 +2307,43 @@ | ||
| 2263 | 2307 | } |
| 2264 | 2308 | |
| 2265 | 2309 | public function blog_comments( $userid, $user_email ) { |
| 2266 | 2310 | global $wpdb; |
| 2267 | - if( ! $userid || ! $user_email ) return 0; | |
| 2268 | 2311 | |
| 2269 | - return (int) $wpdb->get_var( | |
| 2270 | - "SELECT COUNT(*) FROM " . $wpdb->comments . " WHERE `user_id` = " . intval( | |
| 2271 | - $userid | |
| 2272 | - ) . " OR `comment_author_email` = '" . esc_sql( $user_email ) . "'" | |
| 2273 | - ); | |
| 2312 | + $userid = intval( $userid ); | |
| 2313 | + | |
| 2314 | + // Match the author by WP user id and/or by email (for guest/imported comments). | |
| 2315 | + // A comment made while logged in has both columns set, but COUNT(*) still counts | |
| 2316 | + // that single row once, so the OR never double-counts. | |
| 2317 | + $author_conditions = []; | |
| 2318 | + $params = []; | |
| 2319 | + if( $userid ) { | |
| 2320 | + $author_conditions[] = 'c.user_id = %d'; | |
| 2321 | + $params[] = $userid; | |
| 2322 | + } | |
| 2323 | + if( $user_email && is_email( $user_email ) ) { | |
| 2324 | + $author_conditions[] = 'c.comment_author_email = %s'; | |
| 2325 | + $params[] = $user_email; | |
| 2326 | + } | |
| 2327 | + if( empty( $author_conditions ) ) return 0; | |
| 2328 | + | |
| 2329 | + // WordPress stores every kind of comment in the same table (pingbacks, trackbacks, | |
| 2330 | + // reviews, comments on pages/CPTs, etc.). Restrict to real, approved, public blog | |
| 2331 | + // post comments only: | |
| 2332 | + // - comment_approved = '1' -> approved only (not spam/trash/pending) | |
| 2333 | + // - comment_type IN ('','comment') -> regular comments (not pingback/trackback/review) | |
| 2334 | + // - p.post_type = 'post' -> blog posts, not pages/products/other CPTs | |
| 2335 | + // - p.post_status = 'publish' -> public posts only | |
| 2336 | + $sql = "SELECT COUNT(*) | |
| 2337 | + FROM {$wpdb->comments} AS c | |
| 2338 | + INNER JOIN {$wpdb->posts} AS p ON p.ID = c.comment_post_ID | |
| 2339 | + WHERE ( " . implode( ' OR ', $author_conditions ) . " ) | |
| 2340 | + AND c.comment_approved = '1' | |
| 2341 | + AND c.comment_type IN ( '', 'comment' ) | |
| 2342 | + AND p.post_type = 'post' | |
| 2343 | + AND p.post_status = 'publish'"; | |
| 2344 | + | |
| 2345 | + return (int) $wpdb->get_var( $wpdb->prepare( $sql, $params ) ); | |
| 2274 | 2346 | } |
| 2275 | 2347 | |
| 2276 | 2348 | public function show_delete_form( $current_user, $userids ) { |
| 2277 | 2349 | if( empty( $current_user ) || empty( $userids ) ) return; |
| @@ -4162,9 +4234,9 @@ | ||
| 4162 | 4234 | $sbj ); |
| 4163 | 4235 | $msg = str_replace( [ '[blogname]', '[user_login]', '[login_link]', '[login_url]' ], |
| 4164 | 4236 | [ $blogname, $user->user_login, $login_link, $login_url ], |
| 4165 | 4237 | $msg ); |
| 4166 | - wpforo_send_email( $user->user_email, $sbj, $msg ); | |
| 4238 | + wpforo_send_email( $user->user_email, $sbj, $msg, '', 'user_approved', (int) $user->ID ); | |
| 4167 | 4239 | } |
| 4168 | 4240 | } |
| 4169 | 4241 | |
| 4170 | 4242 | public function get_activity_url( $filter = '', $boardid = null, $arg = null ) { |
| @@ -4308,9 +4380,9 @@ | ||
| 4308 | 4380 | // Prefer buffered email if available |
| 4309 | 4381 | if( is_array( $this->front_delete_email_buffer ) ) { |
| 4310 | 4382 | $sbj = (string) wpfval( $this->front_delete_email_buffer, 'subject' ); |
| 4311 | 4383 | $msg = (string) wpfval( $this->front_delete_email_buffer, 'message' ); |
| 4312 | - wpforo_send_email( wpforo_setting( 'email', 'admin_emails' ), $sbj, $msg ); | |
| 4384 | + wpforo_send_email( wpforo_setting( 'email', 'admin_emails' ), $sbj, $msg, '', 'admin_notification' ); | |
| 4313 | 4385 | $this->front_delete_email_buffer = null; // clear buffer |
| 4314 | 4386 | } |
| 4315 | 4387 | } |
| 4316 | 4388 | } |