Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- 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;