CVE-2026-52841
Easy!Appointments: Authorization bypass in Google OAuth provider binding lets any backend user rebind a peer provider's Google sync
Easy!Appointments is a self hosted appointment scheduler. In versions prior to 1.6.0, 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. Version 1.6.0 patches the issue.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- High
- User interaction
- Required
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
3.1
Easy!Appointments: Authorization bypass in Google OAuth provider binding lets any backend user rebind a peer provider's Google sync
Easy!Appointments is a self hosted appointment scheduler. In versions prior to 1.6.0, 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. Version 1.6.0 patches the issue.
- Record
CVE-2026-52841- CVSS 3.1
- 3.1 Low
CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
EUVDEUVD-2026-43723
3.1
Easy!Appointments is a self hosted appointment scheduler. In versions prior to 1.6.0, 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. Version 1.6.0 patches the issue.
- Record
EUVD-2026-43723- CVSS 3.1
- 3.1 Low
CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-8hm4-r66f-29wr
3.1
Easy!Appointments: Authorization bypass in Google OAuth provider binding lets any backend user rebind a peer provider's Google sync
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
// 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());
}
// 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:
// 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_savedatapplication/libraries/Synchronization.php:51runs on every booking save. The path includes the unauthenticated public booking flow (Booking::registeratapplication/controllers/Booking.php:463) and the backend save (Calendar::save_appointmentatapplication/controllers/Calendar.php:306). When$provider['settings']['google_sync']is truthy the handler readsgoogle_tokenfrom the row - now the attacker's - refreshes it, and callsGoogle_sync::add_appointment.Google_sync::add_appointmentatapplication/libraries/Google_sync.php:184-189adds the customer as a Google calendar attendee with their first name, last name, and email.- The cron-triggered
Console::sync->Google::sync($provider_id)atapplication/controllers/Google.php:44walks the existingsync_past_daysandsync_future_dayswindows and…
- Record
GHSA-8hm4-r66f-29wr- CVSS 3.1
- 3.1 Low
CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
NVD
3.1
Easy!Appointments is a self hosted appointment scheduler. In versions prior to 1.6.0, 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. Version 1.6.0 patches the issue.
- Record
CVE-2026-52841- CVSS 3.1
- 3.1 Low
CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSV
3.1
Easy!Appointments: Authorization bypass in Google OAuth provider binding lets any backend user rebind a peer provider's Google sync
Easy!Appointments is a self hosted appointment scheduler. In versions prior to 1.6.0, 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. Version 1.6.0 patches the issue.
- Record
CVE-2026-52841- CVSS 3.1
- 3.1 Low
CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSVGHSA-8hm4-r66f-29wr
3.1
Easy!Appointments: Authorization bypass in Google OAuth provider binding lets any backend user rebind a peer provider's Google sync
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
// 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());
}
// 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:
// 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_savedatapplication/libraries/Synchronization.php:51runs on every booking save. The path includes the unauthenticated public booking flow (Booking::registeratapplication/controllers/Booking.php:463) and the backend save (Calendar::save_appointmentatapplication/controllers/Calendar.php:306). When$provider['settings']['google_sync']is truthy the handler readsgoogle_tokenfrom the row - now the attacker's - refreshes it, and callsGoogle_sync::add_appointment.Google_sync::add_appointmentatapplication/libraries/Google_sync.php:184-189adds the customer as a Google calendar attendee with their first name, last name, and email.- The cron-triggered
Console::sync->Google::sync($provider_id)atapplication/controllers/Google.php:44walks the existingsync_past_daysandsync_future_dayswindows and…
- Record
GHSA-8hm4-r66f-29wr- CVSS 3.1
- 3.1 Low
CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
alextselegidis/easyappointmentsPackagist |
through 1.5.2 | None known |
alextselegidis/easyappointmentsPackagist |
through 1.5.2, plus listed versions 1.1.01.1.0-beta.11.1.0-beta.21.1.11.2.0 Show 18 moreShow less1.2.0-alpha.11.2.0-beta.11.2.11.3.01.3.0-alpha.11.3.0-beta.11.3.0-beta.21.3.11.3.1-beta.11.3.21.3.2-beta.11.4.01.4.0-beta.11.4.11.4.21.4.2-beta.11.4.31.4.3-beta.1 |
None known |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-52841 is a low vulnerability (CVSS 3.1) in Alextselegidis Easyappointments, published July 14, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.21%, the 10th percentile. No source names a fixed version.
References
github.com/alextselegidis/easyappointments/security/advisories/GHSA-8hm4-r66f-29wr
github.com/alextselegidis/easyappointments/commit/4b2d245d2cd2058dc76e05f6eb65b26699268471
nvd.nist.gov/vuln/detail/CVE-2026-52841
github.com/alextselegidis/easyappointments/releases/tag/1.6.0
github.com/advisories/GHSA-8hm4-r66f-29wr
github.com/CVEProject/cvelistV5/tree/main/cves/2026/52xxx/CVE-2026-52841.json
github.com/alextselegidis/easyappointments
Left out here: the rest of 2 source descriptions. The JSON record has them.