Описание
Easy!Appointments has unauthenticated customer PII disclosure on booking reschedule page
Summary
The 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.
Details
Root 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 viahtml_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 inlineconst vars = {..., "customer_data": {...}, ...}JavaScript in the response HTML.
Fields disclosed
Confirmed 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 Concept
Reproduction
- Bring 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(orphp index.php console installfrom the php-fpm container). - Book one appointment through the public flow (the bundled
--seedhelper does this automatically and prints the resulting hash). - Run the unauthenticated extractor:
python3 poc_001_easyapp_pii_disclosure.py --target http://localhost:8000 --hash <HASH>
- Observed output (verified 5/5 consecutive runs):
email = 'victim.disclosure@example.invalid' phone_number = '+1-555-0100' address = '100 Privacy Lane' city = 'Sensitiveville' zip_code = '00001' custom_field_1 = 'CFLD1-CANARY' [+] DISCLOSURE CONFIRMED -- 6 PII field(s) accessible without auth.
The PoC and its docker-compose reproduction environment are attached.
Impact
A 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.
Workarounds
Operators can mitigate temporarily by:
- Disabling the reschedule link in confirmation emails (in
Booking_settings/Email_settingstemplates), forcing customers to re-book instead. - Disabling the public booking page entirely (
disable_bookingsetting) for deployments that can tolerate it.
Neither workaround removes the root cause; an attacker who already holds a hash can still extract.
Suggested fix
Whitelist 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.
Credits
- Discovered through source-code audit by peoplstar
References
application/controllers/Booking.phplines 184-270 (v1.5.2)application/models/Customers_model.php::find— returns the full row, no projectionapplication/config/config.phpcsrf_exclude_uris— whitelistingbooking/.*- Related prior PR #1753 (permission checks on appointment search) — same project, adjacent code, vendor previously accepted this class of issue.
docker-compose.yml easyapp_pii_disclosure.py requirements.txt consistency_test.txt leaked_customer_data.txt
Ссылки
- https://github.com/alextselegidis/easyappointments/security/advisories/GHSA-xgr6-pqjv-3pf8
- https://nvd.nist.gov/vuln/detail/CVE-2026-52837
- https://github.com/alextselegidis/easyappointments/commit/40bb0b31b531540bc9006efce4220eb0a437ed2b
- https://github.com/alextselegidis/easyappointments/releases/tag/1.6.0
Пакеты
alextselegidis/easyappointments
<= 1.5.2
Отсутствует
Связанные уязвимости
Easy!Appointments is a self hosted appointment scheduler. In versions up to and including 1.5.2, the 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. Version 1.6.0 contains a patch.