CVE-2026-52841
Easy!Appointments: Authorization bypass in Google OAuth provider binding lets any backend user rebind a peer provider's Google sync
Summary
### Summary `Google::oauth` at `application/controllers/Google.php:278` stores its URL-supplied `provider_id` in the session, and `oauth_callback` saves the issued Google OAuth token against that row without checking the caller owns the provider. Any logged-in backend user (admin, provider, or secretary) rebinds a peer provider's Google sync to a Google account they control. The peer's appointments then sync into the attacker's calendar with each customer's name and email attached as attendee data. ### Preconditions - Attacker holds a backend login on the target instance (admin, provider, or secretary). The customer role cannot log in. - The instance has Google Calendar OAuth configured at `application/config/google.php` - i.e. any deployment that uses the Google sync feature at all. - Default deployment per the project's own `docker-compose.yml`; no non-default flags required. ### Details ```php // application/controllers/Google.php:278-289 public function oauth(string $provider_id): void { if (!$this->session->userdata('user_id')) { show_error('Forbidden', 403); } // Store the provider id for use on the callback function. session(['oauth_provider_id' => $provider_id]); // (*) attacker-chosen id stored unchecked // Redirect browser to google user content page. header('Location: ' . $this->google_sync->get_auth_url()); } ``` ```php // application/controllers/Google.php:305-337 public function oauth_callback(): void { if (!session('user_id')) { abort(403, 'Forbidden'); } $code = request('code'); if (empty($code)) { response('Code authorization failed.'); return; } $token = $this->google_sync->authenticate($code); if (empty($token)) { response('Token authorization failed.'); return; } $oauth_provider_id = session('oauth_provider_id'); if ($oauth_provider_id) { $this->providers_model->set_setting($oauth_provider_id, 'google_sync', true); // (*) $this->providers_model->set_setting($oauth_provider_id, 'google_token', json_encode($token)); // (*) $this->providers_model->set_setting($oauth_provider_id, 'google_calendar', 'primary'); } else { response('Sync provider id not specified.'); } } ``` The same controller already carries the right gate on every other sync-management entry. `select_google_calendar` at `application/controllers/Google.php:389` and `disable_provider_sync` at `application/controllers/Google.php:423` both refuse the call when the caller is neither an admin nor the provider themselves: ```php // application/controllers/Google.php:389 if (cannot('edit', PRIV_USERS) && (int) $user_id !== (int) $provider_id) { throw new RuntimeException('You do not have the required permissions for this task.'); } ``` `oauth` and `oauth_callback` skip that check. Once the callback runs with `oauth_provider_id` pointing at a peer provider, the peer's `user_settings` row is overwritten with the attacker's OAuth token and `google_sync` is forcibly enabled. The attack chain that delivers the data: - `Synchronization::sync_appointment_saved` at `application/libraries/Synchronization.php:51` runs on every booking save. The path includes the unauthenticated public booking flow (`Booking::register` at `application/controllers/Booking.php:463`) and the backend save (`Calendar::save_appointment` at `application/controllers/Calendar.php:306`). When `$provider['settings']['google_sync']` is truthy the handler reads `google_token` from the row - now the attacker's - refreshes it, and calls `Google_sync::add_appointment`. - `Google_sync::add_appointment` at `application/libraries/Google_sync.php:184-189` adds the customer as a Google calendar attendee with their first name, last name, and email. - The cron-triggered `Console::sync` -> `Google::sync($provider_id)` at `application/controllers/Google.php:44` walks the existing `sync_past_days` and `sync_future_days` windows and pushe
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| Packagist | alextselegidis/easyappointments | — | — |
Remediation: No patched version is listed by GitHub.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.
CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.
EPSS scores provided by the FIRST.org Exploit Prediction Scoring System.