GHSA-xgr6-pqjv-3pf8 Easy!Appointments has unauthenticated customer PII disclosure on booking reschedule page
SummaryThe booking reschedule view at `/index.php/booking/reschedule/{appointment_hash}` (handled by `Booking::index()`) embeds the **entire customer record** as inline JavaScript (`const vars = {... "customer_data": {...}, ...}`) without authentication and without field whitelisting. Anyone in possession of the 12-character `appointment_hash` — which appears in plain text in reschedule emails, confirmation page URLs, and operator-side calendar links — can read every column of that customer's row in the `ea_users` table.Verified against v1.5.2 with a Docker reproduction; a single anonymous GET to the reschedule URL returns 13 customer fields including email, phone, full address, custom fields, timezone, language, LDAP DN, and `id_roles`.DetailsRoot cause`application/controllers/Booking.php` at line 184 reads the hash from the request, fetches the appointment, then loads the customer with `Customers_model::find()` — which returns the full row, no projection. It then passes the full record into `script_vars()`, which inlines it into the response HTML as a JavaScript constant. The reschedule UI itself uses only `first_name` and `last_name`; everything else is exposed for no functional reason.The URL pattern is in the CSRF exemption list (`application/config/config.php`, `csrf_exclude_uris` covers `booking/.*`) and no authentication middleware applies, by design — that's the intended customer-facing reschedule flow. The bug is the over-disclosure, not the lack of auth.Source-to-Sink**Source**: HTTP GET to `/index.php/booking/reschedule/{appointment_hash}` — unauthenticated; hash read via `html_vars('appointment_hash')` (Booking.php:184).**Intermediate**: `appointments_model->get(['hash' => $hash])` → row fetched; `customers_model->find($appointment['id_users_customer'])` returns the full customers row.**Sink**: `script_vars(['customer_data' => $customer, ...])` (Booking.php:254-270) emits inline `const vars = {..., "customer_data": {...}, ...}` JavaScript in the response HTML.Fields disclosedConfirmed in PoC output (canary values used as markers):Fields that would also leak when populated: `mobile_number`, `state`, `notes` (free-form, operators often store sensitive context here), `custom_field_2`–`custom_field_5`, `ldap_dn`.Proof of ConceptReproductionBring up the lab from the project's official `docker-compose.yml` (pinned to v1.5.2). Complete the one-time install at `/index.php/installation` (or `php index.php console install` from the php-fpm container).Book one appointment through the public flow (the bundled `--seed` helper does this automatically and prints the resulting hash).Run the unauthenticated extractor:Observed output (verified 5/5 consecutive runs):The PoC and its docker-compose reproduction environment are attached.ImpactA single anonymous GET request returns the customer's full record. The `appointment_hash` is not a secret to the customer — it appears in every reschedule email, every confirmation page URL, and the operator-side calendar reschedule link. So it leaks through the usual side channels: email forwarding, shared inboxes, mail-server logs, browser history, HTTP `Referer` headers when the customer clicks an outbound link from the reschedule page.For a typical Easy!Appointments deployment (medical clinics, salons, legal/tutoring consultancies, hairdressers) the disclosed fields include regulated personal information — GDPR Article 5(1)(f) / Article 32 confidentiality, HIPAA contact-data exposure, and equivalent regional regimes. The free-form `notes` and the five configurable custom fields are frequently used by operators to store sensitive supplementary data (health context, insurance number, allergies, DOB, government ID).A secondary chain worth flagging: the same response emits `customer_token`, a 600-second cache key bound to the customer ID. If `display_delete_personal_information` is enabled, an attacker holding the hash can also trigger a customer-record deletion at `/privacy/delete_personal_information` using the disclosed token — escalating an information-disclosure issue into a destructive one. Treating that as a secondary concern, out of scope for this report.WorkaroundsOperators can mitigate temporarily by:Disabling the reschedule link in confirmation emails (in `Booking_settings`/`Email_settings` templates), forcing customers to re-book instead.Disabling the public booking page entirely (`disable_booking` setting) for deployments that can tolerate it.Neither workaround removes the root cause; an attacker who already holds a hash can still extract.Suggested fixWhitelist customer fields before inlining. The reschedule UI only needs first/last name:The same pattern likely needs auditing in `Booking_confirmation::of()` and `Booking_cancellation::of()` — anywhere the customer record is loaded and inlined into a publicly-reachable view.CreditsDiscovered through source-code audit by peoplstarReferences`application/controllers/Booking.php` lines 184-270 (v1.5.2)`application/models/Customers_model.php::find` — returns the full row, no projection`application/config/config.php` `csrf_exclude_uris` — whitelisting `booking/.*`Related prior PR #1753 (permission checks on appointment search) — same project, adjacent code, vendor previously accepted this class of issue.[docker-compose.yml](https://github.com/user-attachments/files/27761710/docker-compose.yml)
[easyapp_pii_disclosure.py](https://github.com/user-attachments/files/27761711/easyapp_pii_disclosure.py)
[requirements.txt](https://github.com/user-attachments/files/27761712/requirements.txt)
[consistency_test.txt](https://github.com/user-attachments/files/27761722/consistency_test.txt)
[leaked_customer_data.txt](https://github.com/user-attachments/files/27761723/leaked_customer_data.txt)
CVSS v4.0 score: 6.9