Every CVE Search/ Get API key

CVE-2026-52841

Easy!Appointments: Authorization bypass in Google OAuth provider binding lets any backend user rebind a peer provider's Google sync

3.1 Low

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

0.21%Up 0.1 points since Jul 15, 2026
90%50%10%1%

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 LowCVSS: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 LowCVSS: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_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…
Record
GHSA-8hm4-r66f-29wr
CVSS 3.1
3.1 LowCVSS: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 LowCVSS: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 LowCVSS: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_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…
Record
GHSA-8hm4-r66f-29wr
CVSS 3.1
3.1 LowCVSS: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

Affected packages
PackageAffectedFixed
alextselegidis/easyappointments
Packagist
through 1.5.2 None known
alextselegidis/easyappointments
Packagist
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 less
1.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

History
RecordedChangedSources
first indexedCVE 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

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open