Published : August 13, 2026

CVE-2026-73403: User Registration & Membership – Free & Paid Memberships, Subscriptions, Content Restriction, User Profile, Custom User Registration & Login Builder <= 5.2.6 Missing Authorization PoC, Patch Analysis & Rule

Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 5.2.6
Patched Version 5.2.7
Disclosed August 12, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-73403: The User Registration & Membership plugin for WordPress, versions up to and including 5.2.6, contains a missing authorization vulnerability. An unauthenticated attacker can trigger a restricted administrative action because a capability check is absent on a specific function. The vulnerability has a CVSS score of 5.3 and is categorized under CWE-862 (Missing Authorization).

The root cause lies in the `run_membership_migration_script()` function in `user-registration/includes/admin/class-ur-admin.php`. In the vulnerable version, the function instantiates the `MembershipService` and fetches the logger before verifying the caller’s capabilities. The capability check, `if ( ! current_user_can( ‘manage_options’ ) ) { return; }`, appears after these object instantiations. While the check itself prevents unauthorized execution of the migration logic, the early instantiation of `MembershipService` may trigger side effects, such as loading additional classes or performing operations, before the permission gate is enforced. The patch moves the capability check to the start of the function, ensuring no premature code execution occurs for unauthorized users.

The attack vector is direct invocation of the affected hook. The `run_membership_migration_script` method is likely registered as an admin_init or similar hook, which runs on every admin page load. An unauthenticated attacker can access any `/wp-admin/` URL, such as `/wp-admin/index.php`, to trigger the hook. The vulnerable code path executes without requiring authentication or a nonce, leading to the unauthorized action.

The patch modifies `run_membership_migration_script()` in `class-ur-admin.php`. It moves the capability check to the beginning of the function, before any object instantiations. It also adds a class existence check for `MembershipService` to handle cases where the class is not available, and delays instantiation until after the capability check. This ensures that only users with `manage_options` capability can trigger the migration logic, and that the service is only instantiated when needed.

The impact of this vulnerability is unauthorized access to administrative functionality. An unauthenticated attacker could potentially trigger migration operations or other side effects, leading to data corruption, configuration changes, or information disclosure. The vulnerability is rated medium severity (CVSS 5.3) due to the limited direct impact of the unauthorized action, but it still violates the principle of least privilege and could be chained with other vulnerabilities.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/user-registration/chunks/dashboard.asset.php
+++ b/user-registration/chunks/dashboard.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '8bf239503745017ae83b');
+<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => 'a90781b6227decba5f58');
--- a/user-registration/includes/admin/class-ur-admin-menus.php
+++ b/user-registration/includes/admin/class-ur-admin-menus.php
@@ -583,12 +583,22 @@
 				$content_rules->add_urcr_menus();
 			}

+			// Instantiate regardless of the gate: the constructor wires the invoice
+			// download handler and gateway options, which must keep working even when
+			// the Payments submenu itself is hidden.
 			$orders_obj = new Orders();
-			$orders_obj->add_orders_menu();
+
+			if ( ur_should_show_payments_menu() ) {
+				$orders_obj->add_orders_menu();
+			}

 			if ( ur_check_module_activation( 'membership' ) ) {
+				// Instantiated for its admin_init delete handler, see above.
 				$subscription_obj = new Subscriptions();
-				$subscription_obj->add_menu();
+
+				if ( ur_should_show_subscriptions_menu() ) {
+					$subscription_obj->add_menu();
+				}
 			}

 			if ( UR_PRO_ACTIVE && ur_check_module_activation( 'coupon' ) && class_exists( 'WPEverestURMembershipCouponsCoupons' ) ) {
--- a/user-registration/includes/admin/class-ur-admin.php
+++ b/user-registration/includes/admin/class-ur-admin.php
@@ -67,12 +67,12 @@
 	 */
 	public function run_membership_migration_script() {

-		$membership_service = new MembershipService();
-		$logger             = ur_get_logger();
-
 		if ( ! current_user_can( 'manage_options' ) ) {
 			return;
 		}
+
+		$logger = ur_get_logger();
+
 		update_option( 'user_registration_content_restriction_enable', true );
 		if ( ur_check_module_activation( 'payments' ) && ! get_option( 'global_paypal_setting_migration', false ) ) {
 			$get_all_forms = ur_get_all_user_registration_form();
@@ -89,6 +89,18 @@

 		if ( UR_PRO_ACTIVE && UR_VERSION <= '5.0' && is_plugin_active( 'user-registration-membership/user-registration-membership.php' ) && ! get_option( 'membership_migration_finished', false ) ) {

+			if ( ! class_exists( MembershipService::class ) ) {
+				$logger->error(
+					'MembershipService class not found. Skipping membership migration.',
+					array(
+						'source' => 'migration-logger',
+					)
+				);
+				return;
+			}
+
+			$membership_service = new MembershipService();
+
 			deactivate_plugins( 'user-registration-membership/user-registration-membership.php' );

 			$logger->notice( '---------- Begin Membership Migration. ----------', array( 'source' => 'migration-logger' ) );
@@ -384,7 +396,7 @@
 		if ( $user_id > 0 ) {
 			$user_meta    = get_userdata( $user_id );
 			$user_roles   = $user_meta->roles;
-			$option_roles = get_option( 'user_registration_general_setting_disabled_user_roles', array() );
+			$option_roles = get_option( 'user_registration_general_setting_disabled_user_roles', array( 'subscriber' ) );
 			if ( ! is_array( $option_roles ) ) {
 				$option_roles = array();
 			}
--- a/user-registration/includes/admin/notifications/class-ur-admin-notices.php
+++ b/user-registration/includes/admin/notifications/class-ur-admin-notices.php
@@ -60,6 +60,7 @@
 		add_action('admin_init', array(__CLASS__, 'user_registration_install_pages_notice'));
 		add_action('admin_notices', array(__CLASS__, 'php_deprecation_notice'));
 		add_action('admin_init', array(__CLASS__, 'same_membership_in_group'));
+		add_action('admin_notices', array(__CLASS__, 'registration_disabled_notice'));

 		/**
 		 * Render Notice with Logo and Buttons.
@@ -773,6 +774,11 @@

 				foreach ($wp_filter[$wp_notice]->callbacks as $priority => $hooks) {
 					foreach ($hooks as $name => $arr) {
+						// Always keep the registration disabled notice, it blocks every registration form.
+						if (is_string($name) && false !== strpos($name, 'registration_disabled_notice')) {
+							continue;
+						}
+
 						// Remove all notices if the page is form builder page.
 						if ('add-new-registration' === $_REQUEST['page'] || 'user-registration-dashboard' === $_REQUEST['page']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
 							unset($wp_filter[$wp_notice]->callbacks[$priority][$name]);
@@ -900,6 +906,27 @@
 	}

 	/**
+	 * Notify admins when the WordPress "Anyone can register" option blocks registration.
+	 *
+	 * @since 5.2.7
+	 */
+	public static function registration_disabled_notice()
+	{
+		if (ur_users_can_register() || ! current_user_can('manage_options')) {
+			return;
+		}
+
+		$message = '<strong>' . esc_html__('User Registration & Membership: ', 'user-registration') . '</strong>';
+		$message .= sprintf(
+			/* translators: %1$s - WordPress general settings link. */
+			__('Your registration forms are showing "Registration is currently disabled." because the WordPress <strong>Anyone can register</strong> option is turned off. Enable it in <a href="%1$s">General Settings</a> to let visitors register.', 'user-registration'),
+			esc_url(admin_url('options-general.php#users_can_register'))
+		);
+
+		echo '<div class="notice notice-warning is-dismissible ur-registration-disabled-notice"><p>' . wp_kses_post($message) . '</p></div>';
+	}
+
+	/**
 	 * If user have same membership in multiple groupds display the notice.
 	 */
 	public static function same_membership_in_group()
--- a/user-registration/includes/admin/settings/class-ur-settings-membership.php
+++ b/user-registration/includes/admin/settings/class-ur-settings-membership.php
@@ -76,39 +76,7 @@
 				 *
 				 * @param array Options to be enlisted.
 				 */
-				$settings = apply_filters(
-					'user_registration_membership_settings',
-					array(
-						'title'    => '',
-						'sections' => array(
-							'membership_settings' => array(
-								'title'    => __( 'General', 'user-registration' ),
-								'type'     => 'card',
-								'desc'     => sprintf(
-									/* translators: %s - Admin URL for membership page settings */
-									__( '<strong>Membership page setting has moved.</strong> Configure your membership page <a href="%s">here</a>.', 'user-registration' ),
-									admin_url( 'admin.php?page=user-registration-settings&tab=general&section=pages' )
-								),
-								'settings' => array(
-									array(
-										'title'    => __( 'Renewal Behaviour', 'user-registration' ),
-										'desc'     => __( 'Choose how membership subscriptions are renewed, automatically through the payment provider or manually by the user', 'user-registration' ),
-										'id'       => 'user_registration_renewal_behaviour',
-										'type'     => 'select',
-										'default'  => 'automatic',
-										'class'    => 'ur-enhanced-select',
-										'css'      => '',
-										'options'  => array(
-											'automatic' => __( 'Renew Automatically', 'user-registration' ),
-											'manual'    => __( 'Renew Manually', 'user-registration' ),
-										),
-										'desc_tip' => true,
-									),
-								),
-							),
-						),
-					)
-				);
+				$settings = apply_filters( 'user_registration_membership_settings', $this->get_general_membership_settings() );
 			} elseif ( 'content-rules' === $current_section ) {
 				$settings = $this->urcr_settings();
 			}
@@ -116,6 +84,106 @@
 		}

 		/**
+		 * General membership settings (Renewal Behaviour), or an empty-state
+		 * notice in place of it when no active membership plan exists yet.
+		 *
+		 * @return array
+		 */
+		private function get_general_membership_settings() {
+			$card = array(
+				'title' => __( 'General', 'user-registration' ),
+				'type'  => 'card',
+			);
+
+			$plan_state = $this->get_membership_plan_state();
+
+			if ( 'active' !== $plan_state ) {
+				if ( 'inactive' === $plan_state ) {
+					$card['desc'] = sprintf(
+						/* translators: %s - Admin URL to the membership plan list */
+						__( '<strong>No active membership plan.</strong> Activate a plan to configure the Renewal Behaviour. <a href="%s">Manage memberships →</a>', 'user-registration' ),
+						admin_url( 'admin.php?page=user-registration-membership' )
+					);
+				} else {
+					$card['desc'] = sprintf(
+						/* translators: %s - Admin URL to create a new membership plan */
+						__( '<strong>No membership plans yet.</strong> Set up a plan to configure the Renewal Behaviour. <a href="%s">Create a membership →</a>', 'user-registration' ),
+						admin_url( 'admin.php?page=user-registration-membership&action=add_new_membership' )
+					);
+				}
+
+				$card['settings'] = array();
+			} else {
+				$is_new_installation = ur_string_to_bool( get_option( 'urm_is_new_installation', '' ) );
+				if ( ! $is_new_installation ) {
+					$card['desc'] = sprintf(
+						/* translators: %s - Admin URL for membership page settings */
+						__( '<strong>Membership page setting has moved.</strong> Configure your membership page <a href="%s">here</a>.', 'user-registration' ),
+						admin_url( 'admin.php?page=user-registration-settings&tab=general&section=pages' )
+					);
+				}
+				$card['settings'] = array(
+					array(
+						'title'    => __( 'Renewal Behaviour', 'user-registration' ),
+						'desc'     => __( 'Choose how membership subscriptions are renewed, automatically through the payment provider or manually by the user', 'user-registration' ),
+						'id'       => 'user_registration_renewal_behaviour',
+						'type'     => 'select',
+						'default'  => 'automatic',
+						'class'    => 'ur-enhanced-select',
+						'css'      => '',
+						'options'  => array(
+							'automatic' => __( 'Renew Automatically', 'user-registration' ),
+							'manual'    => __( 'Renew Manually', 'user-registration' ),
+						),
+						'desc_tip' => true,
+					),
+				);
+			}
+
+			return array(
+				'title'    => '',
+				'sections' => array(
+					'membership_settings' => $card,
+				),
+			);
+		}
+
+		/**
+		 * State of the published membership plans.
+		 *
+		 * Deactivated plans do not count as usable, since there is nothing to
+		 * renew while every plan is switched off.
+		 *
+		 * @return string One of 'active' (at least one active plan exists),
+		 *                'inactive' (plans exist but all are deactivated) or
+		 *                'none' (no plan has been created yet).
+		 */
+		private function get_membership_plan_state() {
+			if ( ! class_exists( 'WPEverestURMembershipAdminRepositoriesMembershipRepository' ) ) {
+				return 'active';
+			}
+
+			$membership_repository = new WPEverestURMembershipAdminRepositoriesMembershipRepository();
+			$memberships           = $membership_repository->get_all_memberships_without_status_filter();
+			$has_plan              = false;
+
+			foreach ( $memberships as $membership ) {
+				if ( ! isset( $membership['post_status'] ) || 'publish' !== $membership['post_status'] ) {
+					continue;
+				}
+
+				$has_plan = true;
+				$status   = isset( $membership['post_content']['status'] ) ? $membership['post_content']['status'] : false;
+
+				if ( ur_string_to_bool( $status ) ) {
+					return 'active';
+				}
+			}
+
+			return $has_plan ? 'inactive' : 'none';
+		}
+
+		/**
 		 * Content restriction settings.
 		 *
 		 * @return array
@@ -137,10 +205,50 @@
 			if ( ! empty( $global_rule_id ) ) {
 				$content_rule_url .= '&id=' . $global_rule_id;
 			}
+
+			$has_membership_plans = false;
+			if ( class_exists( 'WPEverestURMembershipAdminRepositoriesMembershipRepository' ) ) {
+				$membership_repository = new WPEverestURMembershipAdminRepositoriesMembershipRepository();
+				$has_membership_plans  = ! empty( $membership_repository->get_all_memberships_without_status_filter() );
+			}
+
+			/*
+			 * Rules that prove content restriction is actually in use:
+			 * - 'custom' rules, which can only be created in Pro.
+			 * - the auto-migrated "Legacy: Global Site Rule" (flagged with urcr_is_global), present on
+			 *   older installs that used the old global restriction setting, in both free and Pro.
+			 * Membership-generated rules are deliberately excluded, they are already covered by
+			 * $has_membership_plans.
+			 */
+			$has_content_rules = false;
+			if ( post_type_exists( 'urcr_access_rule' ) ) {
+				$has_content_rules = (bool) get_posts(
+					array(
+						'post_type'      => 'urcr_access_rule',
+						'post_status'    => 'any',
+						'posts_per_page' => 1,
+						'fields'         => 'ids',
+						'meta_query'     => array(
+							'relation' => 'OR',
+							array(
+								'key'   => 'urcr_rule_type',
+								'value' => 'custom',
+							),
+							array(
+								'key'     => 'urcr_is_global',
+								'compare' => 'EXISTS',
+							),
+						),
+					)
+				);
+			}
+
+			$has_restriction_in_use = $has_membership_plans || $has_content_rules;
+
 			$sections['user_registration_content_restriction_settings'] = array(
 				'title'    => __( 'Content Restriction', 'user-registration' ),
 				'type'     => 'card',
-				'settings' => array(
+				'settings' => $has_restriction_in_use ? array(
 					array(
 						'title'                            => __( 'Global Restriction Message', 'user-registration' ),
 						'desc'                             => __( ' Default message for all restricted content.', 'user-registration' ),
@@ -153,15 +261,35 @@
 						'show-reset-content-button'        => false,
 						'desc_tip'                         => true,
 					),
-				),
+				) : array(),
 			);
-			$is_new_installation                                        = ur_string_to_bool( get_option( 'urm_is_new_installation', '' ) );
-			if ( ! $is_new_installation ) {
-				$sections['user_registration_content_restriction_settings']['desc'] = sprintf(
-					/* translators: %s - Content rule URL */
-					__( '<strong>The Global Restriction setting has moved.</strong> You can now manage it <a href="%1$s" target="_blank" style="text-decoration: underline;" >here.</a>', 'user-registration' ),
-					esc_url_raw( $content_rule_url )
-				);
+
+			if ( ! $has_restriction_in_use ) {
+				$create_membership_url = admin_url( 'admin.php?page=user-registration-membership&action=add_new_membership' );
+
+				if ( defined( 'UR_PRO_ACTIVE' ) && UR_PRO_ACTIVE && ur_check_module_activation( 'content-restriction' ) ) {
+					$sections['user_registration_content_restriction_settings']['desc'] = sprintf(
+						/* translators: 1: Add new membership URL, 2: Content rule URL */
+						__( '<strong>No membership plans yet.</strong> Restrict content by creating a membership plan or setting up Content Rules. <a href="%1$s">Create a membership →</a> <a href="%2$s">Set up Content Rules →</a>', 'user-registration' ),
+						esc_url( $create_membership_url ),
+						esc_url( $content_rule_url )
+					);
+				} else {
+					$sections['user_registration_content_restriction_settings']['desc'] = sprintf(
+						/* translators: %s - Add new membership URL */
+						__( '<strong>No membership plans yet.</strong> Create a membership plan to start restricting content and customize this message. <a href="%s">Create a membership →</a>', 'user-registration' ),
+						esc_url( $create_membership_url )
+					);
+				}
+			} else {
+				$is_new_installation = ur_string_to_bool( get_option( 'urm_is_new_installation', '' ) );
+				if ( ! $is_new_installation ) {
+					$sections['user_registration_content_restriction_settings']['desc'] = sprintf(
+						/* translators: %s - Content rule URL */
+						__( '<strong>The Global Restriction setting has moved.</strong> You can now manage it <a href="%1$s" target="_blank" style="text-decoration: underline;" >here.</a>', 'user-registration' ),
+						esc_url_raw( $content_rule_url )
+					);
+				}
 			}

 			return apply_filters(
--- a/user-registration/includes/blocks/block-types/class-ur-block-membership-buy-now.php
+++ b/user-registration/includes/blocks/block-types/class-ur-block-membership-buy-now.php
@@ -55,7 +55,7 @@
 		$block_id = isset( $this->attributes['clientId'] ) ? $this->attributes['clientId'] : '';
 		$attr     = $this->attributes;

-		$page_id = get_option( 'user_registration_member_registration_page_id' );
+		$page_id = ur_get_translated_page_id( get_option( 'user_registration_member_registration_page_id' ) );

 		$page_url = get_permalink( absint( $page_id ) );

@@ -302,7 +302,7 @@
 		$membership_details = $membership_repository->get_single_membership_by_ID( $membership_id );

 		$intended_action = $membership_service->fetch_intended_action( $action_to_take, $membership_details, $user_membership_ids );
-		$thank_you_page_id   = get_option( 'user_registration_thank_you_page_id', false );
+		$thank_you_page_id   = ur_get_translated_page_id( get_option( 'user_registration_thank_you_page_id', false ) );

 		$redirect_link_builder = array(
 				'action'  => $intended_action,
--- a/user-registration/includes/class-ur-ajax.php
+++ b/user-registration/includes/class-ur-ajax.php
@@ -94,6 +94,7 @@
 			'create_default_form'                  => false,
 			'generate_required_pages'              => false,
 			'handle_default_wordpress_login'       => false,
+			'enable_emails'                        => false,
 			'skip_site_assistant_section'          => false,
 			'login_settings_page_validation'       => false,
 			'activate_dependent_module'            => false,
@@ -273,8 +274,23 @@
 			}
 		}

-		$profile                        = user_registration_form_data( $user_id, $form_id );
-		$is_admin_user                  = $_POST['is_admin_user'] ?? false;
+		$profile = user_registration_form_data( $user_id, $form_id );
+
+		// No resolvable UR form (e.g. wp-admin/WooCommerce created user) - fall back to basic fields.
+		if ( empty( $profile ) ) {
+			$profile = ur_get_non_urm_user_profile_fields( $user_id );
+		}
+
+		if ( empty( $profile ) ) {
+			wp_send_json_error(
+				array(
+					'message' => __( 'Unable to update profile. No editable profile fields were found for this account.', 'user-registration' ),
+				)
+			);
+		}
+
+		// Server-derived, not client-supplied - avoids a spoofable $_POST flag.
+		$is_admin_user                  = $user_id !== get_current_user_id();
 		list( $profile, $single_field ) = urm_process_profile_fields( $profile, $single_field, $form_data, $form_id, $user_id, $is_admin_user );
 		$user                           = get_userdata( $user_id );

@@ -301,7 +317,7 @@
 				'email'          => ! empty( $single_field['user_registration_user_email'] ) ? $single_field['user_registration_user_email'] : '',
 			);

-			if ( $email_updated && ! is_admin() ) {
+			if ( $email_updated && ! $is_admin_user ) {
 				UR_Form_Handler::send_confirmation_email( $user, $pending_email, $form_id );
 				$response['oldUserEmail'] = $user->user_email;
 				/* translators: %s : user email */
@@ -329,7 +345,7 @@
 				);
 			}

-			if ( is_admin() && ! empty( $pending_email ) ) {
+			if ( $is_admin_user && ! empty( $pending_email ) ) {
 				wp_update_user(
 					array(
 						'ID'         => $user_id,
@@ -2614,6 +2630,25 @@
 	}

 	/**
+	 * Turn the master "Disable emails" setting back off.
+	 */
+	public static function enable_emails() {
+		check_ajax_referer( 'wp_rest', 'security' );
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error( array( 'message' => __( 'You do not have permission to modify email settings.', 'user-registration' ) ) );
+		}
+
+		update_option( 'user_registration_email_setting_disable_email', 'no' );
+
+		wp_send_json_success(
+			array(
+				'message' => __( 'Emails have been enabled successfully.', 'user-registration' ),
+			)
+		);
+	}
+
+	/**
 	 * Skip site assistant section.
 	 *
 	 * @since 4.0
--- a/user-registration/includes/class-ur-emailer.php
+++ b/user-registration/includes/class-ur-emailer.php
@@ -97,11 +97,15 @@
 	 * @return void
 	 */
 	public static function ur_after_register_mail( $valid_form_data, $form_id, $user_id ) {
+		if ( ur_option_checked( 'user_registration_email_setting_disable_email' ) ) {
+			return;
+		}
+
 		$valid_form_data = ur_array_clone( $valid_form_data );

 		$login_option = ur_get_user_login_option( $user_id );

-		if ( ( 'email_confirmation' !== $login_option || 'admin_approval_after_email_confirmation' !== $login_option ) && ur_option_checked( 'user_registration_email_setting_disable_email' ) ) {
+		if ( ( 'email_confirmation' !== $login_option && 'admin_approval_after_email_confirmation' !== $login_option ) && ur_option_checked( 'user_registration_email_setting_disable_email' ) ) {
 			return;
 		}
 		/**
@@ -134,7 +138,7 @@

 			self::send_mail_to_user( $email, $username, $user_id, $data_html, $name_value, $attachments, $template_id );

-			if ( 'admin_approval' === $login_option || 'admin_approval_after_email_confirmation' === $login_option ) {
+			if ( 'admin_approval' === $login_option ) {
 				self::send_approve_link_in_email( $email, $username, $user_id, $data_html, $name_value, $attachments, $template_id );
 			}
 			self::send_mail_to_admin( $email, $username, $user_id, $data_html, $name_value, $attachments, $template_id );
@@ -294,6 +298,11 @@
 	public static function user_registration_process_and_send_email( $email, $subject, $message, $header, $attachment, $template_id ) {

 		$logger = ur_get_logger();
+
+		if ( ur_option_checked( 'user_registration_email_setting_disable_email' ) ) {
+			$logger->notice( 'Email not sent: emails are disabled from email settings.', array( 'source' => 'ur_mail_logs' ) );
+			return true;
+		}
 		$logger->notice( '=============== Email Sending Start ================', array( 'source' => 'ur_mail_logs' ) );
 		$logger->debug(
 			'Email details:' . "n" . wp_json_encode(
@@ -564,9 +573,6 @@
 	 */
 	public static function ur_profile_details_changed_mail( $user_id, $form_id ) {

-		if ( ur_option_checked( 'user_registration_email_setting_disable_email' ) ) {
-			return;
-		}
 		$profile      = user_registration_form_data( $user_id, $form_id );
 		$name_value   = array();
 		$data_html    = '<table class="user-registration-email__entries" cellpadding="0" cellspacing="0"><tbody>';
--- a/user-registration/includes/class-ur-shortcodes.php
+++ b/user-registration/includes/class-ur-shortcodes.php
@@ -265,18 +265,9 @@
 	 * @param mixed $atts Extra attributes.
 	 */
 	public static function form( $atts ) {
-		/**
-		 * Applies a filter to override the 'users_can_register' setting.
-		 *
-		 * The 'ur_register_setting_override' filter allows developers to customize
-		 * the 'users_can_register' setting by providing an alternative value.
-		 *
-		 * @param bool $default_value Default value retrieved from the 'users_can_register' setting.
-		 */
-		$users_can_register = apply_filters( 'ur_register_setting_override', get_option( 'users_can_register' ) );
-		$check_user_state   = isset( $atts['userState'] ) && 'logged_in' === $atts['userState'];
+		$check_user_state = isset( $atts['userState'] ) && 'logged_in' === $atts['userState'];

-		if ( ! is_user_logged_in() && ! $check_user_state && ! $users_can_register ) {
+		if ( ! is_user_logged_in() && ! $check_user_state && ! ur_users_can_register() ) {
 			return apply_filters( 'ur_register_pre_form_message', '<p class="alert" id="ur_register_pre_form_message">' . __( 'Registration is currently disabled.', 'user-registration' ) . '</p>' );
 		}

--- a/user-registration/includes/class-ur-user-approval.php
+++ b/user-registration/includes/class-ur-user-approval.php
@@ -315,8 +315,9 @@
 				$last_order               = $members_order_repository->get_member_orders( $user->ID );
 			}

-			$payment_status = get_user_meta( $user->ID, 'ur_payment_status', true );
-			$is_member      = $is_membership_active && ! empty( $membership ) && ! empty( $last_order );
+			$payment_status   = get_user_meta( $user->ID, 'ur_payment_status', true );
+			$requires_payment = 'yes' === get_user_meta( $user->ID, 'ur_requires_payment', true );
+			$is_member        = $is_membership_active && ! empty( $membership ) && ! empty( $last_order );
 			if ( $is_member ) {
 				$payment_status            = $last_order['status'];
 				$membership_payment_method = $last_order['payment_method'];
@@ -331,7 +332,7 @@
 			 */
 			do_action( 'ur_user_before_check_payment_status_on_login', $payment_status, $user );

-			if ( ! empty( $payment_status ) && 'completed' !== $payment_status ) {
+			if ( 'completed' !== $payment_status && ( $requires_payment || ! empty( $payment_status ) ) ) {
 				$message = '<strong>' . __( 'ERROR:', 'user-registration' ) . '</strong> ' . __( 'Your account is still pending payment.', 'user-registration' );

 				$payment_method = $is_member ? $membership_payment_method : get_user_meta( $user->ID, 'ur_payment_method', true );
--- a/user-registration/includes/frontend/class-ur-frontend-form-handler.php
+++ b/user-registration/includes/frontend/class-ur-frontend-form-handler.php
@@ -60,6 +60,10 @@
 			)
 		);

+		// Reset per-submission static state so a prior request can't leak stale data into this one (e.g. on persistent PHP workers).
+		self::$response_array  = array();
+		self::$valid_form_data = array();
+
 		self::$form_id      = $form_id;
 		$post_content_array = ( $form_id ) ? UR()->form->get_form( $form_id, array( 'content_only' => true ) ) : array();

@@ -506,6 +510,19 @@
 		$login_option = ur_get_user_login_option( $user_id );
 		update_user_meta( $user_id, 'ur_login_option', $login_option );

+		// Server-side payment-gate flag so login fails closed even if the client omits membership signals. UR-4811.
+		if ( 'payment' === $login_option && function_exists( 'ur_check_module_activation' ) && ur_check_module_activation( 'membership' ) ) {
+			$hidden_fields = isset( $_POST['urcl_hide_fields'] ) ? (array) json_decode( wp_unslash( $_POST['urcl_hide_fields'] ), true ) : array();
+			foreach ( $valid_form_data as $field ) {
+				if ( isset( $field->extra_params['field_key'] ) && 'membership' === $field->extra_params['field_key'] ) {
+					if ( ! in_array( $field->field_name, $hidden_fields, true ) ) {
+						update_user_meta( $user_id, 'ur_requires_payment', 'yes' );
+					}
+					break;
+				}
+			}
+		}
+
 		$current_language = ur_get_current_language();
 		$current_language = isset( $_POST['registration_language'] ) ? ur_clean( $_POST['registration_language'] ) : $current_language; //phpcs:ignore.
 		update_user_meta( $user_id, 'ur_registered_language', $current_language );
--- a/user-registration/includes/frontend/class-ur-frontend.php
+++ b/user-registration/includes/frontend/class-ur-frontend.php
@@ -93,10 +93,11 @@
 				$form_data = json_decode( wp_unslash( $_POST['form_data'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

 			foreach ( $form_data as $data ) {
-				if ( isset( $data->field_name ) && 'user_registration_profile_pic_url' === $data->field_name ) {
+				// Unrendered fields send field_name only - require value too, or this wipes the picture.
+				if ( isset( $data->field_name, $data->value ) && 'user_registration_profile_pic_url' === $data->field_name ) {
 					if ( ! is_array( $data->value ) && ! ur_is_valid_url( $data->value ) ) {
 						$valid_form_data['profile_pic_url']        = new stdClass();
-						$valid_form_data['profile_pic_url']->value = isset( $data->value ) ? $data->value : '';
+						$valid_form_data['profile_pic_url']->value = $data->value;
 					}
 				}
 			}
@@ -361,6 +362,10 @@
 	 * Check if can add payment tabs.
 	 */
 	public function ur_register_payment_tab_if_eligible() {
+		// Rewrite rules are global, so the endpoints are always registered regardless of the current user.
+		$this->ur_add_payments_tab_endpoint();
+		$this->ur_add_membership_tab_endpoint();
+
 		$user_id = get_current_user_id();

 		$payment_method = get_user_meta( $user_id, 'ur_payment_method', true );
@@ -374,7 +379,6 @@
 		$is_admin = in_array( 'administrator', (array) $user->roles, true );

 		if ( 'membership' === $user_source || $payment_method || $is_admin ) {
-			add_action( 'wp_loaded', array( $this, 'ur_add_payments_tab_endpoint' ) );
 			add_filter( 'user_registration_account_menu_items', array( $this, 'urm_payment_history_tab' ), 10, 1 );
 			add_action(
 				'user_registration_account_urm-payments_endpoint',
@@ -386,7 +390,6 @@
 		}

 		if ( 'membership' === $user_source || ( ! empty( $payment_method ) && ( ! empty( $ur_payment_subscription ) || 'paypal_standard' === $payment_method ) ) ) {
-			add_action( 'wp_loaded', array( $this, 'ur_add_membership_tab_endpoint' ) );
 			add_filter( 'user_registration_account_menu_items', array( $this, 'ur_membership_tab' ), 10, 1 );
 			add_action(
 				'user_registration_account_ur-membership_endpoint',
@@ -408,9 +411,6 @@
 		$new_items['urm-payments'] = __( 'Payments', 'user-registration' );
 		$items                     = array_merge( $items, $new_items );

-		$mask = Ur()->query->get_endpoints_mask();
-		add_rewrite_endpoint( 'ur-membership', $mask );
-
 		return $this->insert_after_helper( $items, $new_items, 'edit-profile' );
 	}

@@ -450,7 +450,7 @@
 		$is_admin = in_array( 'administrator', (array) $user->roles, true );

 		if ( $is_admin ) {
-			echo esc_html_e( 'You do not have any payment records', 'user-registration' );
+			esc_html_e( 'You do not have any payment records', 'user-registration' );
 			return;
 		}

@@ -564,7 +564,7 @@
 		$mask = Ur()->query->get_endpoints_mask();

 		add_rewrite_endpoint( 'urm-payments', $mask );
-		flush_rewrite_rules();
+		ur_maybe_flush_rewrite_rules( 'urm-payments' );
 	}

 	/**
@@ -574,7 +574,7 @@
 		$mask = Ur()->query->get_endpoints_mask();

 		add_rewrite_endpoint( 'ur-membership', $mask );
-		flush_rewrite_rules();
+		ur_maybe_flush_rewrite_rules( 'ur-membership' );
 	}

 	/**
--- a/user-registration/includes/functions-ur-core.php
+++ b/user-registration/includes/functions-ur-core.php
@@ -3737,7 +3737,8 @@
 				'requires_membership' => true,
 			),
 			'user_registration_thank_you_page_id'          => array(
-				'name'                => 'membership-thankyou',
+				// Same slug the setup wizard uses, so the thank-you URL does not depend on which flow created the page.
+				'name'                => 'thankyou',
 				'title'               => __( 'Membership Thank You', 'user-registration' ),
 				'content'             => '[user_registration_membership_thank_you]',
 				'requires_membership' => true,
@@ -5652,13 +5653,7 @@
 			}
 		}

-		/**
-		 * Filter to override the register settings.
-		 * Default value is the get_option('users_can_register')
-		 */
-		$users_can_register = apply_filters( 'ur_register_setting_override', get_option( 'users_can_register' ) );
-
-		if ( ! is_user_logged_in() && ! $users_can_register ) {
+		if ( ! is_user_logged_in() && ! ur_users_can_register() ) {
 			$logger->warning(
 				sprintf( '[Form #%d] Registration is disabled by the site administrator.', $form_id ) . "n",
 				array(
@@ -8402,6 +8397,23 @@
 }
 add_filter( 'ur_register_setting_override', 'ur_rsssl_anyone_can_register_conflict_resolver', 10, 1 );

+if ( ! function_exists( 'ur_users_can_register' ) ) {
+	/**
+	 * Whether registration is allowed, honoring the WordPress "Anyone can register" option.
+	 *
+	 * @since 5.2.7
+	 *
+	 * @return bool
+	 */
+	function ur_users_can_register() {
+		/**
+		 * Filter to override the register settings.
+		 * Default value is the get_option('users_can_register')
+		 */
+		return (bool) apply_filters( 'ur_register_setting_override', get_option( 'users_can_register' ) );
+	}
+}
+
 add_filter( 'user_registration_settings_prevent_default_login', 'ur_prevent_default_login' );
 if ( ! function_exists( 'ur_prevent_default_login' ) ) {
 	/**
@@ -10662,6 +10674,70 @@
 	}
 }

+if ( ! function_exists( 'ur_has_membership_plans' ) ) {
+	/**
+	 * Check whether at least one active membership plan exists.
+	 *
+	 * A deactivated plan stays published and only has its status flag turned off,
+	 * so the post status alone is not enough. This mirrors the active check in
+	 * MembershipService::prepare_membership_data().
+	 *
+	 * @return bool
+	 */
+	function ur_has_membership_plans() {
+		if ( ! post_type_exists( 'ur_membership' ) ) {
+			return false;
+		}
+
+		$memberships = get_posts(
+			array(
+				'post_type'              => 'ur_membership',
+				'post_status'            => 'publish',
+				'posts_per_page'         => -1,
+				'no_found_rows'          => true,
+				'update_post_meta_cache' => false,
+				'update_post_term_cache' => false,
+			)
+		);
+
+		foreach ( $memberships as $membership ) {
+			$membership_content = json_decode( wp_unslash( $membership->post_content ), true );
+
+			if ( ! empty( $membership_content['status'] ) ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+}
+
+if ( ! function_exists( 'ur_is_membership_registration_type' ) ) {
+	/**
+	 * Check whether the setup wizard's registration type is a membership type
+	 * (as opposed to "Advanced Registration", stored as 'normal').
+	 *
+	 * @return bool
+	 */
+	function ur_is_membership_registration_type() {
+		$membership_type = get_option( 'urm_onboarding_membership_type', 'normal' );
+
+		return in_array( $membership_type, array( 'free_membership', 'paid_membership' ), true );
+	}
+}
+
+if ( ! function_exists( 'ur_should_show_membership_requirements' ) ) {
+	/**
+	 * Membership-registration-type users always need these; Advanced Registration
+	 * users only need them once a membership plan is created.
+	 *
+	 * @return bool
+	 */
+	function ur_should_show_membership_requirements() {
+		return ur_is_membership_registration_type() || ur_has_membership_plans();
+	}
+}
+
 if ( ! function_exists( 'ur_get_site_assistant_data' ) ) {
 	/**
 	 * Get site assistant data with all options status.
@@ -10679,8 +10755,10 @@
 			'user_registration_membership_pricing_page_id' => 'Membership Pricing Page',
 		);

-		// Check if membership module is activated.
-		$is_membership_activated = ur_check_module_activation( 'membership' );
+		$has_membership_plans = ur_has_membership_plans();
+
+		// Advanced Registration users only need membership pages once a plan exists.
+		$show_membership_requirements = ur_is_membership_registration_type() || $has_membership_plans;

 		$missing_pages_data = array();

@@ -10697,7 +10775,7 @@
 			}

 			if ( $is_page_missing ) {
-				// Only include membership pages if membership module is activated.
+				// Only include membership pages if membership requirements should be shown.
 				$is_membership_page = in_array(
 					$option_name,
 					array(
@@ -10708,7 +10786,7 @@
 					true
 				);

-				if ( ! $is_membership_page || $is_membership_activated ) {
+				if ( ! $is_membership_page || $show_membership_requirements ) {
 					$missing_pages_data[] = array(
 						'name'   => $page_name,
 						'option' => $option_name,
@@ -10739,22 +10817,11 @@

 		$membership_field_handled = ( ! $membership_enabled ) || $default_form_has_membership || $membership_field_skipped;

-		$has_membership_plans = false;
-
-		if ( post_type_exists( 'ur_membership' ) ) {
-			$has_membership_plans = (bool) get_posts(
-				array(
-					'post_type'      => 'ur_membership',
-					'post_status'    => 'publish',
-					'posts_per_page' => 1,
-					'fields'         => 'ids',
-				)
-			);
-		}
-
 		$site_assistant_data = array(
+			'users_can_register'                => ur_users_can_register(),
 			'has_default_form'                  => ! empty( $default_form_post ),
 			'missing_pages'                     => $missing_pages_data,
+			'disabled_emails_handled'           => ! ur_option_checked( 'user_registration_email_setting_disable_email' ),
 			'test_email_sent'                   => get_option( 'user_registration_successful_test_mail', false ),
 			'spam_protection_handled'           => ur_string_to_bool( get_option( 'user_registration_captcha_setting_v2_connection_status', false ) ) || ur_string_to_bool( get_option( 'user_registration_spam_protection_skipped', false ) ),
 			'payment_setup_handled'             => $payment_setup_handled,
@@ -10833,7 +10900,7 @@
 		$connections = array();

 		// Check Stripe connection (available in free version).
-		if ( ur_check_module_activation( 'stripe' ) || ur_check_module_activation( 'membership' ) ) {
+		if ( ur_check_module_activation( 'stripe' ) || ur_should_show_membership_requirements() ) {
 			$connections['stripe'] = array(
 				'name'         => 'Stripe',
 				'is_connected' => ur_string_to_bool( get_option( 'urm_stripe_connection_status', false ) ),
@@ -10842,7 +10909,7 @@
 		}

 		// Check PayPal connection (available in free version).
-		if ( ur_check_module_activation( 'payments' ) || ur_check_module_activation( 'membership' ) ) {
+		if ( ur_check_module_activation( 'payments' ) || ur_should_show_membership_requirements() ) {
 			$connections['paypal'] = array(
 				'name'         => 'PayPal',
 				'is_connected' => ur_string_to_bool( get_option( 'urm_paypal_connection_status', false ) ),
@@ -10851,7 +10918,7 @@
 		}

 		// Check Bank connection (membership only).
-		if ( ur_check_module_activation( 'membership' ) ) {
+		if ( ur_should_show_membership_requirements() ) {
 			$connections['bank'] = array(
 				'name'         => 'Bank Payment',
 				'is_connected' => ur_string_to_bool( get_option( 'urm_bank_connection_status', false ) ),
@@ -10991,8 +11058,10 @@
 		$site_assistant_data = ur_get_site_assistant_data();

 		return (
-			! $site_assistant_data['has_default_form']
+			! $site_assistant_data['users_can_register']
+			|| ! $site_assistant_data['has_default_form']
 			|| ! empty( $site_assistant_data['missing_pages'] )
+			|| ! $site_assistant_data['disabled_emails_handled']
 			|| ! $site_assistant_data['test_email_sent']
 			|| ! $site_assistant_data['spam_protection_handled']
 			|| ! $site_assistant_data['payment_setup_handled']
@@ -11012,8 +11081,10 @@
 		$site_assistant_data = ur_get_site_assistant_data();

 		$checks = array(
+			! $site_assistant_data['users_can_register'],
 			! $site_assistant_data['has_default_form'],
 			! empty( $site_assistant_data['missing_pages'] ),
+			! $site_assistant_data['disabled_emails_handled'],
 			! $site_assistant_data['test_email_sent'],
 			! $site_assistant_data['spam_protection_handled'],
 			! $site_assistant_data['payment_setup_handled'],
@@ -11134,6 +11205,51 @@
 	}
 }

+if ( ! function_exists( 'ur_get_non_urm_user_profile_fields' ) ) {
+	/**
+	 * Fallback profile fields for users with no resolvable UR registration form.
+	 *
+	 * @param int $user_id User ID.
+	 *
+	 * @return array
+	 */
+	function ur_get_non_urm_user_profile_fields( $user_id ) {
+		if ( ! get_userdata( $user_id ) ) {
+			return array();
+		}
+
+		return apply_filters(
+			'user_registration_non_urm_user_profile_fields',
+			array(
+				'user_registration_user_login' => array(
+					'label'     => __( 'Username', 'user-registration' ),
+					'type'      => 'text',
+					'field_key' => 'user_login',
+					'required'  => true,
+				),
+				'user_registration_first_name' => array(
+					'label'     => __( 'First Name', 'user-registration' ),
+					'type'      => 'text',
+					'field_key' => 'first_name',
+					'required'  => false,
+				),
+				'user_registration_user_email' => array(
+					'label'     => __( 'User Email', 'user-registration' ),
+					'type'      => 'email',
+					'field_key' => 'user_email',
+					'required'  => true,
+				),
+				'user_registration_last_name'  => array(
+					'label'     => __( 'Last Name', 'user-registration' ),
+					'type'      => 'text',
+					'field_key' => 'last_name',
+					'required'  => false,
+				),
+			),
+			$user_id
+		);
+	}
+}

 if ( ! function_exists( 'urm_update_user_profile_data' ) ) {
 	/**
@@ -11383,6 +11499,314 @@
 	}
 }

+if ( ! function_exists( 'ur_membership_table_exists' ) ) {
+	/**
+	 * Check whether a membership module table exists.
+	 *
+	 * Mirrors the guard the membership repositories apply before querying, e.g.
+	 * OrdersRepository::get_all() and SubscriptionRepository::query().
+	 *
+	 * @param string $table Fully qualified table name.
+	 *
+	 * @return bool
+	 * @since x.x.x
+	 */
+	function ur_membership_table_exists( $table ) {
+		global $wpdb;
+
+		static $checked = array();
+
+		if ( empty( $table ) ) {
+			return false;
+		}
+
+		if ( ! isset( $checked[ $table ] ) ) {
+			$checked[ $table ] = $table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
+		}
+
+		return $checked[ $table ];
+	}
+}
+
+if ( ! function_exists( 'ur_has_membership_plans' ) ) {
+	/**
+	 * Check whether at least one published membership plan exists.
+	 *
+	 * The `ur_membership` post type is only registered when the membership module
+	 * is loaded, so fall back to a direct query when it is not.
+	 *
+	 * @return bool
+	 * @since x.x.x
+	 */
+	function ur_has_membership_plans() {
+		static $has_plans = null;
+
+		if ( null !== $has_plans ) {
+			return $has_plans;
+		}
+
+		if ( post_type_exists( 'ur_membership' ) ) {
+			$has_plans = (bool) get_posts(
+				array(
+					'post_type'      => 'ur_membership',
+					'post_status'    => 'publish',
+					'posts_per_page' => 1,
+					'fields'         => 'ids',
+				)
+			);
+
+			return $has_plans;
+		}
+
+		global $wpdb;
+
+		$has_plans = (bool) $wpdb->get_var(
+			$wpdb->prepare(
+				"SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s LIMIT 1",
+				'ur_membership',
+				'publish'
+			)
+		);
+
+		return $has_plans;
+	}
+}
+
+if ( ! function_exists( 'ur_has_payment_entries' ) ) {
+	/**
+	 * Check whether the Payments page has at least one record to display.
+	 *
+	 * Mirrors the two sources merged by OrdersListTable::prepare_items():
+	 * membership orders (only when the membership module is active, and only rows
+	 * whose plan post and user still exist, matching the INNER JOINs in
+	 * OrdersRepository::get_all()) and normal registration payments, which are
+	 * stored as the `ur_payment_status` user meta.
+	 *
+	 * @return bool
+	 * @since x.x.x
+	 */
+	function ur_has_payment_entries() {
+		global $wpdb;
+
+		static $has_entries = null;
+
+		if ( null !== $has_entries ) {
+			return $has_entries;
+		}
+
+		$has_entries = false;
+
+		// Membership orders, gated exactly as OrdersListTable gates its repository.
+		if ( function_exists( 'ur_check_module_activation' ) && ur_check_module_activation( 'membership' ) ) {
+			$orders_table = class_exists( 'WPEverestURMembershipTableList' )
+				? WPEverestURMembershipTableList::orders_table()
+				: $wpdb->prefix . 'ur_membership_orders';
+
+			if ( ur_membership_table_exists( $orders_table ) ) {
+				$orders_query = "SELECT o.ID
+					FROM {$orders_table} o
+					INNER JOIN {$wpdb->posts} p ON o.item_id = p.ID
+					INNER JOIN {$wpdb->users} u ON o.user_id = u.ID
+					LIMIT 1";
+
+				$has_entries = (bool) $wpdb->get_var( $orders_query ); // phpcs:ignore
+			}
+		}
+
+		// Payments made through a registration form.
+		if ( ! $has_entries ) {
+			$has_entries = (bool) $wpdb->get_var(
+				$wpdb->prepare(
+					"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s LIMIT 1",
+					'ur_payment_status'
+				)
+			);
+		}
+
+		return $has_entries;
+	}
+}
+
+if ( ! function_exists( 'ur_has_subscription_entries' ) ) {
+	/**
+	 * Check whether at least one subscription record exists.
+	 *
+	 * Mirrors SubscriptionRepository::query(), which counts the subscriptions
+	 * table directly without joining anything.
+	 *
+	 * @return bool
+	 * @since x.x.x
+	 */
+	function ur_has_subscription_entries() {
+		global $wpdb;
+
+		static $has_entries = null;
+
+		if ( null !== $has_entries ) {
+			return $has_entries;
+		}
+
+		$subscriptions_table = class_exists( 'WPEverestURMembershipTableList' )
+			? WPEverestURMembershipTableList::subscriptions_table()
+			: $wpdb->prefix . 'ur_membership_subscriptions';
+
+		$has_entries = ur_membership_table_exists( $subscriptions_table )
+			&& (bool) $wpdb->get_var( "SELECT ID FROM {$subscriptions_table} LIMIT 1" ); // phpcs:ignore
+
+		return $has_entries;
+	}
+}
+
+if ( ! function_exists( 'ur_has_payment_enabled_form' ) ) {
+	/**
+	 * Check whether any published registration form collects a payment.
+	 *
+	 * There is no per-form "payments enabled" flag; payment fields live inside the
+	 * form's post_content JSON, so this matches on the `"field_key":"..."` markers
+	 * the same way MembershipRepository::get_membership_forms() does.
+	 *
+	 * @return bool
+	 * @since x.x.x
+	 */
+	function ur_has_payment_enabled_form() {
+		global $wpdb;
+
+		static $has_form = null;
+
+		if ( null !== $has_form ) {
+			return $has_form;
+		}
+
+		/**
+		 * Field keys that on their own make a registration form charge the user.
+		 *
+		 * Deliberately narrower than user_registration_payment_fields(): `total_field`
+		 * and `quantity_field` are payment fields but never trigger a gateway by
+		 * themselves. This list matches the gateway check in
+		 * UR_Pro_Payments_Frontend::payment_process_after_registration().
+		 *
+		 * @param array $field_keys Charging field keys.
+		 *
+		 * @since x.x.x
+		 */
+		$field_keys = apply_filters(
+			'user_registration_payments_menu_field_keys',
+			array( 'single_item', 'multiple_choice', 'subscription_plan' )
+		);
+
+		$conditions = array();
+
+		foreach ( (array) $field_keys as $field_key ) {
+			$pattern      = '%' . $wpdb->esc_like( '"field_key":"' . $field_key . '"' ) . '%';
+			$conditions[] = 'post_content LIKE ' . $wpdb->prepare( '%s', $pattern );
+		}
+
+		if ( ! empty( $conditions ) ) {
+			$where_clause = implode( ' OR ', $conditions );
+
+			$fields_query = "SELECT ID FROM {$wpdb->posts}
+				WHERE post_type = 'user_registration'
+				AND post_status = 'publish'
+				AND ({$where_clause})
+				LIMIT 1";
+
+			$has_form = (bool) $wpdb->get_var( $fields_query ); // phpcs:ignore
+
+			if ( $has_form ) {
+				return $has_form;
+			}
+		}
+
+		$has_form = false;
+
+		// A `range` field only charges when its payment slider is enabled. The stored
+		// value is boolean-ish, so candidate forms have to be confirmed in PHP.
+		if ( ! function_exists( 'ur_get_form_fields' ) ) {
+			return $has_form;
+		}
+
+		$candidates = $wpdb->get_col(
+			$wpdb->prepare(
+				"SELECT ID FROM {$wpdb->posts}
+				WHERE post_type = 'user_registration'
+				AND post_status = 'publish'
+				AND post_content LIKE %s",
+				'%' . $wpdb->esc_like( 'enable_payment_slider' ) . '%'
+			)
+		);
+
+		foreach ( $candidates as $form_id ) {
+			foreach ( (array) ur_get_form_fields( $form_id ) as $field ) {
+				if ( isset( $field->field_key, $field->advance_setting->enable_payment_slider )
+					&& 'range' === $field->field_key
+					&& ur_string_to_bool( $field->advance_setting->enable_payment_slider ) ) {
+					$has_form = true;
+
+					return $has_form;
+				}
+			}
+		}
+
+		return $has_form;
+	}
+}
+
+if ( ! function_exists( 'ur_should_show_payments_menu' ) ) {
+	/**
+	 * Whether the Payments submenu should be registered.
+	 *
+	 * Shown when payments are actually in use: the Payments feature is enabled, the
+	 * page already has records, a membership plan exists, or a registration form
+	 * collects a payment. Onboarding's "Advanced Registration" path matches none of
+	 * these, which is what keeps the menu hidden there.
+	 *
+	 * @return bool
+	 * @since x.x.x
+	 */
+	function ur_should_show_payments_menu() {
+		$show = ( function_exists( 'ur_check_module_activation' ) && ur_check_module_activation( 'payments' ) )
+			|| ur_has_payment_entries()
+			|| ur_has_membership_plans()
+			|| ur_has_payment_enabled_form();
+
+		/**
+		 * Filter whether the Payments submenu is registered.
+		 *
+		 * @param bool $show Whether to show the menu.
+		 *
+		 * @since x.x.x
+		 */
+		return (bool) apply_filters( 'user_registration_show_payments_menu', $show );
+	}
+}
+
+if ( ! function_exists( 'ur_should_show_subscriptions_menu' ) ) {
+	/**
+	 * Whether the Subscriptions submenu should be registered.
+	 *
+	 * The membership module alone is not enough: onboarding enables it before the
+	 * user picks a registration type, so also require something to manage — a
+	 * membership plan or an existing subscription record.
+	 *
+	 * @return bool
+	 * @since x.x.x
+	 */
+	function ur_should_show_subscriptions_menu() {
+		$show = function_exists( 'ur_check_module_activation' )
+			&& ur_check_module_activation( 'membership' )
+			&& ( ur_has_membership_plans() || ur_has_subscription_entries() );
+
+		/**
+		 * Filter whether the Subscriptions submenu is registered.
+		 *
+		 * @param bool $show Whether to show the menu.
+		 *
+		 * @since x.x.x
+		 */
+		return (bool) apply_filters( 'user_registration_show_subscriptions_menu', $show );
+	}
+}
+
 /**
  * Get the count of membership rules.
  * This function checks for content access rules with rule_type = 'membership'.
@@ -12298,3 +12722,21 @@
 		}
 	}
 }
+
+if ( ! function_exists( 'ur_maybe_flush_rewrite_rules' ) ) {
+	// Flush only when one of the given account endpoints is missing from the saved rules.
+	function ur_maybe_flush_rewrite_rules( $endpoints ) {
+		if ( ! get_option( 'permalink_structure' ) ) {
+			return;
+		}
+
+		$rules = get_option( 'rewrite_rules', array() );
+
+		foreach ( (array) $endpoints as $endpoint ) {
+			if ( ! isset( $rules[ '(.?.+?)/' . $endpoint . '(/(.*))?/?$' ] ) ) {
+				flush_rewrite_rules();
+				return;
+			}
+		}
+	}
+}
--- a/user-registration/includes/functions-ur-page.php
+++ b/user-registration/includes/functions-ur-page.php
@@ -236,8 +236,8 @@
 		$permalink = 0 < $my_account_page_id ? get_permalink( $my_account_page_id ) : '';

 		if ( $permalink ) {
-			if ( '/' !== substr( $permalink, -1 ) ) {
-				$permalink = $permalink . '/';
+			if ( false === strpos( $permalink, '?' ) ) {
+				$permalink = trailingslashit( $permalink );
 			}
 			return $permalink;
 		}
--- a/user-registration/includes/validation/class-ur-form-validation.php
+++ b/user-registration/includes/validation/class-ur-form-validation.php
@@ -68,6 +68,10 @@
 	 * @return array
 	 */
 	public function reorganize_form_data( $valid_form_data, $form_field_data, $form_id ) {
+		if ( ! is_array( $valid_form_data ) ) {
+			$valid_form_data = array();
+		}
+
 		if ( empty( $form_field_data ) ) {
 			return $valid_form_data;
 		}
--- a/user-registration/modules/functions-ur-modules.php
+++ b/user-registration/modules/functions-ur-modules.php
@@ -462,7 +462,8 @@
 			'option'  => '',
 		);
 		$pages['membership_thankyou'] = array(
-			'name'    => _x( 'membership-thankyou', 'Page slug', 'user-registration' ),
+			// Same slug the setup wizard uses, so the thank-you URL does not depend on which flow created the page.
+			'name'    => _x( 'thankyou', 'Page slug', 'user-registration' ),
 			'title'   => _x( 'Membership ThankYou', 'Page title', 'user-registration' ),
 			'content' => '[user_registration_membership_thank_you]',
 			'option'  => 'user_registration_thank_you_page_id',
--- a/user-registration/modules/masteriyo/includes/Frontend.php
+++ b/user-registration/modules/masteriyo/includes/Frontend.php
@@ -311,7 +311,7 @@

 		add_rewrite_endpoint( 'urm-courses', $mask );
 		add_rewrite_endpoint( 'urm-course-portal', $mask );
-		flush_rewrite_rules();
+		ur_maybe_flush_rewrite_rules( array( 'urm-courses', 'urm-course-portal' ) );
 	}

 	public function get_current_user_course() {
--- a/user-registration/modules/membership/includes/AJAX.php
+++ b/user-registration/modules/membership/includes/AJAX.php
@@ -945,6 +945,9 @@

 		if ( $update_stripe_order['status'] ) {

+			// UR-4710: payment confirmed — apply any deferred membership role for this in-session member.
+			( new MembersService() )->maybe_grant_pending_role( $member_id );
+
 			if ( $is_upgrading ) {
 				$next_subscription     = json_decode( get_user_meta( $member_id, 'urm_next_subscription_data', true ), true );
 				$previous_subscription = get_user_meta( $member_id, 'urm_previous_subscription_data', true );
@@ -1902,7 +1905,7 @@
 		}

 		if ( ! empty( $_POST['tax_rate'] ) ) {
-			$data['tax_rate']               = sanitize_text_field( $_POST['tax_rate'] );
+			$data['tax_rate']               = max( 0, floatval( $_POST['tax_rate'] ) );
 			$data['tax_calculation_method'] = ! empty( $_POST['tax_calculation_method'] ) ? sanitize_text_field( $_POST['tax_calculation_method'] ) : '1';
 		}

@@ -2179,7 +2182,7 @@
 		}

 		if ( ! empty( $_POST['tax_rate'] ) ) {
-			$data['tax_rate']               = sanitize_text_field( $_POST['tax_rate'] );
+			$data['tax_rate']               = max( 0, floatval( $_POST['tax_rate'] ) );
 			$data['tax_calculation_method'] = ! empty( $_POST['tax_calculation_method'] ) ? sanitize_text_field( $_POST['tax_calculation_method'] ) : '1';
 		}

--- a/user-registration/modules/membership/includes/Admin.php
+++ b/user-registration/modules/membership/includes/Admin.php
@@ -319,6 +319,10 @@
 		}

 		public function add_memberships_in_urcr_settings( $settings ) {
+			if ( empty( $settings['sections']['user_registration_content_restriction_settings']['settings'] ) ) {
+				return $settings;
+			}
+
 			$options             = get_active_membership_id_name();
 			$additional_settings = array(
 				array(
@@ -347,12 +351,22 @@
 		 * is not sent during form submission — it should only fire after payment is confirmed.
 		 */
 		public function set_payment_process_for_membership( $success_params, $valid_form_data, $form_id, $user_id ) {
-			if ( empty( $_POST['is_membership_active'] ) && empty( $_POST['membership_type'] ) ) {
+			$selected_membership = 0;
+			foreach ( (array) $valid_form_data as $field_data ) {
+				if ( isset( $field_data->extra_params['field_key'] ) && 'membership' === $field_data->extra_params['field_key'] ) {
+					$selected_membership = isset( $field_data->value ) ? absint( $field_data->value ) : 0;
+					break;
+				}
+			}
+			if ( empty( $selected_membership ) ) {
 				return $success_params;
 			}

-			$data = isset( $_POST['members_data'] ) ? (array) json_decode( wp_unslash( $_POST['members_data'] ), true ) : array();
-			if ( empty( $data['payment_method'] ) || 'free' === $data['payment_method'] ) {
+			// Determine paid vs free from the server-side membership record, not client members_data.
+			$membership_repository = new MembershipRepository();
+			$membership_data       = $membership_repository->get_single_membership_by_ID( $selected_membership );
+			$membership_meta       = ! empty( $membership_data['meta_value'] ) ? json_decode( wp_unslash( $membership_data['meta_value'] ), true ) : array();
+			if ( empty( $membership_meta['type'] ) || 'free' === $membership_meta['type'] ) {
 				return $success_params;
 			}

@@ -396,37 +410,46 @@
 			if ( ! ur_check_module_activation( 'membership' ) ) {
 				return $success_params;
 			}
-			// membership POST signals present
-			if ( empty( $_POST['is_membership_active'] ) && empty( $_POST['membership_type'] ) ) {
-				return $success_params;
-			}
-			// Guard 3: form has a membership field
-			$has_membership_field  = false;
+			// Membership processing requirement is derived from the server-side selected value of the
+			// membership field, never from client POST signals (is_membership_active / membership_type)
+			// or members_data — those are trivially omitted to skip enrollment and the payment gate.
 			$membership_field_data = null;
 			foreach ( $valid_form_data as $field_data ) {
 				if ( isset( $field_data->extra_params['field_key'] ) && 'membership' === $field_data->extra_params['field_key'] ) {
-					$has_membership_field  = true;
 					$membership_field_data = $field_data;
 					break;
 				}
 			}
-			if ( ! $has_membership_field ) {
+			if ( ! $membership_field_data ) {
 				return $success_params;
 			}

-			$data = apply_filters(
-				'user_registration_membership_before_register_member',
-				isset( $_POST['members_data'] ) ? (array) json_decode( wp_unslash( $_POST['members_data'] ), true ) : array()
-			);
-			if ( empty( $data ) || empty( $data['payment_method'] ) || empty( $data['membership'] ) ) {
+			$selected_membership = isset( $membership_field_data->value ) ? absint( $membership_field_data->value ) : 0;
+			if ( empty( $selected_membership ) ) {
+				// No plan selected server-side: no enrollment intended. Leave a plain account.
 				return $success_params;
 			}

-			// Inject user identity from the just-created user
 			$member    = get_userdata( $user_id );
 			$member_id = $user_id;
 			if ( ! $member ) {
-				return $success_params;
+				wp_delete_user( absint( $user_id ) );
+				wp_send_json_error( array( 'message' => esc_html__( 'Invalid membership selection.', 'user-registration' ) ) );
+			}
+
+			$data = apply_filters(
+				'user_registration_membership_before_register_member',
+				isset( $_POST['members_data'] ) ? (array) json_decode( wp_unslash( $_POST['members_data'] ), true ) : array()
+			);
+			if ( ! is_array( $data ) ) {
+				$data = array();
+			}
+			// Authoritative membership id from the server-side field value, not client members_data.
+			$data['membership'] = $selected_membership;
+
+			// Client-submitted tax_rate must never be negative (would reduce the charge below base).
+			if ( isset( $data['tax_rate'] ) ) {
+				$data['tax_rate'] = max( 0, floatval( $data['tax_rate'] ) );
 			}

 			// Validate that the submitted membership ID is one the form is actually configured to offer.
@@ -511,12 +534,25 @@
 				}
 			}

-			// Get membership type for logging
+			// Resolve membership type from the server-side membership record.
 			$membership_repository = new MembershipRepository();
 			$membership_data       = $membership_repository->get_single_membership_by_ID( $data['membership'] );
-			$membership_meta       = json_decode( wp_unslash( $membership_data['meta_value'] ), true );
-			$membership_type       = $membership_meta['type'] ?? 'unknown';
-			$payment_gateway       = $data['payment_method'] ?? 'unknown';
+			if ( empty( $membership_data ) || empty( $membership_data['meta_value'] ) ) {
+				wp_delete_user( absint( $member_id ) );
+				wp_send_json_error( array( 'message' => esc_html__( 'Invalid membership selection.', 'user-registration' ) ) );
+			}
+			$membership_meta = json_decode( wp_unslash( $membership_data['meta_value'] ), true );
+			$membership_type = $membership_meta['type'] ?? 'unknown';
+
+			// A paid plan needs a payment method to produce a payable order. Missing method (forged or
+			// omitted members_data) must fail closed, not create an unpaid payment-gated account.
+			if ( 'free' === $membership_type ) {
+				$data['payment_method'] = 'free';
+			} elseif ( empty( $data['payment_method'] ) ) {
+				wp_delete_user( absint( $member_id ) );
+				wp_send_json_error( array( 'message' => esc_html__( 'Invalid membership selection.', 'user-registration' ) ) );
+			}
+			$payment_gateway = $data['payment_method'] ?? 'unknown';

 			// Reject attacker-supplied payment_method values that don't match the membership.
 			// A paid/subscription membership must use one of its configured gateways; 'free'
@@ -701,6 +737,7 @@

 			} else {
 				$message = isset( $response['message'] ) ? $response['message'] : esc_html__( 'Sorry! There was an unexpected error while registering the user.', 'user-registration' );
+				wp_delete_user( absint( $member_id ) );
 				wp_send_json_error( array( 'message' => $message ) );
 			}
 		}
--- a/user-registration/modules/membership/includes/Admin/Forms/Views/admin-membership.php
+++ b/user-registration/modules/membership/includes/Admin/Forms/Views/admin-membership.php
@@ -107,6 +107,10 @@
 // Get field label.
 $field_label = esc_html( $this->get_general_setting_data( 'label' ) );

+// A single membership is auto-selected and rendered without a selectable radio,
+// both here in the builder preview and on the live frontend.
+$memberships_count = count( $memberships );
+
 ?>
 <div class="ur-input-type-select ur-admin-template">
 	<div class="ur-label">
@@ -165,7 +169,7 @@
 							}
 						}
 						?>
-						<div class="urmg-plan-card <?php echo

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.