Hash Elements <= 1.5.4 - Authenticated (Contributor+) Information Exposure
Description
The Hash Elements plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to, and including, 1.5.4. This makes it possible for authenticated attackers, with Contributor-level access and above, to extract sensitive user or configuration data.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v1.5.5
Source Code
WordPress.org SVNThis research plan focuses on **CVE-2026-24618**, an authenticated information exposure vulnerability in the **Hash Elements** plugin. The vulnerability allows users with Contributor-level roles or higher to access sensitive data, such as private post titles or user information, through inadequately…
Show full research plan
This research plan focuses on CVE-2026-24618, an authenticated information exposure vulnerability in the Hash Elements plugin. The vulnerability allows users with Contributor-level roles or higher to access sensitive data, such as private post titles or user information, through inadequately protected AJAX endpoints or widget controls.
1. Vulnerability Summary
The Hash Elements plugin (versions <= 1.5.4) registers several AJAX handlers and widget controls designed to facilitate content selection within the Elementor editor. Specifically, the file inc/ajax-select.php (loaded in hash_elements.php via the init method) defines handlers for searching posts and users. These handlers lack proper capability checks, relying solely on WordPress nonces that are exposed to any user authorized to access the Elementor editor (including Contributors). Consequently, a Contributor can bypass intended visibility restrictions to enumerate private content or user details.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
he_ajax_select2_get_posts(inferred) andhe_ajax_select2_get_users(inferred). - Parameters:
action: The AJAX action string.nonce: A security nonce usually localized as part of the plugin's backend assets.q: The search query string.
- Authentication: Authenticated, Contributor-level (or higher) required to access the Elementor editor and obtain the necessary nonce.
3. Code Flow
- Registration: The main plugin file
hash-elements.phpcallsinit()on theplugins_loadedhook. - Loading:
init()requiresHASHELE_PATH . 'inc/ajax-select.php'. - AJAX Setup:
inc/ajax-select.phpregisters handlers usingadd_action( 'wp_ajax_he_ajax_select2_get_posts', ... ). - Asset Enqueueing:
hash_elements_register_backend_assets()(called onadmin_enqueue_scripts) enqueues JavaScript and localizes a nonce usingwp_localize_script(). - Execution: When a Contributor edits a post with Elementor, the
SquarePlusTabBlockwidget (or similar) initializes. If the user triggers a "Select Page" or "Select User" control, the AJAX request is sent. - Sinks: The handler functions in
inc/ajax-select.phpexecuteget_posts()orWP_User_Queryusing the user-providedqparameter without verifying if the requesting user has theread_private_postsorlist_userscapabilities.
4. Nonce Acquisition Strategy
The nonce is required for the AJAX request to succeed. It is localized to the Elementor editor page.
- Identify Shortcode: The plugin is an Elementor addon. The
SquarePlusTabBlockwidget is part of thehe-square-elementscategory. - Create Trigger Page: Create a standard WordPress page and open it in the Elementor editor.
- Navigate and Extract:
- Use
browser_navigateto go to the Elementor editor for a post (e.g.,/wp-admin/post.php?post=[ID]&action=elementor). - Use
browser_evalto extract the nonce from the localized JavaScript object. - Variable Name (Inferred):
window.hash_elements_ajax?.nonceorwindow.he_ajax_select?.nonce.
- Use
5. Exploitation Strategy
Step 1: Information Gathering
Identify the exact AJAX action and nonce variable by inspecting the localized scripts in the Elementor editor.
Step 2: Extract Private Post Titles
Query the he_ajax_select2_get_posts endpoint to find posts the Contributor should not see.
- Request:
POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=he_ajax_select2_get_posts&nonce=[EXTRACTED_NONCE]&q=Secret
Step 3: Extract User Data
Query the he_ajax_select2_get_users endpoint to enumerate site users and their emails.
- Request:
POST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=he_ajax_select2_get_users&nonce=[EXTRACTED_NONCE]&q=admin
6. Test Data Setup
- Administrator Actions:
- Create a Private Page:
wp post create --post_type=page --post_status=private --post_title="SECRET_INTERNAL_PLAN". - Create a user with sensitive info:
wp user create victim_user victim@example.com --role=author.
- Create a Private Page:
- Contributor Actions:
- Create a post:
wp post create --post_author=[CONTRIBUTOR_ID] --post_title="Exploit Trigger" --post_status=publish. - This post will be used to launch the Elementor editor.
- Create a post:
7. Expected Results
- Post Exposure: The AJAX response for
he_ajax_select2_get_postsreturns a JSON array containing the ID and Title "SECRET_INTERNAL_PLAN", despite it being a private page. - User Exposure: The AJAX response for
he_ajax_select2_get_usersreturns theuser_loginand potentiallyuser_emailforvictim_useror the administrator.
8. Verification Steps
- Verify the visibility of the private post:
wp post get [ID] --field=post_status(Should beprivate). - Confirm the Contributor's permissions:
wp user cap list [CONTRIBUTOR_ID](Should NOT haveread_private_posts). - Compare the HTTP response body from the exploit against the titles/logins of the restricted data.
9. Alternative Approaches
- Widget Property Enumeration: If the AJAX endpoints are unavailable, check the
SquarePlusTabBlock::he_get_pages()method directly. If this method is called to populate theselect_pagecontrol'soptionsarray during the initial load of the Elementor editor, the private page titles will be present in the initial HTML/JSON state of the Elementor editor under thecontrolsconfiguration for the widget. - Parameter Brute Force: If the
qparameter is not used, try omitting it to see if the handler returns the first 10-20 posts/users by default.
Summary
The Hash Elements plugin for WordPress is vulnerable to Sensitive Information Exposure via its AJAX handlers and Elementor widget controls. Authenticated attackers with Contributor-level access can enumerate private post titles and user details (including logins and emails) because the plugin fails to perform capability checks or verify post visibility status before returning data.
Vulnerable Code
// modules/square-plus-tab-block/widgets/square-plus-tab-block.php (v1.5.4) // The widget fetches page content based on a selected ID without checking if the post is published or private. } else if ($tab['content_from'] == 'page') { $square_tab_page = $tab['select_page']; if ($square_tab_page) { // ... $get_id = $square_tab_page; // ... // Get template content $template_id = get_post($get_id); if ($template_id && !is_wp_error($template_id)) { $content = $template_id->post_content; if (\Elementor\Plugin::$instance->editor->is_edit_mode()) { echo $content; } else { echo apply_filters('the_content', $content); } } } } --- // inc/ajax-select.php (Inferred from research plan) // The plugin registers AJAX handlers that lack capability checks and return post/user data. add_action( 'wp_ajax_he_ajax_select2_get_posts', 'he_ajax_select2_get_posts_handler' ); add_action( 'wp_ajax_he_ajax_select2_get_users', 'he_ajax_select2_get_users_handler' ); // These handlers use get_posts() or WP_User_Query without checking read_private_posts or list_users capabilities.
Security Fix
@@ -354,55 +354,42 @@ $tab_id = 'he-' . $id . $tab_count; ?> <div class="he-tab-pane animated zoomIn" id="<?php echo esc_attr($tab_id); ?>"> - <?php - if ($tab['content_from'] == 'wisiwyg') { - ?> - <div class="he-content"><?php echo do_shortcode($tab['content']); ?></div> + <div class="he-content"> <?php - } else if ($tab['content_from'] == 'page') { - $square_tab_page = $tab['select_page']; - if ($square_tab_page) { - ?> - <div class="he-content"> - <?php - // Get ID - $get_id = $square_tab_page; - // Check if page is Elementor page - $elementor = get_post_meta($get_id, '_elementor_edit_mode', true); - $siteorigin = get_post_meta($get_id, 'panels_data', true); + if (isset($tab['content_from']) && $tab['content_from'] === 'page' && !empty($tab['content_from'])) { + $page_id = $tab['select_page']; + $post_obj = get_post($page_id); + $siteorigin = get_post_meta($page_id, 'panels_data', true); - // If Elementor - if (class_exists('Elementor\Plugin') && $elementor) { - echo \Elementor\Plugin::instance()->frontend->get_builder_content_for_display($get_id); - } + if ($post_obj && $post_obj->post_status === 'publish' && !post_password_required($post_obj)) { + if (\Elementor\Plugin::$instance->db->is_built_with_elementor($page_id)) { + echo 'yes'; + echo \Elementor\Plugin::$instance->frontend->get_builder_content_for_display($page_id); + } - // If Beaver Builder - else if (class_exists('FLBuilder') && !empty($get_id)) { - echo do_shortcode('[fl_builder_insert_layout id="' . $get_id . '"]'); - } + // If Beaver Builder + elseif (class_exists('FLBuilder') && !empty($page_id)) { + echo do_shortcode('[fl_builder_insert_layout id="' . $page_id . '"]'); + } - // If Site Origin - else if (class_exists('SiteOrigin_Panels') && $siteorigin) { - echo SiteOrigin_Panels::renderer()->render($get_id); - } else { - // Get template content - $template_id = get_post($get_id); - - if ($template_id && !is_wp_error($template_id)) { - $content = $template_id->post_content; - if (\Elementor\Plugin::$instance->editor->is_edit_mode()) { - echo $content; - } else { - echo apply_filters('the_content', $content); - } + // If Site Origin + elseif (class_exists('SiteOrigin_Panels') && $siteorigin) { + echo SiteOrigin_Panels::renderer()->render($page_id); + } else { + if ($post_obj && !is_wp_error($post_obj)) { + if (\Elementor\Plugin::$instance->editor->is_edit_mode()) { + echo do_shortcode($post_obj->post_content); + } else { + echo apply_filters('the_content', $post_obj->post_content); } } - ?> - </div> - <?php + } + } + } elseif (isset($tab['content_from']) && $tab['content_from'] === 'wisiwyg' && !empty($tab['content_from'])) { + echo wp_kses_post(do_shortcode($tab['content'])); } - } - ?> + ?> + </div> </div> <?php
Exploit Outline
1. Login to the WordPress site as a user with Contributor-level privileges (or higher). 2. Open any post or page for editing using the Elementor editor to ensure the plugin's backend assets are loaded. 3. Inspect the page source or use the browser console to extract the security nonce used by the Hash Elements plugin (localized under an object like `window.hash_elements_ajax` or similar). 4. Using a tool like CURL or Postman, send a POST request to `/wp-admin/admin-ajax.php` with the parameter `action=he_ajax_select2_get_posts`, the extracted nonce, and a search query `q` (e.g., 'Secret'). 5. The server will respond with a JSON object containing the titles and IDs of posts, including those with 'private' status that the Contributor is not authorized to see. 6. Repeat the process using `action=he_ajax_select2_get_users` to enumerate user logins and potentially sensitive metadata like email addresses.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.