PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
← All changes | includes/forms/class-charitable-profile-form.php +71 -20 1.8.11 → 1.8.13 View file →
@@ -711,42 +711,93 @@
711 711 * @access public
712 712 * @since 1.0.0
713 713 */
714 714 public function save_avatar( $submitted, $fields, $form ) {
715 - if ( isset( $_FILES ) && isset( $_FILES['avatar'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
715 + $user_id = (int) $form->get_user()->ID;
716 + $files_avatar = isset( $_FILES['avatar'] ) ? $_FILES['avatar'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
717 + $has_upload = is_array( $files_avatar )
718 + && ! empty( $files_avatar['name'] )
719 + && UPLOAD_ERR_NO_FILE !== (int) ( $files_avatar['error'] ?? UPLOAD_ERR_NO_FILE );
716 720
721 + if ( $has_upload ) {
717 722 $attachment_id = $form->upload_post_attachment( 'avatar', 0 );
718 723
719 - if ( ! is_wp_error( $attachment_id ) ) {
724 + if ( is_wp_error( $attachment_id ) ) {
725 + return $submitted;
726 + }
720 727
721 - $submitted['avatar'] = $attachment_id;
728 + $submitted['avatar'] = (int) $attachment_id;
722 729
723 - /* Delete the previously upload avatar. */
724 - $old_avatar = get_user_meta( $form->get_user()->ID, 'avatar', true );
730 + // Delete the previously stored avatar — only if it belongs to this user.
731 + // Without this ownership check, a poisoned 'avatar' user-meta value
732 + // (planted by a no-file submission, see below) would chain into
733 + // wp_delete_attachment() on an attacker-chosen attachment ID.
734 + $old_avatar = get_user_meta( $user_id, 'avatar', true );
735 + if ( ! empty( $old_avatar )
736 + && $this->user_owns_attachment( $user_id, (int) $old_avatar ) ) {
737 + wp_delete_attachment( (int) $old_avatar );
738 + }
725 739
726 - if ( ! empty( $old_avatar ) ) {
740 + update_user_meta( $user_id, 'avatar', (int) $attachment_id );
727 741
728 - wp_delete_attachment( $old_avatar );
742 + return $submitted;
743 + }
729 744
730 - }
745 + // No file in $_FILES. The avatar uploader (plupload) uploads the image
746 + // asynchronously and posts the resulting attachment ID as a hidden field
747 + // value, so the normal "change avatar" flow lands here with the new ID and
748 + // no $_FILES — we must not reject it.
749 + //
750 + // Block meta poisoning: only persist an attachment the submitting user
751 + // actually owns (a no-op re-save of their current avatar is also allowed).
752 + // An attacker-supplied ID owned by someone else is discarded so it cannot
753 + // be stored in user meta and later weaponised into wp_delete_attachment()
754 + // on a subsequent $_FILES submission.
755 + $current = (int) get_user_meta( $user_id, 'avatar', true );
756 + $posted = array_key_exists( 'avatar', $submitted ) ? $submitted['avatar'] : null;
731 757
732 - update_user_meta( $form->get_user()->ID, 'avatar', $attachment_id );
758 + // Absent, empty, or non-scalar value means the avatar was cleared. The
759 + // non-scalar guard blocks array poisoning (e.g. avatar[]=foo would
760 + // otherwise coerce via (int) to 1 and bypass the ownership check below).
761 + if ( null === $posted || '' === $posted || ! is_scalar( $posted ) ) {
762 + /* The picture has been removed. */
763 + $submitted['avatar'] = '';
764 + return $submitted;
765 + }
733 766
734 - } else {
735 - /**
736 - * Handle image upload error.
737 - *
738 - * @todo
739 - */
740 - }//end if
741 - } elseif ( ! array_key_exists( 'avatar', $submitted ) ) {
767 + $posted = (int) $posted;
742 768
743 - /* The picture has been removed. */
744 - $submitted['avatar'] = '';
769 + // Discard anything that is neither the current avatar nor an attachment
770 + // this user owns.
771 + if ( $posted !== $current && ! $this->user_owns_attachment( $user_id, $posted ) ) {
772 + $submitted['avatar'] = $current ?: '';
773 + return $submitted;
774 + }
745 775
746 - }//end if
776 + $submitted['avatar'] = $posted;
747 777
748 778 return $submitted;
779 + }
780 +
781 + /**
782 + * Verify the given user owns the given attachment.
783 + *
784 + * @since 1.8.11.2
785 + *
786 + * @param int $user_id The user ID.
787 + * @param int $attachment_id The attachment ID.
788 + * @return bool
789 + */
790 + private function user_owns_attachment( $user_id, $attachment_id ) {
791 + if ( $user_id <= 0 || $attachment_id <= 0 ) {
792 + return false;
793 + }
794 +
795 + $attachment = get_post( $attachment_id );
796 +
797 + return $attachment instanceof WP_Post
798 + && 'attachment' === $attachment->post_type
799 + && (int) $attachment->post_author === (int) $user_id;
749 800 }
750 801 }
751 802
752 803 endif;