CVE-2026-0909

WP ULike <= 4.8.3.1 - Insecure Direct Object Reference to Authenticated (Subscriber+) Arbitrary Log Deletion via 'id' Parameter

mediumAuthorization Bypass Through User-Controlled Key
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
5.0.0
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=4.8.3.1
PublishedFebruary 2, 2026
Last updatedFebruary 3, 2026
Affected pluginwp-ulike

What Changed in the Fix

Changes introduced in v5.0.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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_api
    • id: 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 the wp-ulike action.
  • Authentication: Authenticated user (Subscriber+) with the stats capability.
  • Preconditions:
    • The attacker must have the capability assigned to the stats access level (configurable in WP ULike settings, but described as available to Subscriber+).
    • The attacker must obtain a valid nonce.

3. Code Flow

  1. 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');
  2. 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 (often read or a custom one if configured for Subscribers).
    • WP_ULIKE_SLUG is the nonce action string (defined as 'wp-ulike').
  3. Parameter Retrieval:
    $item_id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
    $type    = isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : '';
    
  4. Vulnerable Sink:
    $settings = new wp_ulike_setting_type( $type );
    $instance = new wp_ulike_logs( $settings->getTableName()  );
    if( ! $instance->delete_row( $item_id ) ){ ... }
    
    The delete_row method (likely inside includes/classes/class-wp-ulike-logs.php) executes a deletion based purely on the id, without checking the user_id column 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.

  1. Check Accessibility: The statistics page is located at wp-admin/admin.php?page=wp-ulike-statistics.
  2. Trigger Localization: The script wp_ulike_admin_react is enqueued and localized when visiting this page.
  3. Execution:
    • Log in as the Subscriber user.
    • Use browser_navigate to go to http://localhost:8080/wp-admin/admin.php?page=wp-ulike-statistics.
    • Use browser_eval to extract the nonce:
      window.StatsAppConfig?.nonce
      
  4. Action String: Verification uses WP_ULIKE_SLUG ('wp-ulike'), which matches the localization.

5. Exploitation Strategy

  1. Target Identification: Find a log entry id created by another user (e.g., the Administrator).
  2. Request Construction:
    • Method: GET (as the code uses $_GET for parameters)
    • URL: http://localhost:8080/wp-admin/admin-ajax.php
    • Query Params:
      • action=wp_ulike_delete_history_api
      • id=[TARGET_LOG_ID]
      • type=post
      • _wpnonce=[EXTRACTED_NONCE]
  3. Verification: The response should be a JSON success object: {"success":true}.

6. Test Data Setup

  1. Install Plugin: Install and activate wp-ulike.
  2. Create Content: Create a public post.
  3. Generate Victim Logs:
    • As the Admin, "Like" the post. This creates a log entry in the wp_ulike table.
  4. 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
  5. 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: read is the base Subscriber capability).
  6. Login: Authenticate the agent as the Subscriber.

7. Expected Results

  • The AJAX request returns {"success":true}.
  • The log entry with the specific id is removed from the database.
  • The "Like" count on the post for the Victim user is effectively deleted.

8. Verification Steps

  1. 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)
  2. 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=post is not used, try type=comment or type=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 $_REQUEST is used in wp_ulike_is_valid_nonce and if the server accepts POST for this action. (The source shows $_GET, so stick to GET).
Research Findings
Static analysis — not yet PoC-verified

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

--- /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/4.8.3.1/admin/admin-ajax.php	2025-12-28 12:32:12.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.0/admin/admin-ajax.php	2026-02-01 09:28:40.000000000 +0000
@@ -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.