CVE-2026-49766

WP User Manager – User Profile Builder & Membership <= 2.9.16 - Authenticated (Subscriber+) Arbitrary File Deletion

highImproper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
8.1
CVSS Score
8.1
CVSS Score
high
Severity
2.9.17
Patched in
6d
Time to patch

Description

The WP User Manager – User Profile Builder & Membership plugin for WordPress is vulnerable to arbitrary file deletion due to insufficient file path validation in all versions up to, and including, 2.9.16. This makes it possible for authenticated attackers, with Subscriber-level access and above, to delete arbitrary files on the server, which can easily lead to remote code execution when the right file is deleted (such as wp-config.php).

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=2.9.16
PublishedJune 5, 2026
Last updatedJune 10, 2026
Affected pluginwp-user-manager

What Changed in the Fix

Changes introduced in v2.9.17

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan targets **CVE-2026-49766**, an arbitrary file deletion vulnerability in **WP User Manager** (<= 2.9.16). The vulnerability stems from a path traversal flaw in the plugin's file handling logic, allowing authenticated users to delete sensitive system files. ### 1. Vulnerability Sum…

Show full research plan

This research plan targets CVE-2026-49766, an arbitrary file deletion vulnerability in WP User Manager (<= 2.9.16). The vulnerability stems from a path traversal flaw in the plugin's file handling logic, allowing authenticated users to delete sensitive system files.

1. Vulnerability Summary

The vulnerability exists because the plugin provides an AJAX endpoint for removing uploaded files that accepts a file path without sufficient validation. While intended for users to manage their own profile uploads, the lack of path sanitization (e.g., checking against wp_upload_dir()) and inadequate ownership checks allow an attacker to traverse the directory structure and delete arbitrary files.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: wpum_remove_file (Handled by the WPUM_AJAX class, typically in includes/class-wpum-ajax.php).
  • Vulnerable Parameter: file (The file system path to be deleted).
  • Authentication: Authenticated, Subscriber level or higher.
  • Preconditions: The plugin must be active, and the attacker must have a valid session and a valid nonce.

3. Code Flow

  1. Entry Point: The user sends a POST request to admin-ajax.php with action=wpum_remove_file.
  2. Nonce Verification: The handler calls check_ajax_referer( 'wpum_remove_file', 'nonce' ).
  3. Input Retrieval: The code retrieves the path from $_POST['file'].
  4. The Sink: The plugin calls unlink( $file ) or wp_delete_file( $file ) using the raw input.
  5. The Flaw: Because there is no check ensuring the path remains within the uploads directory, an input like /var/www/html/wp-config.php or ../../../../wp-config.php is processed, leading to the deletion of the target file.

4. Nonce Acquisition Strategy

WP User Manager localizes nonces for its AJAX functions on pages where profile management occurs.

  1. Trigger Script Loading: The wpum_remove_file nonce is typically included in the wpum_script_data object on the account management or profile editing page.
  2. Setup: Use a page containing the [wpum_account] or [wpum_profile_editor] shortcode.
  3. Extraction:
    • Navigate to the page as an authenticated Subscriber.
    • Use browser_eval to extract the nonce: window.wpum_script_data?.remove_file_nonce.
    • Inferred identifiers: If wpum_script_data is not present, check wpum_vars or wpum_ajax_options.

5. Exploitation Strategy

The goal is to delete a non-critical file first to prove the concept, then potentially target wp-config.php.

  • Target File: /var/www/html/poc.txt
  • Method: POST
  • URL: {{BASE_URL}}/wp-admin/admin-ajax.php
  • Payload (URL-encoded):
    action=wpum_remove_file&nonce={{NONCE}}&file=/var/www/html/poc.txt
    
  • Steps:
    1. Log in as a Subscriber.
    2. Access the page with the [wpum_account] shortcode.
    3. Extract the remove_file_nonce using the browser tools.
    4. Send the http_request (via Playwright tool) with the payload.

6. Test Data Setup

Prepare the environment to verify the deletion safely:

  1. Create Subscriber: wp user create attacker attacker@example.com --role=subscriber --user_pass=password
  2. Create Test Page: wp post create --post_type=page --post_title="My Account" --post_status=publish --post_content='[wpum_account]'
  3. Create PoC File: touch /var/www/html/poc.txt (Ensure the web server user has write/delete permissions on this file).

7. Expected Results

  • Response: The AJAX request should return a JSON object: {"success":true}.
  • FileSystem Impact: The file /var/www/html/poc.txt should no longer exist.
  • Critical Impact: Deleting wp-config.php would trigger the WordPress installation screen (/wp-admin/setup-config.php) upon the next page refresh.

8. Verification Steps

After the exploit attempt, verify using WP-CLI:

  1. Check File Persistence:
    wp eval "echo file_exists('/var/www/html/poc.txt') ? 'FAILED: File exists' : 'SUCCESS: File deleted';"
  2. Check Site Integrity: If wp-config.php was targeted, verify the site returns a 200 OK but redirects to the setup page.

9. Alternative Approaches

If the wpum_remove_file action is not the culprit:

  • Check Profile Update: In class-wpum-field-file.php, look at how the current_ + $key values are handled during profile save. If the plugin attempts to "clean up" the old file when a new one is set, you can set the "current" file path to wp-config.php in the profile update request.
  • Search for unlinks:
    grep -rn "unlink" includes/ --include="*.php"
    Check if any unlink call uses meta data derived from the File field type without validation.
Research Findings
Static analysis — not yet PoC-verified

Summary

Authenticated users (Subscriber and above) can delete arbitrary files on the server by exploiting a path traversal vulnerability in how the plugin handles file metadata. By injecting a traversal path into avatar or cover image parameters during registration or profile updates, the plugin stores a malicious file path which is subsequently deleted when the user triggers a file removal or update operation.

Vulnerable Code

// includes/fields/types/class-wpum-field-file.php:81
public function get_posted_field( $key, $field ) {
    $file = $this->upload_file( $key, $field );
    if ( ! $file ) {
        $file = parent::get_posted_field( 'current_' . $key, $field );
    } elseif ( is_array( $file ) ) {
        $file = array_filter( array_merge( $file, (array) parent::get_posted_field( 'current_' . $key, $field ) ) );
    }
    return $file;
}

---

// includes/forms/class-wpum-form-registration.php:575
if ( isset( $values['register']['user_cover']['url'] ) ) {
    carbon_set_user_meta( $user->ID, 'user_cover', $values['register']['user_cover']['url'] );
    update_user_meta( $user->ID, '_user_cover_path', $values['register']['user_cover']['path'] );
}

if ( isset( $values['register']['user_avatar']['url'] ) ) {
    carbon_set_user_meta( $user->ID, 'current_user_avatar', $values['register']['user_avatar']['url'] );
    update_user_meta( $user->ID, '_current_user_avatar_path', $values['register']['user_avatar']['path'] );
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-user-manager/2.9.16/includes/fields/types/class-wpum-field-file.php /home/deploy/wp-safety.org/data/plugin-versions/wp-user-manager/2.9.17/includes/fields/types/class-wpum-field-file.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-user-manager/2.9.16/includes/fields/types/class-wpum-field-file.php	2026-02-24 10:02:16.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-user-manager/2.9.17/includes/fields/types/class-wpum-field-file.php	2026-05-11 20:04:04.000000000 +0000
@@ -82,8 +82,20 @@
 		$file = $this->upload_file( $key, $field );
 		if ( ! $file ) {
 			$file = parent::get_posted_field( 'current_' . $key, $field );
+			// Reject array values from POST — only scalar (string URL) is valid.
+			if ( is_array( $file ) ) {
+				$file = '';
+			}
 		} elseif ( is_array( $file ) ) {
-			$file = array_filter( array_merge( $file, (array) parent::get_posted_field( 'current_' . $key, $field ) ) );
+			$current = parent::get_posted_field( 'current_' . $key, $field );
+			// Only merge scalar (string) current values. Array values from POST
+			// could inject arbitrary paths — reject them silently.
+			if ( is_array( $current ) ) {
+				$current = '';
+			}
+			if ( $current ) {
+				$file = array_filter( array_merge( $file, array( 'url' => $current ) ) );
+			}
 		}
 		return $file;
 	}
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-user-manager/2.9.16/includes/forms/class-wpum-form-registration.php /home/deploy/wp-safety.org/data/plugin-versions/wp-user-manager/2.9.17/includes/forms/class-wpum-form-registration.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-user-manager/2.9.16/includes/forms/class-wpum-form-registration.php	2026-02-24 10:02:16.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-user-manager/2.9.17/includes/forms/class-wpum-form-registration.php	2026-05-11 20:04:04.000000000 +0000
@@ -514,6 +514,11 @@
 				return false;
 			}
 
+			// Security: reject array values for avatar/cover POST fields to prevent path injection.
+			if ( ( isset( $_POST['current_user_avatar'] ) && is_array( $_POST['current_user_avatar'] ) ) || ( isset( $_POST['current_user_cover'] ) && is_array( $_POST['current_user_cover'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already verified above.
+				throw new Exception( esc_html__( 'Invalid input.', 'wp-user-manager' ) );
+			}
+
 			$return = $this->validate_fields( $values );
 			if ( is_wp_error( $return ) ) {
 				throw new Exception( $return->get_error_message() );
@@ -572,14 +577,24 @@
 				$user->set_role( $this->role );
 			}
 
+			$upload_dir = wp_upload_dir()['basedir'];
+
 			if ( isset( $values['register']['user_cover']['url'] ) ) {
-				carbon_set_user_meta( $user->ID, 'user_cover', $values['register']['user_cover']['url'] );
-				update_user_meta( $user->ID, '_user_cover_path', $values['register']['user_cover']['path'] );
+				$cover_path = $values['register']['user_cover']['path'] ?? '';
+				// Only store path if it is within the uploads directory and contains no traversal.
+				if ( $cover_path && false === strpos( $cover_path, '..' ) && 0 === strpos( $cover_path, $upload_dir ) ) {
+					carbon_set_user_meta( $user->ID, 'user_cover', esc_url_raw( $values['register']['user_cover']['url'] ) );
+					update_user_meta( $user->ID, '_user_cover_path', $cover_path );
+				}
 			}
 
 			if ( isset( $values['register']['user_avatar']['url'] ) ) {
-				carbon_set_user_meta( $user->ID, 'current_user_avatar', $values['register']['user_avatar']['url'] );
-				update_user_meta( $user->ID, '_current_user_avatar_path', $values['register']['user_avatar']['path'] );
+				$avatar_path = $values['register']['user_avatar']['path'] ?? '';
+				// Only store path if it is within the uploads directory and contains no traversal.
+				if ( $avatar_path && false === strpos( $avatar_path, '..' ) && 0 === strpos( $avatar_path, $upload_dir ) ) {
+					carbon_set_user_meta( $user->ID, 'current_user_avatar', esc_url_raw( $values['register']['user_avatar']['url'] ) );
+					update_user_meta( $user->ID, '_current_user_avatar_path', $avatar_path );
+				}
 			}

Exploit Outline

An attacker logs in as a Subscriber and navigates to the account management page to retrieve a 'wpum_remove_file' nonce. The attacker then submits a POST request to wp-admin/admin-ajax.php using the action 'wpum_remove_file' and a 'file' parameter containing a path traversal string (e.g., '../../../../wp-config.php'). Alternatively, the attacker can manipulate profile update or registration requests to include an array-based payload for file fields (e.g., user_avatar[path]=/var/www/html/wp-config.php). This causes the plugin to store the target file path in the user's meta fields. When the user later updates their profile or removes the associated file, the plugin uses the stored meta value as a parameter for unlink(), deleting the targeted system file.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.