🛡️ CVE-2026-52841 — easyappointments
Description
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
```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_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 pushes every appointment to the attacker's calendar. - The same loop deletes the local row whenever the remote event throws or is
cancelled(application/controllers/Google.php:186-191); the attacker rolls events out of their Google calendar to delete the victim provider's appointments. Events the attacker creates in their own calendar arrive as unavailability records on the victim's schedule (application/controllers/Google.php:209-254).
Proof of concept
Setup
1. Clone the repository, pin to the audited release, copy the sample config, and bring up the bundled stack:
```bash
git clone https://github.com/alextselegidis/easyappointments
cd easyappointments
git checkout 1.5.2
cp config-sample.php config.php
docker compose up -d
until curl -fsS http://localhost/ -o /dev/null; do sleep 2; done
```
2. Run the console install
How this vulnerability can be exploited
This issue can be reached over the network, attack complexity is high, an attacker needs administrative privileges on the target. A user must be tricked into taking some action. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality low, integrity low, availability none.
Weakness class
CVE-2026-52841 is classified as CWE-639: Authorization Bypass Through User-Controlled Key. An object is selected by an identifier from the request without checking the caller owns it.
Affected software
CVE-2026-52841 is recorded against 2 packages.
- alextselegidis/easyappointments
- unknown
Timeline and source
Published on 29 July 2026. No public exploit is currently recorded for this entry. Record sourced from NVD.
References
github.com (Web)
nvd.nist.gov (Advisory)
github.com (Web)
github.com (Package)
github.com (Web)
Details
CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:N
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| alextselegidis/easyappointments | — | — |
| unknown | — | — |
Similar Threats
- Unknown CVE-2026-52837
- Low CVE-2026-52838
- Low CVE-2026-52839
- Low CVE-2026-52840
- High CVE-2026-55651
More CVE 2026 advisories
Browse all of CVE 2026 in the advisory index.
Free Vulnerability Check
Is your site affected by CVE-2026-52841?
BotEraser helps you identify potentially vulnerable plugins and themes by checking your installation against CVE-2026-52841 and other known CVE records.
Scan My Site Free →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.