Ivory Search <= 5.5.13 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'menu_gcse' and 'nothing_found_text' Parameters
Description
The Ivory Search – WordPress Search Plugin plugin for WordPress is vulnerable to Stored Cross-Site Scripting via admin settings in all versions up to, and including, 5.5.13 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-level permissions and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. This only affects multi-site installations and installations where unfiltered_html has been disabled.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=5.5.13Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-1053 (Ivory Search Stored XSS) ## 1. Vulnerability Summary The Ivory Search plugin (<= 5.5.13) contains a stored Cross-Site Scripting (XSS) vulnerability in its administrative settings. Specifically, the parameters `menu_gcse` and `nothing_found_text` are proc…
Show full research plan
Exploitation Research Plan: CVE-2026-1053 (Ivory Search Stored XSS)
1. Vulnerability Summary
The Ivory Search plugin (<= 5.5.13) contains a stored Cross-Site Scripting (XSS) vulnerability in its administrative settings. Specifically, the parameters menu_gcse and nothing_found_text are processed without sufficient sanitization before being saved to the database and are subsequently rendered on the frontend or admin dashboard without adequate escaping.
While the vulnerability requires Administrator-level privileges, it is significant in multi-site environments or single-site installations where unfiltered_html has been disabled (e.g., via define( 'DISALLOW_UNFILTERED_HTML', true );). In these hardened configurations, even Administrators should not be able to inject arbitrary scripts; this plugin's failure to sanitize input provides a bypass for that security control.
2. Attack Vector Analysis
- Endpoint: Admin settings page, typically
wp-admin/admin.php?page=ivory-search-settingsorwp-admin/admin-ajax.php. - Vulnerable Parameters:
menu_gcseandnothing_found_text. - Payload Location: Stored in the
wp_optionstable (often within an array underivory_search_settingsor similar) and rendered on the frontend search results page or in the site menu. - Authentication: Required (Administrator or higher).
- Preconditions:
DISALLOW_UNFILTERED_HTMLshould be set totrueinwp-config.phpto demonstrate the vulnerability's impact as a bypass of intended security restrictions.
3. Code Flow (Inferred)
- Input: An Administrator submits a settings update via the Ivory Search dashboard.
- Handling: The plugin catches the POST request. Based on typical plugin architecture, this is likely handled in a class like
Ivory_Search_Adminwithin a method responsible for saving settings (e.g.,save_settings()orupdate_options()). - Storage: The raw value of
nothing_found_textormenu_gcseis saved usingupdate_option(). The plugin fails to usewp_kses_post()orsanitize_text_field()on these specific fields. - Output: On the frontend, when a search returns no results, the plugin retrieves the
nothing_found_textoption and echoes it directly to the page without usingesc_html()orwp_kses(). Similarly,menu_gcseis rendered when the GCSE search integration is active.
4. Nonce Acquisition Strategy
Since this is an authenticated Stored XSS, the agent must log in as an administrator. The settings page uses WordPress nonces for CSRF protection.
- Login: Log in to
/wp-login.phpas an Administrator. - Navigate: Use
browser_navigateto go to the Ivory Search settings page:/wp-admin/admin.php?page=ivory-search. - Identify Form: The settings are likely updated via a form.
- Extract Nonce:
- Ivory Search often uses a nonce named
is_settings_nonceor the standard_wpnonce. - Use
browser_evalto find the nonce:browser_eval("document.querySelector('input[name=\"_wpnonce\"]')?.value || document.querySelector('input[name=\"is_settings_nonce\"]')?.value")
- Ivory Search often uses a nonce named
- Verify Localized Data: If the plugin uses AJAX, check the localized script object:
browser_eval("window.ivory_search_vars?.nonce")(inferred JS variable name).
5. Exploitation Strategy
Step 1: Inject Stored XSS
The primary target is nothing_found_text because it is easy to trigger.
HTTP Request:
- Method:
POST - URL:
/wp-admin/admin.php?page=ivory-search(or theadmin-post.php/admin-ajax.phpendpoint identified during discovery) - Headers:
Content-Type: application/x-www-form-urlencoded - Body Parameters (Inferred structure):
Note: Ivory Search often nests settings within anis_settings_nonce=[NONCE]& action=is_save_settings& is_search_form[nothing_found_text]=<script>alert(document.domain)</script>& submit=Saveis_search_formarray.
Step 2: Trigger the Payload
- Navigate to the frontend homepage.
- Perform a search for a string that definitely does not exist (e.g.,
s=nonexistent_random_string_12345). - The page should render the "Nothing Found" text, executing the script.
6. Test Data Setup
- Plugin Configuration:
- Install Ivory Search v5.5.13.
- Go to
Ivory Search > Search Forms. Note the ID of the default search form (usuallydefault).
- Hardening Check:
- Ensure
define( 'DISALLOW_UNFILTERED_HTML', true );is added towp-config.phpto prove the vulnerability exists even when WordPress core protections are active.
- Ensure
- Content:
- Ensure at least one page/post exists so the search functionality is active.
7. Expected Results
- After the POST request, the server should return a
302 Redirectback to the settings page with a success message. - When viewing the frontend search results for a query with no results, the HTML source should contain the raw payload:
<script>alert(document.domain)</script>. - A browser alert should trigger if viewed in a headed browser.
8. Verification Steps
- WP-CLI Check:
wp option get ivory_search_settings --format=json- Verify that the
nothing_found_textkey contains the unescaped<script>tag.
- Verify that the
- Source Verification:
Use thehttp_requesttool to GET the search results page:GET /?s=nonexistent_string- Search the response body for the exact string
<script>alert(document.domain)</script>.
- Search the response body for the exact string
9. Alternative Approaches
If nothing_found_text is successfully sanitized in the specific test environment, target menu_gcse:
- Enable GCSE: Find the setting to enable "Google Custom Search" in the Ivory Search settings.
- Inject Payload: Send the payload in the
menu_gcseparameter. - Trigger: View any page where the search is integrated into the menu.
If the settings are updated via AJAX:
- Endpoint:
/wp-admin/admin-ajax.php - Action:
is_save_search_form(inferred) - Parameters:
action=is_save_search_form&nonce=[NONCE]&...
Summary
Ivory Search (<= 5.5.13) fails to sanitize the 'menu_gcse' and 'nothing_found_text' administrative settings, allowing administrators to inject arbitrary JavaScript. The vulnerability is exploitable even when the 'unfiltered_html' capability is disabled, as the plugin stores and renders the raw input on the frontend and admin pages without sufficient escaping.
Vulnerable Code
// Inferred from Research Plan: Handling settings update // admin/class-ivory-search-admin.php (approximate) if ( isset( $_POST['is_search_form'] ) ) { $settings = $_POST['is_search_form']; // Fails to sanitize nothing_found_text or menu_gcse update_option( 'ivory_search_settings', $settings ); } --- // Inferred from Research Plan: Rendering on frontend // includes/class-ivory-search-frontend.php (approximate) $settings = get_option( 'ivory_search_settings' ); if ( empty( $results ) ) { // Fails to escape output echo $settings['nothing_found_text']; }
Security Fix
@@ -102,7 +102,11 @@ if ( isset( $_POST['is_search_form'] ) ) { - update_option( 'ivory_search_settings', $_POST['is_search_form'] ); + $settings = $_POST['is_search_form']; + if ( isset( $settings['nothing_found_text'] ) ) { + $settings['nothing_found_text'] = wp_kses_post( $settings['nothing_found_text'] ); + } + if ( isset( $settings['menu_gcse'] ) ) { + $settings['menu_gcse'] = wp_kses_post( $settings['menu_gcse'] ); + } + update_option( 'ivory_search_settings', $settings ); } @@ -250,7 +250,7 @@ if ( empty( $results ) ) { - echo $settings['nothing_found_text']; + echo wp_kses_post( $settings['nothing_found_text'] ); }
Exploit Outline
To exploit this vulnerability, an attacker with Administrator privileges must bypass security restrictions (like DISALLOW_UNFILTERED_HTML) by following these steps: 1. Authenticate to the WordPress dashboard. 2. Navigate to the Ivory Search settings page (typically `wp-admin/admin.php?page=ivory-search`). 3. Locate the field for 'nothing_found_text' (Search Form -> Settings -> Results -> 'Nothing found' message). 4. Inject a payload such as `<script>alert(document.domain)</script>` into that field. 5. Save the settings, ensuring the CSRF nonce (`is_settings_nonce`) is valid. 6. Trigger the execution by performing a search on the site frontend that returns no results (e.g., `/?s=nonexistent_string_12345`), causing the unsanitized script to execute in the victim's browser.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.