CVE-2026-2419

WP-DownloadManager <= 1.69 - Authenticated (Administrator+) Path Traversal to Arbitrary File Read via 'download_path' Parameter

lowImproper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
2.7
CVSS Score
2.7
CVSS Score
low
Severity
1.69.1
Patched in
1d
Time to patch

Description

The WP-DownloadManager plugin for WordPress is vulnerable to Path Traversal in all versions up to, and including, 1.69 via the 'download_path' configuration parameter. This is due to insufficient validation of the download path setting, which allows directory traversal sequences to bypass the WP_CONTENT_DIR prefix check. This makes it possible for authenticated attackers, with Administrator-level access and above, to configure the plugin to list and access arbitrary files on the server by exploiting the file browser functionality.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.69
PublishedFebruary 17, 2026
Last updatedFebruary 18, 2026
Affected pluginwp-downloadmanager

What Changed in the Fix

Changes introduced in v1.69.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-2419 ## 1. Vulnerability Summary The **WP-DownloadManager** plugin (<= 1.69) contains a path traversal vulnerability in its configuration settings. The plugin attempts to restrict the `download_path` (the directory where downloads are stored) to the `WP_CONTEN…

Show full research plan

Exploitation Research Plan: CVE-2026-2419

1. Vulnerability Summary

The WP-DownloadManager plugin (<= 1.69) contains a path traversal vulnerability in its configuration settings. The plugin attempts to restrict the download_path (the directory where downloads are stored) to the WP_CONTENT_DIR directory. However, the validation logic in download-options.php only checks if the user-provided string starts with the content directory path. It fails to sanitize or check for directory traversal sequences (e.g., ../) following the prefix. This allows an authenticated administrator to escape the intended directory and point the plugin to the server root (e.g., /etc/ or /). Once the path is manipulated, the plugin's file listing and download features can be used to read arbitrary files from the server.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin.php?page=wp-downloadmanager/download-options.php
  • Hook: The options page is registered via add_submenu_page in wp-downloadmanager.php with the manage_downloads capability.
  • Vulnerable Parameter: download_path
  • Authentication: Required (Administrator or user with manage_downloads capability).
  • Preconditions: None, other than administrative access.
  • CSRF Protection: The plugin uses wp_nonce_field('wp-downloadmanager_options') and check_admin_referer('wp-downloadmanager_options').

3. Code Flow

  1. Source (download-options.php):

    • The script receives $_POST['download_path'].
    • It performs a prefix check:
      if ( substr( $download_path, 0, strlen( WP_CONTENT_DIR ) ) !== WP_CONTENT_DIR ) {
          $download_path = WP_CONTENT_DIR;
      }
      
    • If WP_CONTENT_DIR is /var/www/html/wp-content, a payload like /var/www/html/wp-content/../../../../ satisfies the check.
    • The value is saved via update_option('download_path', $download_path).
  2. Sink (download-manager.php and download-add.php):

    • The plugin retrieves the path: $file_path = get_option( 'download_path' );.
    • In the "Add File" or "Edit File" logic, it uses this path to check files:
      $file_size = filesize($file_path.$file);
      
    • When a file is requested on the frontend (via /?dl_id=X), the plugin reads the file from $file_path . $filename and streams it to the user.

4. Nonce Acquisition Strategy

This vulnerability requires an authenticated Administrator. Nonces are required for both updating options and adding files.

  1. Obtaining download-options.php Nonce:

    • Navigate to the Download Options page using browser_navigate.
    • The nonce is generated by wp_nonce_field('wp-downloadmanager_options').
    • Use browser_eval to extract the value from the hidden input:
      document.querySelector('input[name="_wpnonce"]').value
      
  2. Obtaining download-add.php Nonce (if needed to add a file):

    • Navigate to the "Add File" page: wp-admin/admin.php?page=wp-downloadmanager/download-add.php.
    • Extract the nonce for wp-downloadmanager_add-file (inferred action name based on download-manager.php patterns).

5. Exploitation Strategy

Step 1: Update Download Path to Server Root

  • Request: POST /wp-admin/admin.php?page=wp-downloadmanager/download-options.php
  • Body:
    • _wpnonce: [EXTRACTED_NONCE]
    • _wp_http_referer: /wp-admin/admin.php?page=wp-downloadmanager%2Fdownload-options.php
    • download_path: [WP_CONTENT_DIR]/../../../../ (e.g., /var/www/html/wp-content/../../../../ to reach /)
    • Submit: Save Changes
  • Validation: Check if the response contains "Download Path Updated".

Step 2: Add a Sensitive File

  • Request: POST /wp-admin/admin.php?page=wp-downloadmanager/download-add.php (Note: download-add.php handles adding new files).
  • Body:
    • _wpnonce: [EXTRACTED_ADD_NONCE]
    • file_type: 0 (Local file)
    • file: etc/passwd (Relative to our new root /)
    • file_name: Traversed File
    • do: Add File
  • Note: The exact parameters for download-add.php can be verified by observing the form on that page.

Step 3: Trigger Arbitrary File Read

  • Identify the dl_id of the newly created file (usually returned in the URL or displayed in the Manage Downloads table).
  • Request: GET /?dl_id=[ID]
  • Response: The content of /etc/passwd.

6. Test Data Setup

  1. Identify WP_CONTENT_DIR: Use wp eval "echo WP_CONTENT_DIR;" to determine the exact prefix required.
  2. Administrator User: Ensure a user exists with the administrator role.
  3. Plugin Activation: wp plugin activate wp-downloadmanager.

7. Expected Results

  • After Step 1, the download_path option in the database should reflect the traversal string.
  • After Step 2, a new entry in the wp_downloads table should exist with the file column set to etc/passwd.
  • After Step 3, the HTTP response body should contain the string root:x:0:0:root.

8. Verification Steps

  1. Verify Option Change:
    wp option get download_path
  2. Verify File Entry:
    wp db query "SELECT * FROM wp_downloads WHERE file_name = 'Traversed File'"
  3. Verify File Delivery:
    Check the response of the GET /?dl_id=[ID] request for sensitive system data.

9. Alternative Approaches

If "Add File" fails due to directory permissions or complexity:

  • Direct Link Guessing: If the traversal path is set to /, and the plugin uses a predictable file naming/storing convention, check if download-manager.php's "Edit File" mode allows modifying an existing download's file parameter to ../../../../etc/passwd directly.
  • Remote File filesize disclosure: If the plugin refuses to add a file that "doesn't exist," the filesize() call in download-manager.php can be used to verify the existence of files on the system by observing whether the file size is correctly calculated and displayed in the admin UI.
Research Findings
Static analysis — not yet PoC-verified

Summary

WP-DownloadManager versions up to 1.69 are vulnerable to Path Traversal because the 'download_path' configuration check only validates the prefix of the path, allowing directory traversal sequences to escape the intended directory. Authenticated administrators can exploit this to point the plugin to the server root and subsequently read arbitrary system files through the file download functionality.

Vulnerable Code

// download-options.php

$download_path = ! empty( $_POST['download_path'] ) ? sanitize_text_field( $_POST['download_path'] ) : '';

// ... 

// Line 65: Insufficient validation only checks if path starts with WP_CONTENT_DIR
// It does not account for traversal sequences like '../' following the prefix
if ( substr( $download_path, 0, strlen( WP_CONTENT_DIR ) ) !== WP_CONTENT_DIR ) {
    $download_path = WP_CONTENT_DIR;
}

// ... 

update_option('download_path', $download_path);

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-downloadmanager/1.69/download-manager.php /home/deploy/wp-safety.org/data/plugin-versions/wp-downloadmanager/1.69.1/download-manager.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-downloadmanager/1.69/download-manager.php	2024-08-19 13:32:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-downloadmanager/1.69.1/download-manager.php	2026-02-13 01:54:04.000000000 +0000
@@ -139,16 +139,21 @@
 							if( $file_upload_to !== '/' ) {
 								$file_upload_to = $file_upload_to . '/';
 							}
-							if(move_uploaded_file($_FILES['file_upload']['tmp_name'], $file_path.$file_upload_to.basename($_FILES['file_upload']['name']))) {
-								$file = $file_upload_to.basename($_FILES['file_upload']['name']);
-								$file = download_rename_file($file_path, $file);
-								$file_size = filesize($file_path.$file);
+							$validate = wp_check_filetype_and_ext( $_FILES['file_upload']['tmp_name'], basename( $_FILES['file_upload']['name'] ) );
+							if ( $validate['type'] === false ) {
+									$text = '<p style="color: red;">' . __('File type is invalid', 'wp-downloadmanager') . '</p>';
+									break;
+							}
+							if( move_uploaded_file( $_FILES['file_upload']['tmp_name'], $file_path.$file_upload_to . basename( $_FILES['file_upload']['name'] ) ) ) {
+								$file = $file_upload_to . basename( $_FILES['file_upload']['name'] );
+								$file = download_rename_file( $file_path, $file );
+								$file_size = filesize( $file_path . $file );
 							} else {
-								$text = '<p style="color: red;">'.__('Error In Uploading File', 'wp-downloadmanager').'</p>';
+								$text = '<p style="color: red;">' . __('Error In Uploading File', 'wp-downloadmanager') . '</p>';
 								break;
 							}
 						} else {
-							$text = '<p style="color: red;">'.__('Error In Uploading File', 'wp-downloadmanager').'</p>';
+							$text = '<p style="color: red;">' . __('Error In Uploading File', 'wp-downloadmanager') . '</p>';
 							break;
 						}
 					}
@@ -208,21 +213,20 @@
 		case __('Delete File', 'wp-downloadmanager');
 			check_admin_referer('wp-downloadmanager_delete-file');
 			$file_id  = ! empty( $_POST['file_id'] ) ? intval( $_POST['file_id'] ) : 0;
-			$file = ! empty( $_POST['file'] ) ? sanitize_text_field( $_POST['file'] ) : '';
-			$file_name = ! empty( $_POST['file_name'] ) ? sanitize_text_field( $_POST['file_name'] ) : '';
+			$file = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->downloads WHERE file_id = %d", $file_id ) );
 			$unlinkfile = ! empty( $_POST['unlinkfile'] ) ? intval( $_POST['unlinkfile'] ) : 0;
-			if($unlinkfile == 1) {
-				if(!unlink($file_path.$file)) {
-					$text = '<p style="color: red;">'.sprintf(__('Error In Deleting File \'%s (%s)\' From Server', 'wp-downloadmanager'), $file_name, $file).'</p>';
+			if ( $unlinkfile === 1 ) {
+				if ( ! unlink( $file_path . $file->file ) ) {
+					$text = '<p style="color: red;">' . sprintf( __( 'Error In Deleting File \'%s (%s)\' From Server', 'wp-downloadmanager' ), $file->file_name, $file->file ) . '</p>';
 				} else {
-					$text = '<p style="color: green;">'.sprintf(__('File \'%s (%s)\' Deleted From Server Successfully', 'wp-downloadmanager'), $file_name, $file).'</p>';
+					$text = '<p style="color: green;">' . sprintf( __( 'File \'%s (%s)\' Deleted From Server Successfully', 'wp-downloadmanager' ), $file->file_name, $file->file ) . '</p>';
 				}
 			}
-			$deletefile = $wpdb->query("DELETE FROM $wpdb->downloads WHERE file_id = $file_id");
-			if(!$deletefile) {
-				$text .= '<p style="color: red;">'.sprintf(__('Error In Deleting File \'%s (%s)\'', 'wp-downloadmanager'), $file_name, $file).'</p>';
+			$deletefile = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->downloads WHERE file_id = %d", $file->file_id ) );
+			if ( ! $deletefile ) {
+				$text .= '<p style="color: red;">' . sprintf( __('Error In Deleting File \'%s (%s)\'', 'wp-downloadmanager'), $file->file_name, $file->file) . '</p>';
 			} else {
-				$text .= '<p style="color: green;">'.sprintf(__('File \'%s (%s)\' Deleted Successfully', 'wp-downloadmanager'), $file_name, $file).'</p>';
+				$text .= '<p style="color: green;">' . sprintf( __('File \'%s (%s)\' Deleted Successfully', 'wp-downloadmanager'), $file->file_name, $file->file) . '</p>';
 			}
 			break;
 	}
@@ -376,9 +380,7 @@
 		<?php if(!empty($text)) { echo '<!-- Last Action --><div id="message" class="updated fade"><p>'.stripslashes($text).'</p></div>'; } ?>
 		<!-- Delete A File -->
 		<form method="post" action="<?php echo admin_url('admin.php?page='.plugin_basename(__FILE__)); ?>">
-			<input type="hidden" name="file_id" value="<?php echo intval($file->file_id); ?>" />
-			<input type="hidden" name="file" value="<?php echo esc_attr( removeslashes( $file->file ) ); ?>" />
-			<input type="hidden" name="file_name" value="<?php echo esc_attr( removeslashes( $file->file_name ) ); ?>" />
+			<input type="hidden" name="file_id" value="<?php echo esc_attr( intval( $file->file_id ) ); ?>" />
 			<?php wp_nonce_field('wp-downloadmanager_delete-file'); ?>
 			<div class="wrap">
 				<h2><?php _e('Delete A File', 'wp-downloadmanager'); ?></h2>
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-downloadmanager/1.69/download-options.php /home/deploy/wp-safety.org/data/plugin-versions/wp-downloadmanager/1.69.1/download-options.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-downloadmanager/1.69/download-options.php	2025-05-16 01:50:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-downloadmanager/1.69.1/download-options.php	2026-02-13 01:54:04.000000000 +0000
@@ -39,7 +39,10 @@
     $download_options = array('use_filename' => $download_options_use_filename, 'rss_sortby' => $download_options_rss_sortby, 'rss_limit' => $download_options_rss_limit);
     
     // Validate
-    if ( substr( $download_path, 0, strlen( WP_CONTENT_DIR ) ) !== WP_CONTENT_DIR ) {
+    $real_download_path = realpath( $download_path );
+    $real_wp_content_dir = realpath( WP_CONTENT_DIR );
+
+    if ( false === $real_download_path || false === $real_wp_content_dir || strpos( $real_download_path . DIRECTORY_SEPARATOR, $real_wp_content_dir ) !== 0 || strpos( $download_path, '../' ) !== false ) {
         $download_path = WP_CONTENT_DIR;
     }

Exploit Outline

1. Authenticate to the WordPress admin dashboard as an Administrator (or a user with the 'manage_downloads' capability). 2. Navigate to the plugin's 'Download Options' page to obtain a valid nonce for the 'wp-downloadmanager_options' action. 3. Send a POST request to 'wp-admin/admin.php?page=wp-downloadmanager/download-options.php' to update the 'download_path' setting. The payload should include the required WP_CONTENT_DIR prefix followed by enough '../' sequences to reach the server's root directory (e.g., '/var/www/html/wp-content/../../../../'). 4. Navigate to the 'Add File' page and register a new download. Set the 'File Type' to 'Local File' and provide the relative path to a sensitive file (e.g., 'etc/passwd' if the download path was set to '/'). 5. Identify the newly created file's ID (dl_id) and access the frontend download endpoint (e.g., '/?dl_id=[ID]') to receive the content of the traversed file.

Check if your site is affected.

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