WP ULike <= 4.8.3.1 - Insecure Direct Object Reference to Authenticated (Subscriber+) Arbitrary Log Deletion via 'id' Parameter
Description
The WP ULike plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 4.8.3.1. This is due to the `wp_ulike_delete_history_api` AJAX action not verifying that the log entry being deleted belongs to the current user. This makes it possible for authenticated attackers, with Subscriber-level access and above (granted the 'stats' capability is assigned to their role), to delete arbitrary log entries belonging to other users via the 'id' parameter.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=4.8.3.1What Changed in the Fix
Changes introduced in v5.0.0
Source Code
WordPress.org SVN# Vulnerability Research Plan: CVE-2026-0909 (WP ULike IDOR Log Deletion) ## 1. Vulnerability Summary The **WP ULike** plugin for WordPress is vulnerable to an **Insecure Direct Object Reference (IDOR)** in the `wp_ulike_delete_history_api` function. This function is an AJAX handler registered for …
Show full research plan
Vulnerability Research Plan: CVE-2026-0909 (WP ULike IDOR Log Deletion)
1. Vulnerability Summary
The WP ULike plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) in the wp_ulike_delete_history_api function. This function is an AJAX handler registered for the wp_ajax_wp_ulike_delete_history_api action. The vulnerability exists because the code fails to verify that the log entry (identified by the id parameter) actually belongs to the user requesting the deletion. Any user with the stats capability (which can be assigned to Subscriber-level users) can delete arbitrary engagement logs belonging to any user across the site.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
wp_ajax_wp_ulike_delete_history_api - Parameters:
action:wp_ulike_delete_history_apiid: The integer ID of the log entry to delete.type: The type of log (e.g.,post,comment,activity,topic)._wpnonce: A valid nonce for thewp-ulikeaction.
- Authentication: Authenticated user (Subscriber+) with the
statscapability. - Preconditions:
- The attacker must have the capability assigned to the
statsaccess level (configurable in WP ULike settings, but described as available to Subscriber+). - The attacker must obtain a valid nonce.
- The attacker must have the capability assigned to the
3. Code Flow
- Entry Point: In
admin/admin-ajax.php, the hook is registered:add_action('wp_ajax_wp_ulike_delete_history_api','wp_ulike_delete_history_api'); - Permission Check: The function
wp_ulike_delete_history_api()checks:if( ! current_user_can( wp_ulike_get_user_access_capability('stats') ) || ! wp_ulike_is_valid_nonce( WP_ULIKE_SLUG ) ){ wp_send_json_error( ... ); }wp_ulike_get_user_access_capability('stats')returns the capability required to access stats (oftenreador a custom one if configured for Subscribers).WP_ULIKE_SLUGis the nonce action string (defined as'wp-ulike').
- Parameter Retrieval:
$item_id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; $type = isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : ''; - Vulnerable Sink:
The$settings = new wp_ulike_setting_type( $type ); $instance = new wp_ulike_logs( $settings->getTableName() ); if( ! $instance->delete_row( $item_id ) ){ ... }delete_rowmethod (likely insideincludes/classes/class-wp-ulike-logs.php) executes a deletion based purely on theid, without checking theuser_idcolumn of the log entry.
4. Nonce Acquisition Strategy
The nonce is required for the wp-ulike action. It is localized for the React-based Statistics application.
- Check Accessibility: The statistics page is located at
wp-admin/admin.php?page=wp-ulike-statistics. - Trigger Localization: The script
wp_ulike_admin_reactis enqueued and localized when visiting this page. - Execution:
- Log in as the Subscriber user.
- Use
browser_navigateto go tohttp://localhost:8080/wp-admin/admin.php?page=wp-ulike-statistics. - Use
browser_evalto extract the nonce:window.StatsAppConfig?.nonce
- Action String: Verification uses
WP_ULIKE_SLUG('wp-ulike'), which matches the localization.
5. Exploitation Strategy
- Target Identification: Find a log entry
idcreated by another user (e.g., the Administrator). - Request Construction:
- Method:
GET(as the code uses$_GETfor parameters) - URL:
http://localhost:8080/wp-admin/admin-ajax.php - Query Params:
action=wp_ulike_delete_history_apiid=[TARGET_LOG_ID]type=post_wpnonce=[EXTRACTED_NONCE]
- Method:
- Verification: The response should be a JSON success object:
{"success":true}.
6. Test Data Setup
- Install Plugin: Install and activate
wp-ulike. - Create Content: Create a public post.
- Generate Victim Logs:
- As the Admin, "Like" the post. This creates a log entry in the
wp_uliketable.
- As the Admin, "Like" the post. This creates a log entry in the
- Identify Log ID: Use WP-CLI to find the ID of the log just created:
wp db query "SELECT id FROM wp_ulike ORDER BY id DESC LIMIT 1;" --skip-column-names - Setup Attacker:
- Create a Subscriber user.
- Configure WP ULike to allow Subscribers to access stats (if not default):
wp option patch insert wp_ulike_settings stats_access_capability read(Note:readis the base Subscriber capability).
- Login: Authenticate the agent as the Subscriber.
7. Expected Results
- The AJAX request returns
{"success":true}. - The log entry with the specific
idis removed from the database. - The "Like" count on the post for the Victim user is effectively deleted.
8. Verification Steps
- Database Check: After the exploit, verify the log is gone:
wp db query "SELECT COUNT(*) FROM wp_ulike WHERE id = [TARGET_LOG_ID];"
(Expected: 0) - UI Check: Check if the Admin's like is still registered in the stats via WP-CLI:
wp db query "SELECT * FROM wp_ulike;"
9. Alternative Approaches
- Different Types: If
type=postis not used, trytype=commentortype=activity(if BuddyPress is installed). - Brute Force IDs: Since it's an IDOR, an attacker can iterate through IDs 1-1000 to clear the entire engagement history.
- POST vs GET: Although the code explicitly uses
$_GET['id'], check if$_REQUESTis used inwp_ulike_is_valid_nonceand if the server acceptsPOSTfor this action. (The source shows$_GET, so stick to GET).
Summary
The WP ULike plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) via the 'wp_ulike_delete_history_api' AJAX action. Authenticated users with the 'stats' capability (which can be assigned to Subscriber roles) can delete arbitrary engagement log entries because the plugin fails to verify the ownership or administrative status of the user requesting the deletion of the log entry identified by the 'id' parameter.
Vulnerable Code
// admin/admin-ajax.php lines 105-127 function wp_ulike_delete_history_api(){ if( ! current_user_can( wp_ulike_get_user_access_capability('stats') ) || ! wp_ulike_is_valid_nonce( WP_ULIKE_SLUG ) ){ wp_send_json_error( esc_html__( 'Error: You do not have permission to do that.', 'wp-ulike' ) ); } $item_id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; $type = isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : ''; if( empty( $item_id ) || empty( $type ) ){ wp_send_json_error( esc_html__( 'Error: You do not have permission to do that.', 'wp-ulike' ) ); } $settings = new wp_ulike_setting_type( $type ); $instance = new wp_ulike_logs( $settings->getTableName() ); if( ! $instance->delete_row( $item_id ) ){ wp_send_json_error( esc_html__( 'Error: You do not have permission to do that.', 'wp-ulike' ) ); } wp_send_json_success(); }
Security Fix
@@ -76,7 +76,7 @@ * @return void */ function wp_ulike_delete_history_api(){ - if( ! current_user_can( wp_ulike_get_user_access_capability('stats') ) || ! wp_ulike_is_valid_nonce( WP_ULIKE_SLUG ) ){ + if( ! current_user_can( 'manage_options' ) || ! wp_ulike_is_valid_nonce( WP_ULIKE_SLUG ) ){ wp_send_json_error( esc_html__( 'Error: You do not have permission to do that.', 'wp-ulike' ) ); }
Exploit Outline
1. Authenticate as a user with the 'stats' capability (e.g., a Subscriber user if the 'stats_access_capability' is configured for low-level roles in the plugin settings). 2. Obtain a valid security nonce by visiting the plugin's statistics page (/wp-admin/admin.php?page=wp-ulike-statistics) and extracting the 'nonce' value from the localized 'StatsAppConfig' JavaScript object. 3. Identify the integer 'id' of an engagement log entry to be deleted (e.g., an administrator's 'Like' record). 4. Send a GET request to the AJAX endpoint (/wp-admin/admin-ajax.php) with the following parameters: 'action' set to 'wp_ulike_delete_history_api', 'id' set to the target log ID, 'type' set to the log type (e.g., 'post', 'comment'), and '_wpnonce' set to the extracted nonce. 5. The plugin will delete the specified record from the engagement tables regardless of which user created it, effectively manipulating interaction statistics.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.