The Exploit
Unauthenticated attackers can delete any media attachment by POSTing the attachment ID to the exposed REST endpoint.
curl -i -X POST "https://target.example.com/wp-json/bfe/v1/revert" \
-H "Content-Type: text/plain" \
--data "123"
The server accepts the request without login and returns a 200-level response with an empty body. The side effect is that attachment ID 123 is deleted from the WordPress Media Library.
What the Patch Did
Before:
public static function revert_file( \WP_REST_Request $request ) {
$attachment_id = intval( $request->get_body() );
if ( !empty( $attachment_id ) && $attachment_id ) {
$deleted = wp_delete_attachment( $attachment_id, true );
}
}
After:
public static function revert_file( \WP_REST_Request $request ) {
self::start_session_if_needed();
$attachment_id = intval( $request->get_body() );
if ( empty( $attachment_id ) ) {
return new \WP_Error('invalid_id', __( 'Invalid attachment ID.', 'front-editor' ), [
'status' => 400,
]);
}
$authorized = false;
$is_guest_upload = false;
if ( is_user_logged_in() ) {
if ( current_user_can( 'delete_post', $attachment_id ) ) {
$authorized = true;
}
}
if ( !$authorized && !empty( $_SESSION['bfe_uploaded_files'] ) && in_array( $attachment_id, $_SESSION['bfe_uploaded_files'] ) ) {
$authorized = true;
$is_guest_upload = true;
}
if ( !$authorized ) {
return new \WP_Error('rest_forbidden', __( 'Sorry, you are not allowed to delete this attachment.', 'front-editor' ), [
'status' => 403,
]);
}
$deleted = wp_delete_attachment( $attachment_id, true );
if ( $deleted && $is_guest_upload ) {
$index = array_search( $attachment_id, $_SESSION['bfe_uploaded_files'] );
if ( $index !== false ) {
unset($_SESSION['bfe_uploaded_files'][$index]);
}
}
}
The patch added an authorization gate using current_user_can( 'delete_post', $attachment_id ), plus a guest-upload ownership check against $_SESSION['bfe_uploaded_files']. It also validates the attachment ID and returns standard WP_Error responses instead of blindly deleting media.
Root Cause
This is a broken object-level authorization bug (CWE-639). The endpoint POST /wp-json/bfe/v1/revert reads attacker-controlled input from $request->get_body() and passes the resulting attachment ID directly into wp_delete_attachment( $attachment_id, true ) without any capability or ownership check. The vulnerability crosses the security boundary between unauthenticated REST consumers and destructive media deletion.
Why It Works
The load-bearing fix is the current_user_can( 'delete_post', $attachment_id ) authorization check. If that line is removed, the endpoint still allows arbitrary attachment deletion for logged-in users, and because there was no other authorization branch controlling unauthenticated access, the bug remains exploitable. The session startup and guest-upload $_SESSION['bfe_uploaded_files'] check are supporting controls: they enable safe guest deletion and make the endpoint behave correctly for non-logged-in uploads. The ID validation line prevents accidental garbage input from reaching wp_delete_attachment, but it is not the primary defense.
Hardening Checklist
- Use WordPress REST permission callbacks or
current_user_can()to guard destructive REST endpoints before callingwp_delete_attachment(). - Validate IDs with
intval()and reject empty values early usingWP_Errorand a 400 status. - For guest-upload workflows, store ownership in server-side state and verify
$_SESSION['bfe_uploaded_files']before deleting attachments. - Avoid performing destructive actions based solely on unauthenticated request body content; require either capability checks or explicit ownership validation.
- Return explicit authorization failures (
rest_forbidden, HTTP 403) instead of silently doing nothing.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-13419