From 822472451986ab91902ec7a202f83dac3fb36b23 Mon Sep 17 00:00:00 2001 From: vuckro Date: Wed, 10 Jun 2026 11:24:56 +0200 Subject: [PATCH] fix(security): enforce object ownership in customer-panel AJAX handlers The customer-panel modals in the UI elements register their wu_form handlers with the 'exist' capability (any logged-in user) and reference the target object by a client-supplied id/hash. Several handlers acted on that object without verifying the caller owns it, while sibling handlers in the same files (e.g. handle_user_add_new_domain_modal, and the DNS record handlers via customer_can_manage_dns) already enforce ownership. Because the object identifiers are not secret, any authenticated user (including a subscriber on any sub-site) could target another tenant's objects. This adds the same is_customer_allowed()/ownership guard the codebase already uses elsewhere to: - Domain_Mapping_Element::handle_user_delete_domain_modal (delete domain) - Domain_Mapping_Element::handle_user_make_domain_primary_modal - Current_Site_Element::handle_edit_site (rename site) - Billing_Info_Element::handle_update_billing_address Network admins (manage_network) are unaffected; is_customer_allowed() returns true for them. Co-Authored-By: Claude Fable 5 --- inc/ui/class-billing-info-element.php | 4 ++++ inc/ui/class-current-site-element.php | 4 ++++ inc/ui/class-domain-mapping-element.php | 32 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/inc/ui/class-billing-info-element.php b/inc/ui/class-billing-info-element.php index 6c523f80a..fea1724b4 100644 --- a/inc/ui/class-billing-info-element.php +++ b/inc/ui/class-billing-info-element.php @@ -389,6 +389,10 @@ public function handle_update_billing_address(): void { wp_send_json_error($error); } + if ( ! $membership->is_customer_allowed()) { + wp_send_json_error(new \WP_Error('no-permissions', __('You do not have permissions to perform this action.', 'ultimate-multisite'))); + } + $billing_address = $membership->get_billing_address(); $billing_address->load_attributes_from_post(); diff --git a/inc/ui/class-current-site-element.php b/inc/ui/class-current-site-element.php index fbc6527dc..197ed7c77 100644 --- a/inc/ui/class-current-site-element.php +++ b/inc/ui/class-current-site-element.php @@ -493,6 +493,10 @@ public function handle_edit_site(): void { wp_send_json_error($error); } + if ( ! $site->is_customer_allowed()) { + wp_send_json_error(new \WP_Error('no-permissions', __('You do not have permissions to perform this action.', 'ultimate-multisite'))); + } + $new_title = wu_request('site_title'); if ( ! $new_title) { diff --git a/inc/ui/class-domain-mapping-element.php b/inc/ui/class-domain-mapping-element.php index d4e3dfb99..640205cbc 100644 --- a/inc/ui/class-domain-mapping-element.php +++ b/inc/ui/class-domain-mapping-element.php @@ -579,6 +579,30 @@ public function render_user_delete_domain_modal(): void { $form->render(); } + /** + * Checks whether the current user is allowed to manage a given domain. + * + * The customer-panel domain modals are registered with the 'exist' + * capability (any logged-in user) and reference the target by a + * client-supplied, forgeable id/hash. Authorization must therefore be + * enforced per-object: the user must either be a network admin or the + * owner of the site the domain is mapped to. + * + * @since 2.13.2 + * @param \WP_Ultimo\Models\Domain $domain The domain being acted upon. + * @return bool + */ + protected function current_user_can_manage_domain($domain): bool { + + if (current_user_can('manage_network')) { + return true; + } + + $site = $domain ? $domain->get_site() : false; + + return (bool) ($site && $site->is_customer_allowed()); + } + /** * Handles deletion of the selected domain * @@ -599,6 +623,10 @@ public function handle_user_delete_domain_modal(): void { $get_domain = Domain::get_by_id(wu_request('domain_id')); + if ( ! $get_domain || ! $this->current_user_can_manage_domain($get_domain)) { + wp_send_json_error(new \WP_Error('no-permissions', __('You do not have permissions to perform this action.', 'ultimate-multisite'))); + } + $domain = new Domain($get_domain); if ($domain) { @@ -685,6 +713,10 @@ public function handle_user_make_domain_primary_modal(): void { $domain = wu_get_domain($domain_id); + if ( ! $domain || ! $this->current_user_can_manage_domain($domain)) { + wp_send_json_error(new \WP_Error('no-permissions', __('You do not have permissions to perform this action.', 'ultimate-multisite'))); + } + if ($domain) { $domain->set_primary_domain(true);