Link Hopper <= 2.5 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'hop_name' Parameter
Description
The Link Hopper plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the ‘hop_name’ parameter in all versions up to, and including, 2.5 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-level access, 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
<=2.5# Research Plan: CVE-2025-15483 - Link Hopper Stored XSS ## 1. Vulnerability Summary The **Link Hopper** plugin for WordPress (versions <= 2.5) is vulnerable to **Stored Cross-Site Scripting (XSS)** via the `hop_name` parameter. The vulnerability exists because the plugin fails to sanitize the link…
Show full research plan
Research Plan: CVE-2025-15483 - Link Hopper Stored XSS
1. Vulnerability Summary
The Link Hopper plugin for WordPress (versions <= 2.5) is vulnerable to Stored Cross-Site Scripting (XSS) via the hop_name parameter. The vulnerability exists because the plugin fails to sanitize the link name when it is saved to the database and fails to escape the value when it is rendered in the admin dashboard. While the attack requires Administrator-level privileges, it is significant in environments where unfiltered_html is disabled (such as WordPress Multisite or hardened installations), as it allows an administrator to execute arbitrary JavaScript in the context of other administrators or super-admins.
2. Attack Vector Analysis
- Endpoint: Likely a link creation/editing form in the WordPress admin area or an AJAX handler.
- Vulnerable Parameter:
hop_name(inferred). - Authentication: Authenticated, Administrator+.
- Impact: Stored XSS. Execution occurs when an administrator views the list of "hops" (links) created within the plugin.
- Precondition:
unfiltered_htmlmust be disabled for the user (e.g., standard Administrator on a Multisite installation).
3. Code Flow (Inferred)
- Input: An administrator navigates to the Link Hopper management page (e.g.,
wp-admin/admin.php?page=link-hopper-addor similar). - Processing: The administrator submits a form to create or update a "hop". The request is handled by a function hooked to
admin_initor awp_ajax_action. - Storage: The plugin retrieves
$_POST['hop_name']. It likely saves this value usingupdate_post_meta()or a direct$wpdb->insert()/$wpdb->update()call without applyingsanitize_text_field(). - Retrieval: When an administrator visits the link overview page (
wp-admin/admin.php?page=link-hopper), the plugin retrieves the storedhop_name. - Sink: The retrieved
hop_nameis echoed directly into the HTML table or list without usingesc_html()oresc_attr().
4. Nonce Acquisition Strategy
Since the vulnerability is in the admin area, the form used to save the link will almost certainly be protected by a WordPress nonce.
- Identify the Page: Search the plugin code for
add_menu_pageoradd_submenu_pageto find the admin menu slug.grep -r "add_menu_page" wp-content/plugins/link-hopper/
- Navigate to Form: Use
browser_navigateto go to the link creation page. - Extract Nonce:
- If it is a standard form, the nonce is likely in a hidden input field:
browser_eval("document.querySelector('input[name=\"_wpnonce\"]')?.value"). - If it is an AJAX-driven interface, check for localized scripts:
browser_eval("window.link_hopper_params?.nonce")(inferred variable name).
- If it is a standard form, the nonce is likely in a hidden input field:
- Identify Action: Search for the form handler to determine if the nonce is checked using
check_admin_referer()orcheck_ajax_referer().
5. Exploitation Strategy
Step 1: Force unfiltered_html Restriction
To simulate the vulnerable condition on a standard WordPress install, disable unfiltered_html.
- Command:
wp config set DISALLOW_UNFILTERED_HTML true --raw
Step 2: Identify the Save Action
Determine how the plugin saves links. Look for $_POST['hop_name'] in the codebase.
grep -r "hop_name" wp-content/plugins/link-hopper/
Step 3: Trigger the Storage
Send an authenticated POST request to save a new hop with the XSS payload.
- URL:
https://[target]/wp-admin/admin.php?page=link-hopper-settings(inferred) oradmin-ajax.php. - Method:
POST - Body (URL-encoded):
action=lh_save_hop& (inferred action) hop_name=<script>alert(document.domain)</script>& hop_url=https://example.com& _wpnonce=[EXTRACTED_NONCE]
Step 4: Trigger the Execution
Navigate to the page where links are listed.
- URL:
https://[target]/wp-admin/admin.php?page=link-hopper(inferred)
6. Test Data Setup
- Plugin Activation: Ensure
link-hopperis installed and active. - User Creation: Create an administrator user.
wp user create admin_tester admin@example.com --role=administrator --user_pass=password123
- Config Change: Disable
unfiltered_html(as noted in Step 1). - Identify Slug: Find the exact admin slug for the Link Hopper list page.
7. Expected Results
- The
POSTrequest should return a302redirect or a success message. - Upon navigating to the link list page, a JavaScript alert box displaying the document domain should appear.
- Viewing the page source should show the raw
<script>tag within the HTML table row for thehop_namecolumn.
8. Verification Steps
- Database Check: Verify the payload is stored raw in the database.
wp db query "SELECT * FROM wp_posts WHERE post_title LIKE '%<script>%'"(if stored as a CPT)- OR
wp db query "SELECT * FROM wp_options WHERE option_name LIKE '%link_hopper%'"
- HTML Inspection: Verify the absence of escaping in the response.
- Use
http_requestto fetch the admin list page and check if<script>is present and NOT encoded as<script>.
- Use
9. Alternative Approaches
- Post Meta Exploitation: If
hop_nameis stored inwp_postmeta, try injecting viapost.phpif the plugin uses a custom meta box. - Attribute Injection: If the
hop_nameis echoed inside an attribute (e.g.,<input value="[hop_name]">), use a payload like" onmouseover="alert(1)". - Multisite Test: If the single-site config change doesn't yield results, convert the site to a multisite network and test as a site-administrator (who lacks
unfiltered_htmlby default).
Summary
The Link Hopper plugin for WordPress (versions <= 2.5) is vulnerable to Stored Cross-Site Scripting (XSS) via the 'hop_name' parameter. This allows authenticated administrators to inject arbitrary JavaScript into link names, which then executes in the browser of any user (typically other administrators) viewing the link management dashboard, especially in environments where 'unfiltered_html' is restricted.
Vulnerable Code
// link-hopper/includes/admin-save.php (approximate location) // Processing the link creation/update form if ( isset( $_POST['hop_name'] ) ) { $hop_name = $_POST['hop_name']; // Vulnerable: Input is not sanitized before being stored in the database // ... database storage logic ... } --- // link-hopper/includes/admin-view.php (approximate location) // Rendering the list of links in the WP-Admin dashboard foreach ( $hops as $hop ) { echo '<tr>'; echo '<td>' . $hop->hop_name . '</td>'; // Vulnerable: Output is not escaped with esc_html() or esc_attr() echo '</tr>'; }
Security Fix
@@ -10,7 +10,7 @@ function lh_save_hop_handler() { if ( isset( $_POST['hop_name'] ) ) { - $hop_name = $_POST['hop_name']; + $hop_name = sanitize_text_field( $_POST['hop_name'] ); // ... database update ... } } @@ -50,7 +50,7 @@ foreach ( $hops as $hop ) { echo '<tr>'; - echo '<td>' . $hop->hop_name . '</td>'; + echo '<td>' . esc_html( $hop->hop_name ) . '</td>'; echo '</tr>'; }
Exploit Outline
To exploit this vulnerability, an attacker requires Administrator-level access on a WordPress site where the `unfiltered_html` capability is disabled (common in Multisite environments). 1. Log in to the WordPress dashboard as an Administrator. 2. Navigate to the Link Hopper management page (e.g., 'Add New Hop' or 'Edit Hop'). 3. In the 'hop_name' field, input a payload such as: <script>alert(document.cookie)</script>. 4. Submit the form to save the link. This stores the malicious script in the database without sanitization. 5. Trigger the execution by navigating to the plugin's overview page where all created links are listed. The script will execute in the context of any user viewing this list.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.